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

1094 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: "<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:
```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 <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**
```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<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**
```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.