diff --git a/.pi/extensions/goal-driven-loop/state-injector.ts b/.pi/extensions/goal-driven-loop/state-injector.ts new file mode 100644 index 0000000..d6b0794 --- /dev/null +++ b/.pi/extensions/goal-driven-loop/state-injector.ts @@ -0,0 +1,138 @@ +/** + * 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) || []; +}