Files
pi-skills/.pi/extensions/goal-driven-loop/state-injector.ts
T
2026-06-10 17:34:33 +08:00

139 lines
4.0 KiB
TypeScript

/**
* State injection for goal-driven-loop.
*
* Reads .agents/goal/GOAL.md and injects goal state into
* the agent's context via before_agent_start.
*/
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
export interface GoalState {
goal: string;
status: "in-progress" | "blocked" | "complete";
iteration: number;
totalCriteria: number;
passedCriteria: number;
created: string;
updated: string;
blocker?: string;
progressLog: string[];
}
/**
* Parse GOAL.md frontmatter and body to extract goal state.
*/
export function readGoalState(cwd: string): GoalState | null {
const goalPath = join(cwd, ".agents", "goal", "GOAL.md");
if (!existsSync(goalPath)) {
return null;
}
const content = readFileSync(goalPath, "utf-8");
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) {
return null;
}
const frontmatter = frontmatterMatch[1];
const goal = extractField(frontmatter, "goal") || "";
const status =
(extractField(frontmatter, "status") as GoalState["status"]) ||
"in-progress";
const iteration = parseInt(
extractField(frontmatter, "iteration") || "0",
10
);
const created = extractField(frontmatter, "created") || "";
const updated = extractField(frontmatter, "updated") || "";
const criteriaSection = extractSection(content, "Acceptance Criteria");
const totalCriteria = countCheckboxes(criteriaSection);
const passedCriteria = countCheckedBoxes(criteriaSection);
const blockerSection = extractSection(content, "Blocked");
const blocker = blockerSection.trim() || undefined;
const progressSection = extractSection(content, "Progress Log");
const progressLog = extractIterations(progressSection);
return {
goal,
status,
iteration,
totalCriteria,
passedCriteria,
created,
updated,
blocker,
progressLog,
};
}
/**
* Build the injection text for the system prompt or message.
*/
export function buildInjectionText(state: GoalState): string {
if (state.status === "complete") {
return `[goal-driven-loop] GOAL COMPLETE
${state.goal}
Final: ${state.passedCriteria}/${state.totalCriteria} criteria passed across ${state.iteration} iterations.
GOAL.md is being cleaned up.`;
}
if (state.status === "blocked" && state.blocker) {
return `[goal-driven-loop] GOAL BLOCKED
${state.goal}
Blocker: ${state.blocker}
Progress: ${state.passedCriteria}/${state.totalCriteria} criteria passed, iteration ${state.iteration}.
Continue by trying a different strategy. If still blocked after 2 attempts, surface to the user.`;
}
return `[goal-driven-loop] ACTIVE GOAL
${state.goal}
Progress: ${state.passedCriteria}/${state.totalCriteria} criteria passed, iteration ${state.iteration}
Next step: read .agents/goal/GOAL.md and continue the loop.`;
}
/**
* Build a continuation prompt for context tension situations.
*/
export function buildContinuationPrompt(state: GoalState): string {
return `🔄 目标状态已保存至 .agents/goal/GOAL.md
当前进度:${state.passedCriteria}/${state.totalCriteria} criteria 通过,第 ${state.iteration} 轮迭代。`;
}
// ---- helpers ----
function extractField(frontmatter: string, field: string): string | null {
const match = frontmatter.match(new RegExp(`^${field}:\\s*(.*)$`, "m"));
if (!match) return null;
let value = match[1].trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
return value;
}
function extractSection(content: string, sectionName: string): string {
const regex = new RegExp(`## ${sectionName}\\n([\\s\\S]*?)(?=\\n## |$)`);
const match = content.match(regex);
return match ? match[1] : "";
}
function countCheckboxes(section: string): number {
return (section.match(/^- \[[ x]\]/gm) || []).length;
}
function countCheckedBoxes(section: string): number {
return (section.match(/^- \[x\]/gim) || []).length;
}
function extractIterations(section: string): string[] {
return section.match(/### Iteration \d+/g) || [];
}