/** * Deterministic structural expansion of a rough goal into a plan-ready shape. * * This is intentionally non-LLM: the extension turns a free-form goal string * into a consistent structure the model can refine. LLM judgment (whether the * goal is achieved) belongs in packages like @ricoyudog/pi-goal-hermes. */ import { renderPlan, type GoalPlan } from "./template.ts"; export interface ExpandedGoal { /** Original user text, trimmed. */ raw: string; /** One-line restated objective. */ objective: string; /** Concrete, checkable acceptance criteria. */ acceptanceCriteria: string[]; /** Smaller intermediate outcomes that support the objective. */ subgoals: string[]; /** Likely risks / failure modes to watch. */ risks: string[]; /** Questions the agent or user should answer before/while executing. */ openQuestions: string[]; /** Suggested next action for the agent. */ nextAction: string; } const SENTENCE_SPLIT = /(?<=[.!?])\s+|\n+/; function clean(text: string): string { return text.replace(/\s+/g, " ").trim(); } function splitClauses(raw: string): string[] { const parts = raw .split(SENTENCE_SPLIT) .map(clean) .filter((p) => p.length > 0); if (parts.length === 0) return []; if (parts.length === 1) { // Also split on "; " and " and " when there's only one sentence-ish blob. const semi = parts[0]! .split(/\s*;\s+/) .map(clean) .filter(Boolean); if (semi.length > 1) return semi; } return parts; } function asObjective(raw: string): string { const first = splitClauses(raw)[0] ?? raw; let objective = first; // Drop trailing period for a clean objective line. objective = objective.replace(/[.]+$/, ""); // Capitalize first letter. if (objective.length > 0) { objective = objective[0]!.toUpperCase() + objective.slice(1); } return objective; } function defaultCriteria(objective: string): string[] { return [ `The work described by "${objective}" is complete and verified (tests or manual check pass).`, "Changes are committed or staged with a clear summary of what changed.", "No known regressions introduced by the change (relevant checks pass).", ]; } function defaultSubgoals(objective: string, clauses: string[]): string[] { if (clauses.length > 1) { return clauses.slice(0, 6).map((c, i) => { const body = c.replace(/[.]+$/, ""); return `${i + 1}. ${body[0]!.toUpperCase()}${body.slice(1)}`; }); } return [ `1. Clarify scope and constraints for: ${objective}`, "2. Inspect the relevant codebase / environment", "3. Implement the smallest change that satisfies the objective", "4. Verify with tests or a concrete manual check", "5. Summarize results and remaining risks", ]; } function defaultRisks(objective: string): string[] { return [ "Scope creep beyond the stated objective", "Missing or outdated assumptions about the current codebase", `Incomplete verification that "${objective}" is actually done`, ]; } function defaultQuestions(objective: string): string[] { return [ "What does done look like in one sentence?", "Are there hard constraints (time, deps, APIs, backwards compatibility)?", `What should NOT be changed while pursuing: ${objective}?`, ]; } /** * Expand a free-form goal string into a structured plan skeleton. * Throws if the goal is empty/whitespace. */ export function expandGoal(rawInput: string): ExpandedGoal { const raw = clean(rawInput); if (!raw) { throw new Error("goal must be a non-empty string"); } const clauses = splitClauses(raw); const objective = asObjective(raw); return { raw, objective, acceptanceCriteria: defaultCriteria(objective), subgoals: defaultSubgoals(objective, clauses), risks: defaultRisks(objective), openQuestions: defaultQuestions(objective), nextAction: "Confirm or edit the objective and acceptance criteria, then start with subgoal 1.", }; } /** Render an ExpandedGoal as markdown suitable for LLM or human consumption. */ export function formatExpandedGoal(goal: ExpandedGoal): string { const lines: string[] = [ "## Expanded goal", "", `**Objective:** ${goal.objective}`, "", "### Acceptance criteria", ...goal.acceptanceCriteria.map((c) => `- [ ] ${c}`), "", "### Subgoals", ...goal.subgoals.map((s) => `- ${s}`), "", "### Risks", ...goal.risks.map((r) => `- ${r}`), "", "### Open questions", ...goal.openQuestions.map((q) => `- ${q}`), "", `### Next action`, goal.nextAction, "", `### Raw input`, `> ${goal.raw}`, ]; return lines.join("\n"); } /** * Expand a free-form goal into a full GoalPlan (judged contract + guidance). */ export function expandToPlan(rawInput: string): GoalPlan { const expanded = expandGoal(rawInput); const checklist = expanded.subgoals.map((s) => ({ text: s.replace(/^\d+\.\s*/, ""), done: false, })); return { headline: expanded.objective, objective: expanded.objective, goalKind: "code-change", acceptanceCriteria: expanded.acceptanceCriteria, verificationPlan: [ { tag: "gating", text: "Run relevant automated tests or listed manual checks; all must pass.", }, { tag: "evidence", text: "Evidence index lists an artifact row per acceptance criterion.", }, { tag: "gating", text: "No open blocking risks remain for the stated objective.", }, ], nonGoals: [ "Unrelated refactors or drive-by cleanups", "Scope beyond the stated objective without an explicit plan deviation", ], assumedScope: [ `Work is limited to what is needed to achieve: ${expanded.objective}`, "Existing project conventions and tooling are preferred over new stacks", ], subgoals: expanded.subgoals.map((title) => ({ title })), implementationApproach: [ expanded.nextAction, ...expanded.openQuestions.map((q) => `Resolve: ${q}`), ], taskChecklist: checklist, risks: expanded.risks, deviations: [], }; } /** Convenience: expand raw goal → full plan markdown. */ export function expandToPlanMarkdown(rawInput: string): string { return renderPlan(expandToPlan(rawInput)); }