/** * 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 blockerRaw = blockerSection.trim() || undefined; const blocker = blockerRaw ? blockerRaw.replace(/^[-*]\s*/, "").trim() || undefined : 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) || []; }