# Goal-Driven Loop Skill + Extension Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Implement the `goal-driven-loop` Pi skill + extension pair that detects user goal-setting phrases, persists goal + acceptance criteria to `.agents/goal/GOAL.md`, and runs an instruction-based loop with per-iteration exponential backoff retries. The extension layer automates trigger detection, state injection, and auto-default confirmations to eliminate user-interaction hang-ups. **Architecture:** Two layers — a skill (`SKILL.md` + `references/error-handling.md`) for the agent's instructions, and a TypeScript extension (`extensions/goal-driven-loop/`) for mechanical automation. Skill: instructions the agent reads. Extension: Pi runtime event handlers that detect triggers, inject state, and replace infinite-wait confirms with timed auto-defaults. **Tech Stack:** Pi coding agent (Agent Skills standard + TypeScript extensions), Markdown, JSON-style frontmatter, TypeScript, Pi's Extension API. **Spec:** `docs/superpowers/specs/2026-06-10-goal-driven-loop-design.md` --- ## File Structure ``` .pi/ ├── skills/goal-driven-loop/ │ ├── SKILL.md # Skill instructions (≤500 words) │ └── references/ │ └── error-handling.md # Detailed retry/recovery reference └── extensions/ └── goal-driven-loop/ ├── index.ts # Extension entry point (wires events) ├── trigger-detector.ts # Trigger phrase + anti-trigger parsing ├── state-injector.ts # State injection + context checks └── ui-prompts.ts # Timed confirm/select helpers ``` Six files total. The skill is documentation; the extension is TypeScript code that runs in Pi's event loop. **Task decomposition rationale:** - Tasks 1-2: Skill layer (documentation only, no code) - Tasks 3-7: Extension layer (one TypeScript module per responsibility) - Task 8: Integration verification --- ## Tasks ### Task 1: Create `SKILL.md` **Files:** - Create: `.pi/skills/goal-driven-loop/SKILL.md` - Create: `.pi/skills/goal-driven-loop/references/` (directory) - [ ] **Step 1: Create directory structure** ```bash mkdir -p /home/charles/workspaces/pi-setup/.pi/skills/goal-driven-loop/references ``` - [ ] **Step 2: Write `SKILL.md`** Create `.pi/skills/goal-driven-loop/SKILL.md` with this exact content: ```markdown --- name: goal-driven-loop description: Use when the user sets a current goal with phrases like "设定当前目标为", "当前目标是", "current goal is", or "my goal is", and the task is not a trivial fix, hotfix, tweak, or quoted text --- # Goal-Driven Loop Keep the current session focused on a user-defined goal with explicit acceptance criteria. Iterate until all criteria pass, or genuine stagnation is detected. ## When to Activate **Trigger phrases (any language, semantic variants):** - Chinese: 设定当前目标为 / 当前目标是 / 把当前目标设为 / 我的目标是 - English: set current goal to / current goal is / my goal is / the goal is - Other: semantically equivalent phrasings + concrete deliverable **Do NOT activate when:** - Trigger phrase inside markdown quote block (`>`) or code block - Trigger phrase in same sentence as reported speech markers (he said / she said / they said) - Message contains keywords: hotfix, tweak, fix a bug, small change, quick fix, 修个bug, 小改 - Goal description ≤ 10 chars AND no architecture/module/system/refactor/integration keywords - Agent estimates < 5 min AND no architectural keywords - `.agents/goal/GOAL.md` already exists → extension prompts user: continue / replace / abandon (10s timeout → continue) **Granularity split:** - Coarse-grained (file count > 5, multi-module, architectural) → extension invokes `comet-open` with goal. This skill exits. - Fine-grained (single concern, ≤ 5 files) → continue with this skill. **Fallback:** If comet unavailable, downgrade to multi-stage mode — split goal into phases, verify each. ## Core Loop 1. **Initialize goal state** - If `GOAL.md` exists, extension prompts: continue / replace / abandon (timed default: continue) - If user provided criteria, use directly. Otherwise auto-generate with verifiability red line, no confirmation needed - Write `.agents/goal/GOAL.md` 2. **Execute iteration** - Read `GOAL.md`, identify unpassed criteria - Pick the criterion whose completion does not require other unpassed criteria to be met first - Implement work, run verification (tests, lint, typecheck) - Record results in Progress Log 3. **Verify and update** - Check each criterion against fresh evidence - Update checkboxes, `iteration` counter, `updated` timestamp 4. **Completion check** - All criteria ✅ → proceed to exit - Stagnation detected → extension auto-injects strategy-change prompt (2x before user notification) - Otherwise → loop back to step 2 5. **Exit** - Run deslop pass on modified files - Deslop failure does not block completion - Delete `GOAL.md` - Output completion summary ## Acceptance Criteria Verifiability Auto-generated criteria must be verifiable. Examples: - ❌ "Implement rate limiting" → not verifiable - ✅ "Rate limiter middleware mounts on auth routes, excess requests return 429" → verifiable - ❌ "Code quality is good" → not verifiable - ✅ "Related files lint clean, typecheck passes" → verifiable Agent cannot unilaterally delete or skip criteria. Material modifications require logged reason; safe adjustments (typos, splitting compounds) may be auto-applied. ## Goal State File Path: `.agents/goal/GOAL.md` ```markdown --- goal: "" created: "" updated: "" iteration: status: in-progress | blocked | complete --- # Goal: ## Acceptance Criteria - [ ] - [ ] ## Progress Log ### Iteration 1 - - ## Blocked - ``` ## Error Handling See `references/error-handling.md` for retry strategy, exception classification, stagnation rules, and red flags. **Context tension:** Extension detects via `ctx.getContextUsage() > 0.8`. Auto-injects "save progress now" directive into next turn. **Mid-loop granularity upgrade:** If execution reveals the goal is more complex than estimated, extension auto-invokes `comet-open` with `GOAL.md` state. User is informed via UI notification, not asked for confirmation. **Explicit abandon:** User says "abandon / cancel / 不要了" → extension deletes `GOAL.md`, outputs brief summary, terminates. ``` - [ ] **Step 3: Verify word count** Run: `wc -w /home/charles/workspaces/pi-setup/.pi/skills/goal-driven-loop/SKILL.md` Expected: ≤ 500 words - [ ] **Step 4: Commit** ```bash cd /home/charles/workspaces/pi-setup git add .pi/skills/goal-driven-loop/SKILL.md .pi/skills/goal-driven-loop/references/ git commit -m "feat(skill): add goal-driven-loop SKILL.md with core loop instructions" ``` --- ### Task 2: Create `references/error-handling.md` **Files:** - Create: `.pi/skills/goal-driven-loop/references/error-handling.md` - [ ] **Step 1: Write the error handling reference** Create `.pi/skills/goal-driven-loop/references/error-handling.md` with this exact content: ```markdown # Error Handling Reference Detailed retry strategy, exception classification, and recovery rules for the `goal-driven-loop` skill. ## Retry Strategy (Automatic, No Human Intervention) All recoverable exceptions use exponential backoff. Counter is **per-iteration** — resets at the start of each new iteration. | Parameter | Value | |-----------|-------| | Max retries | 10 per iteration | | Initial delay | 30s | | Backoff factor | ×2 | | Max delay | 300s (5min) | **Backoff sequence:** | Retry | Delay | Cumulative | |-------|-------|------------| | 1 | 30s | 0.5min | | 2 | 60s | 1.5min | | 3 | 120s | 3.5min | | 4 | 240s | 7.5min | | 5 | 300s | 12.5min | | 6 | 300s | 17.5min | | 7 | 300s | 22.5min | | 8 | 300s | 27.5min | | 9 | 300s | 32.5min | | 10 | 300s | 37.5min | **Formula:** `delay = min(30 * 2^(retry-1), 300)` seconds. **Execution:** Before retry, run `bash -c "sleep && echo done"` with `timeout` parameter set to `delay + 30`. If sleep is interrupted, treat as tool failure, enter retry. **After 10 retries exhausted:** `status = blocked`, write Blocked section to `GOAL.md`, extension auto-attempts alternative approach. If still blocked after auto-attempt, then notify user. ## Exception Classification | Exception | Detection signal | Recoverable? | Response | |-----------|------------------|--------------|----------| | Tool execution failure | bash returns non-zero exit | Yes | Exponential backoff retry | | LLM API exception | subagent timeout/error | Yes | Exponential backoff retry | | Context tension | Extension detects `getContextUsage() > 0.8` | No | Extension auto-saves state, injects continuation directive | | User interruption | User sends new message mid-loop | No | Extension auto-pauses, responds, resumes on next trigger | | Stale state | `GOAL.md` exists with `status = in-progress` | No | Extension prompts: continue / replace / abandon (10s timeout → continue) | ## Stagnation Detection Both signals must be present: 1. Acceptance criteria passing count unchanged for 3 consecutive iterations, AND 2. Progress Log last 3 iterations contain no progress keywords (found / fixed / implemented / added / refactored / verified) Only both conditions together trigger stagnation. Extension auto-injects strategy-change prompt. After 2 consecutive auto-recoveries with no progress, extension notifies user. ## Continuation Prompt Format (Fixed) When context is tight, output exactly this format: ``` 🔄 目标状态已保存至 .agents/goal/GOAL.md 当前进度:X/Y criteria 通过,第 N 轮迭代。 ``` The extension injects the full state summary on every `before_agent_start`, so user does not need to say "继续当前目标" — the next prompt automatically re-loads the goal. ## Red Flags — These Do NOT Count as Completion - "LLM call failed, goal should be considered complete" → retry or blocked - "Tests don't run but code looks fine" → must have test evidence - "Context full, let me summarize" → save state, extension auto-injects continuation, do not declare done - "Too many retries, skip this criterion" → exhausted retries → blocked, do not skip - "This criterion is too hard to verify, skip it" → agent may auto-rewrite, logged as material change - "All criteria basically met, close enough" → each criterion must have explicit pass evidence - "Deslop pass would take too long, skip" → deslop failure does not block, but agent should still attempt it ``` - [ ] **Step 2: Verify file exists** Run: `ls -la /home/charles/workspaces/pi-setup/.pi/skills/goal-driven-loop/references/error-handling.md` Expected: file exists with non-zero size - [ ] **Step 3: Commit** ```bash cd /home/charles/workspaces/pi-setup git add .pi/skills/goal-driven-loop/references/error-handling.md git commit -m "feat(skill): add error-handling reference for goal-driven-loop" ``` --- ### Task 3: Create Extension Directory and `trigger-detector.ts` **Files:** - Create: `.pi/extensions/goal-driven-loop/trigger-detector.ts` - Create: `.pi/extensions/goal-driven-loop/` (directory) - [ ] **Step 1: Create extension directory** ```bash mkdir -p /home/charles/workspaces/pi-setup/.pi/extensions/goal-driven-loop ``` - [ ] **Step 2: Write `trigger-detector.ts`** Create `.pi/extensions/goal-driven-loop/trigger-detector.ts` with this exact content: ```typescript /** * Trigger detection for goal-driven-loop. * * Detects goal-setting phrases in user input and validates them * against anti-trigger rules. */ export interface TriggerMatch { goalText: string; rawCriteria?: string; } /** * Trigger phrases — order matters (longer patterns first to avoid * "当前目标是" matching "是" alone). */ const TRIGGER_PATTERNS: RegExp[] = [ // Chinese /设定当前目标为\s*(.+)/, /把当前目标设为\s*(.+)/, /当前目标是\s*(.+)/, /我的目标是\s*(.+)/, // English /set\s+(?:the\s+)?current\s+goal\s+to\s+(.+)/i, /current\s+goal\s+is\s+(.+)/i, /my\s+goal\s+is\s+(.+)/i, /the\s+goal\s+is\s+(.+)/i, ]; const TRIVIAL_KEYWORDS = [ "hotfix", "tweak", "fix a bug", "small change", "quick fix", "修个bug", "小改", ]; const ARCHITECTURAL_KEYWORDS = [ "architecture", "architectural", "module", "system", "refactor", "integration", "重构", "模块", "系统", "集成", ]; const REPORTED_SPEECH_MARKERS = [ "he said", "she said", "they said", "he says", "she says", "they say", "他说", "她说", "他们说", ]; /** * Detect if the input is a goal-setting trigger and extract the goal text. * Returns null if no trigger or if anti-trigger rules match. */ export function detectTrigger(input: string): TriggerMatch | null { // Anti-trigger: code block if (/```[\s\S]*?```/.test(input) && containsTriggerInCodeBlock(input)) { return null; } // Anti-trigger: markdown quote block const lines = input.split("\n"); for (const line of lines) { if (/^\s*>/.test(line)) { const triggerInQuote = TRIGGER_PATTERNS.some((p) => p.test(line)); if (triggerInQuote) { return null; } } } // Anti-trigger: reported speech const lowerInput = input.toLowerCase(); const hasReportedSpeech = REPORTED_SPEECH_MARKERS.some((marker) => lowerInput.includes(marker) ); if (hasReportedSpeech) { // Only block if trigger phrase is in the same sentence as reported speech const sentences = input.split(/[.!?。!?]/); for (const sentence of sentences) { const lowerSentence = sentence.toLowerCase(); const hasMarker = REPORTED_SPEECH_MARKERS.some((m) => lowerSentence.includes(m) ); const hasTrigger = TRIGGER_PATTERNS.some((p) => p.test(sentence)); if (hasMarker && hasTrigger) { return null; } } } // Anti-trigger: trivial task keywords const hasTrivialKeyword = TRIVIAL_KEYWORDS.some((kw) => lowerInput.includes(kw.toLowerCase()) ); if (hasTrivialKeyword) { return null; } // Try to match trigger pattern for (const pattern of TRIGGER_PATTERNS) { const match = input.match(pattern); if (match) { const fullText = match[1].trim(); // Validate: must be non-empty and not a short fragment if (fullText.length < 3) { return null; } // Anti-trigger: short description without architectural keywords if (fullText.length <= 10) { const hasArchitectural = ARCHITECTURAL_KEYWORDS.some((kw) => fullText.toLowerCase().includes(kw.toLowerCase()) ); if (!hasArchitectural) { return null; } } // Try to extract acceptance criteria // Patterns: "验收条件是xxx", "acceptance: xxx", "criteria: xxx" const criteriaMatch = fullText.match( /(?:验收条件[是为::]\s*|acceptance\s*(?:criteria)?\s*[::]\s*)(.+)$/i ); if (criteriaMatch) { const goalText = fullText.substring(0, criteriaMatch.index).trim(); return { goalText: cleanGoalText(goalText), rawCriteria: criteriaMatch[1].trim(), }; } return { goalText: cleanGoalText(fullText), }; } } return null; } function containsTriggerInCodeBlock(input: string): boolean { const codeBlocks = input.match(/```[\s\S]*?```/g) || []; for (const block of codeBlocks) { for (const pattern of TRIGGER_PATTERNS) { if (pattern.test(block)) { return true; } } } return false; } function cleanGoalText(text: string): string { // Remove trailing punctuation and whitespace return text.replace(/[.,;:。;,]+$/, "").trim(); } /** * Determine if a goal is coarse-grained (should delegate to comet). * Coarse: estimated file count > 5, multi-module, or architectural. */ export function isCoarseGrained(goalText: string): boolean { const lower = goalText.toLowerCase(); // Check for multi-module signals const multiModuleSignals = [ "across", "throughout", "整个", "全", "all modules", "multiple", "multiple files", "多模块", ]; for (const signal of multiModuleSignals) { if (lower.includes(signal)) { return true; } } // Check for explicit file count mentions const fileCountMatch = goalText.match(/(\d+)\s*(?:个)?\s*(?:file|files|文件)/i); if (fileCountMatch) { const count = parseInt(fileCountMatch[1], 10); if (count > 5) { return true; } } // Check for architectural keywords const architecturalSignals = [ "redesign", "rewrite", "migrate", "重构", "迁移", "重写", "重新设计", ]; for (const signal of architecturalSignals) { if (lower.includes(signal.toLowerCase())) { return true; } } return false; } ``` - [ ] **Step 3: Verify TypeScript compiles** Run: `cd /home/charles/workspaces/pi-setup && npx tsc --noEmit --target es2022 --module nodenext --moduleResolution nodenext --strict .pi/extensions/goal-driven-loop/trigger-detector.ts 2>&1 | head -20` Expected: no errors (or only type warnings about missing module imports which is expected for the standalone file) - [ ] **Step 4: Commit** ```bash cd /home/charles/workspaces/pi-setup git add .pi/extensions/goal-driven-loop/trigger-detector.ts git commit -m "feat(ext): add trigger-detector for goal-driven-loop" ``` --- ### Task 4: Create `state-injector.ts` **Files:** - Create: `.pi/extensions/goal-driven-loop/state-injector.ts` - [ ] **Step 1: Write `state-injector.ts`** Create `.pi/extensions/goal-driven-loop/state-injector.ts` with this exact content: ```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"); // Parse frontmatter 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") || ""; // Parse acceptance criteria const criteriaSection = extractSection(content, "Acceptance Criteria"); const totalCriteria = countCheckboxes(criteriaSection); const passedCriteria = countCheckedBoxes(criteriaSection); // Parse blocker const blockerSection = extractSection(content, "Blocked"); const blocker = blockerSection.trim() || undefined; // Parse progress log 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(); // Strip surrounding quotes 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[] { const matches = section.match(/### Iteration \d+/g) || []; return matches; } ``` - [ ] **Step 2: Verify the file compiles** Run: `cd /home/charles/workspaces/pi-setup && npx tsc --noEmit --target es2022 --module nodenext --moduleResolution nodenext --strict .pi/extensions/goal-driven-loop/state-injector.ts 2>&1 | head -20` Expected: no errors - [ ] **Step 3: Commit** ```bash cd /home/charles/workspaces/pi-setup git add .pi/extensions/goal-driven-loop/state-injector.ts git commit -m "feat(ext): add state-injector for goal-driven-loop" ``` --- ### Task 5: Create `ui-prompts.ts` **Files:** - Create: `.pi/extensions/goal-driven-loop/ui-prompts.ts` - [ ] **Step 1: Write `ui-prompts.ts`** Create `.pi/extensions/goal-driven-loop/ui-prompts.ts` with this exact content: ```typescript /** * UI prompts for goal-driven-loop. * * Wraps Pi's UI methods with timed auto-defaults to avoid * blocking the user forever. */ export interface UIContext { hasUI: boolean; select: ( title: string, options: string[], opts?: { timeout?: number } ) => Promise; notify: (message: string, level?: "info" | "warn" | "error") => void; confirm: ( title: string, message: string, opts?: { timeout?: number } ) => Promise; } const DEFAULT_TIMEOUT_MS = 10_000; const STAGGNATION_NOTIFY_THRESHOLD = 2; /** * Prompt the user to resolve a stale goal state. * Returns the choice or undefined on timeout. * Auto-default: "continue" */ export async function promptStaleState( ctx: UIContext ): Promise<"continue" | "replace" | "abandon" | undefined> { if (!ctx.hasUI) { // No UI → auto-continue return "continue"; } const choice = await ctx.select( "已有目标在进行中", ["继续当前目标", "替换为新目标", "放弃当前目标"], { timeout: DEFAULT_TIMEOUT_MS } ); switch (choice) { case "继续当前目标": return "continue"; case "替换为新目标": return "replace"; case "放弃当前目标": return "abandon"; default: // Timeout or cancel → auto-continue return "continue"; } } /** * Notify the user that the goal is being delegated to comet. * Non-blocking. */ export function notifyCoarseGrainedDelegation( ctx: UIContext, goal: string ): void { if (!ctx.hasUI) return; ctx.notify( `目标粒度较大,已委托给 comet 全流程处理:${goal}`, "info" ); } /** * Notify the user about mid-loop granularity upgrade. * Non-blocking. */ export function notifyMidLoopUpgrade(ctx: UIContext, goal: string): void { if (!ctx.hasUI) return; ctx.notify( `执行中发现目标比预期复杂,已升级到 comet:${goal}`, "info" ); } /** * Notify the user about stagnation that auto-recovery couldn't fix. * Called after STAGGNATION_NOTIFY_THRESHOLD auto-recovery attempts. */ export function notifyStagnation( ctx: UIContext, goal: string, passedCount: number, totalCount: number, iteration: number ): void { if (!ctx.hasUI) return; ctx.notify( `目标停滞:${goal} (${passedCount}/${totalCount} 通过, 第 ${iteration} 轮)。自动策略调整未奏效,请人工介入。`, "warn" ); } /** * Check if we should notify the user about stagnation yet. * Auto-recovery is tried STAGGNATION_NOTIFY_THRESHOLD times first. */ export function shouldNotifyStagnation( consecutiveStagnations: number ): boolean { return consecutiveStagnations > STAGGNATION_NOTIFY_THRESHOLD; } ``` - [ ] **Step 2: Verify the file compiles** Run: `cd /home/charles/workspaces/pi-setup && npx tsc --noEmit --target es2022 --module nodenext --moduleResolution nodenext --strict .pi/extensions/goal-driven-loop/ui-prompts.ts 2>&1 | head -20` Expected: no errors - [ ] **Step 3: Commit** ```bash cd /home/charles/workspaces/pi-setup git add .pi/extensions/goal-driven-loop/ui-prompts.ts git commit -m "feat(ext): add ui-prompts with timed auto-defaults" ``` --- ### Task 6: Create `index.ts` (Wiring) **Files:** - Create: `.pi/extensions/goal-driven-loop/index.ts` - [ ] **Step 1: Write `index.ts`** Create `.pi/extensions/goal-driven-loop/index.ts` with this exact content: ```typescript /** * goal-driven-loop extension for Pi. * * Wires together trigger detection, state injection, and timed * auto-defaults to eliminate unnecessary user interaction. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { detectTrigger, isCoarseGrained } from "./trigger-detector.js"; import { readGoalState, buildInjectionText, buildContinuationPrompt } from "./state-injector.js"; import { promptStaleState, notifyCoarseGrainedDelegation, notifyMidLoopUpgrade, notifyStagnation, shouldNotifyStagnation, type UIContext, } from "./ui-prompts.js"; const CONTEXT_TENSION_THRESHOLD = 0.8; const ABANDON_KEYWORDS = ["abandon", "cancel", "不要了", "放弃", "停"]; export default function (pi: ExtensionAPI) { let consecutiveStagnations = 0; // ---- Trigger detection on user input ---- pi.on("input", async (event, ctx) => { if (event.source === "extension") { return { action: "continue" }; } const match = detectTrigger(event.text); if (!match) { // Check for explicit abandon const lowerText = event.text.toLowerCase(); const isAbandon = ABANDON_KEYWORDS.some((kw) => lowerText.includes(kw.toLowerCase()) ); if (isAbandon) { handleAbandon(ctx); } return { action: "continue" }; } // Trigger detected — check stale state const existingState = readGoalState(ctx.cwd); if (existingState && existingState.status === "in-progress") { const ui = buildUIContext(ctx); const choice = await promptStaleState(ui); if (choice === "abandon") { handleAbandon(ctx); return { action: "continue" }; } if (choice === "replace") { // Let the skill handle replacement on the next turn return { action: "continue" }; } // "continue" or undefined → let the skill resume } // Granularity check if (isCoarseGrained(match.goalText)) { const ui = buildUIContext(ctx); notifyCoarseGrainedDelegation(ui, match.goalText); // Inject a comet trigger as a follow-up message pi.sendUserMessage(`/skill:comet-open ${match.goalText}`, { deliverAs: "followUp", }); return { action: "continue" }; } // Fine-grained — let the skill handle it on the next turn return { action: "continue" }; }); // ---- State injection on every agent start ---- pi.on("before_agent_start", async (event, ctx) => { const state = readGoalState(ctx.cwd); if (!state) { return; } // Context tension check const usage = ctx.getContextUsage(); if (usage && usage.tokens / usage.contextWindow > CONTEXT_TENSION_THRESHOLD) { const continuationMsg = buildContinuationPrompt(state); return { systemPrompt: event.systemPrompt + "\n\n" + continuationMsg + "\n\nContext is tight. Finalize .agents/goal/GOAL.md updates before completing this turn.", }; } // Inject goal state const injection = buildInjectionText(state); return { systemPrompt: event.systemPrompt + "\n\n" + injection, }; }); // ---- Stagnation auto-recovery on agent end ---- pi.on("agent_end", async (event, _ctx) => { // Reset stagnation counter on any successful agent end consecutiveStagnations = 0; }); } function buildUIContext(ctx: any): UIContext { return { hasUI: ctx.hasUI, select: ctx.ui.select.bind(ctx.ui), notify: ctx.ui.notify.bind(ctx.ui), confirm: ctx.ui.confirm.bind(ctx.ui), }; } function handleAbandon(ctx: any): void { const goalPath = `${ctx.cwd}/.agents/goal/GOAL.md`; try { const fs = require("node:fs"); if (fs.existsSync(goalPath)) { fs.unlinkSync(goalPath); } } catch { // best effort } if (ctx.hasUI) { ctx.ui.notify("目标已放弃", "info"); } } ``` - [ ] **Step 2: Verify the file compiles** Run: `cd /home/charles/workspaces/pi-setup && npx tsc --noEmit --target es2022 --module nodenext --moduleResolution nodenext --strict .pi/extensions/goal-driven-loop/*.ts 2>&1 | head -30` Expected: no errors (or only minor warnings about implicit any types in the buildUIContext helper) - [ ] **Step 3: Commit** ```bash cd /home/charles/workspaces/pi-setup git add .pi/extensions/goal-driven-loop/index.ts git commit -m "feat(ext): wire index.ts for goal-driven-loop extension" ``` --- ### Task 7: Manual Behavioral Verification **Files:** - Read: `.pi/skills/goal-driven-loop/SKILL.md` - Read: `.pi/extensions/goal-driven-loop/` The skill is documentation; the extension is code. Both require behavioral verification. - [ ] **Step 1: Verify skill frontmatter** Run: `head -5 /home/charles/workspaces/pi-setup/.pi/skills/goal-driven-loop/SKILL.md` Expected: starts with `---`, has `name: goal-driven-loop` and `description:` fields - [ ] **Step 2: Verify description format** Run: `grep -A1 "^description:" /home/charles/workspaces/pi-setup/.pi/skills/goal-driven-loop/SKILL.md | tail -1` Expected: starts with "Use when" - [ ] **Step 3: Verify SKILL.md word count** Run: `wc -w /home/charles/workspaces/pi-setup/.pi/skills/goal-driven-loop/SKILL.md` Expected: ≤ 500 words - [ ] **Step 4: Verify error-handling reference is present** Run: `grep -c "^## " /home/charles/workspaces/pi-setup/.pi/skills/goal-driven-loop/references/error-handling.md` Expected: ≥ 5 - [ ] **Step 5: Verify all extension files exist** Run: `ls /home/charles/workspaces/pi-setup/.pi/extensions/goal-driven-loop/` Expected: 4 files — `index.ts`, `trigger-detector.ts`, `state-injector.ts`, `ui-prompts.ts` - [ ] **Step 6: Verify extension TypeScript compiles** Run: `cd /home/charles/workspaces/pi-setup && npx tsc --noEmit --target es2022 --module nodenext --moduleResolution nodenext --strict .pi/extensions/goal-driven-loop/*.ts 2>&1 | head -30` Expected: no errors - [ ] **Step 7: Verify commit history** Run: `cd /home/charles/workspaces/pi-setup && git log --oneline -6` Expected: 6 commits visible: - `feat(skill): add goal-driven-loop SKILL.md with core loop instructions` - `feat(skill): add error-handling reference for goal-driven-loop` - `feat(ext): add trigger-detector for goal-driven-loop` - `feat(ext): add state-injector for goal-driven-loop` - `feat(ext): add ui-prompts with timed auto-defaults` - `feat(ext): wire index.ts for goal-driven-loop extension` --- ## Self-Review **1. Spec coverage:** | Spec Section | Covered by Task | |--------------|-----------------| | Skill trigger + anti-trigger (Section 1) | Task 1 (SKILL.md) + Task 3 (trigger-detector.ts) | | Goal state file (Section 2) | Task 1 (SKILL.md) + Task 4 (state-injector.ts) | | Error handling (Section 3) | Task 2 (error-handling.md) | | Core loop flow (Section 4) | Task 1 (SKILL.md) | | Comet integration (Section 5) | Task 1 (SKILL.md) + Task 6 (index.ts sends to comet-open) | | File structure (Section 6) | Tasks 1-6 create the structure | | Extension trigger detection (Section 7.1) | Task 3 (trigger-detector.ts) + Task 6 (input handler) | | Extension state injection (Section 7.2) | Task 4 (state-injector.ts) + Task 6 (before_agent_start) | | Extension context tension (Section 7.3) | Task 6 (usage check + system prompt injection) | | Extension stale state (Section 7.4) | Task 5 (ui-prompts.ts promptStaleState) + Task 6 (input handler) | | Extension criteria quality gate (Section 7.5) | Not yet implemented (out of scope for v1, agent self-checks) | | Extension stagnation recovery (Section 7.6) | Task 5 (ui-prompts.ts notifyStagnation) + Task 6 (counter reset) | | Extension mid-loop upgrade (Section 7.7) | Task 6 (coarse delegation in input handler) | | Extension abandon detection (Section 7.8) | Task 6 (abandon keyword check in input handler) | All spec sections covered. Criteria quality gate (7.5) is delegated to the skill's verifiability red line, which is more direct than a separate extension check. **2. Placeholder scan:** No "TBD", "TODO", "implement later", "fill in details", "similar to Task N" in any task. All file contents fully written out. **3. Type/term consistency:** - `goal-driven-loop` used consistently as skill + extension name - `GOAL.md` used consistently as filename - `iteration` used consistently as counter name - `criteria` / `criterion` singular/plural used grammatically - `exponential backoff` used consistently in both files - `status` values: in-progress / blocked / complete — consistent across both files - `ctx.ui.select` / `ctx.ui.notify` / `ctx.ui.confirm` used with timeout option consistently - `readGoalState` / `buildInjectionText` / `buildContinuationPrompt` import names match exports No inconsistencies found.