0869060284
- trigger-detector: strip code blocks before pattern matching (triggers outside code blocks now fire) - trigger-detector: use code-stripped content for quote and reported-speech anti-trigger checks - state-injector: strip markdown list prefix from blocker text Tests: 27/27 trigger-detector, 17/17 state-injector passing
142 lines
4.1 KiB
TypeScript
142 lines
4.1 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 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) || [];
|
|
}
|