Files
pi-skills/docs/superpowers/plans/2026-06-10-goal-driven-loop.md
T

34 KiB
Raw Blame History

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

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:

---
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: "<goal text>"
created: "<ISO 8601>"
updated: "<ISO 8601>"
iteration: <int>
status: in-progress | blocked | complete
---

# Goal: <goal text>

## Acceptance Criteria
- [ ] <criterion 1>
- [ ] <criterion 2>

## Progress Log
### Iteration 1
- <work done>
- <verification results>

## Blocked
- <blocker, or delete section if empty>

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:

# 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 <delay> && 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
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

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:

/**
 * 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
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:

/**
 * 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
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:

/**
 * 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<string | undefined>;
  notify: (message: string, level?: "info" | "warn" | "error") => void;
  confirm: (
    title: string,
    message: string,
    opts?: { timeout?: number }
  ) => Promise<boolean>;
}

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
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:

/**
 * 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
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.