diff --git a/docs/specs/execution-pipeline.md b/docs/specs/execution-pipeline.md new file mode 100644 index 0000000..b32f236 --- /dev/null +++ b/docs/specs/execution-pipeline.md @@ -0,0 +1,107 @@ +# SPEC: fable-agent execution pipeline + +**Date:** 2026-07-04 +**Status:** draft — pending approval +**Author:** harness build session + +## Problem + +Five harness components exist in isolation. None are wired together. The pipeline is the 20% that delivers 80%. + +## Existing components (all built, all tested) + +| Component | File | Lines | Role | +|---|---|---|---| +| WorktreeManager | `worktree-isolation.ts` | 170 | Create/remove disposable git worktrees | +| buildRepoMap / minifiedTree | `repo-mapper.ts` | 95 | Generate absolute repo tree for prompt injection | +| runVerifyGate / verifyAndClean | `verify-gate.ts` | 102 | Build → lint → test, short-circuit on build fail, wipe worktree on fail | +| humanInterruptGate / requireApproval | `interrupt-gate.ts` | 53 | Stdin TTY approval gate (allow/deny/skip/quit) | +| FlatLedger | `flat-ledger.ts` | 77 | JSONL state file outside LLM context | + +**Total: 497 lines of real code, 14 tests, all passing.** + +## What we're building + +**One file: `src/fable5/execution-pipeline.ts` (~80 lines).** + +A single async function that chains the 5 components in order: + +``` +1. create worktree +2. inject repo map into prompt +3. (caller runs agent task in worktree) +4. verify-gate: build → lint → test +5. on fail → wipe worktree, record to ledger, done +6. on pass → interrupt-gate: human approval +7. on deny → wipe worktree, record to ledger, done +8. on allow → record to ledger, return success +``` + +## Interface + +```typescript +export interface PipelineSpec { + taskName: string; // becomes worktree branch + ledger session + agentPrompt: string; // prompt with repo map injected by pipeline + repoRoot: string; // root for buildRepoMap + worktree base + buildCmd: string[]; // e.g. ["npm", "run", "-s", "build"] + testCmd: string[]; // e.g. ["npm", "run", "-s", "test"] + lintCmd?: string[]; // optional + runAgent: (cwd: string, prompt: string) => Promise; // caller provides +} + +export interface PipelineResult { + worktreePath: string | null; + repoMapInjected: boolean; + verify: VerifyGateResult; + approved: boolean; + worktreeCleaned: boolean; + ledgerEntry: LedgerEntry; + durationMs: number; +} + +export async function runPipeline(spec: PipelineSpec): Promise; +``` + +## Design decisions (Carmack-style) + +1. **One function, one file.** No class. No factory. No config object with 20 optional fields. One async function with a clear spec. + +2. **`runAgent` is a callback.** The pipeline doesn't know about Pi, subagents, or Claude. Caller provides `async (cwd, prompt) => string`. This keeps the pipeline testable without a live model. + +3. **Ledger records every outcome.** Pass, fail, denied — all go to the JSONL ledger. No hidden state. + +4. **Worktree cleaned on fail OR deny.** Only survives if verify passes AND human approves. + +5. **Repo map injected into prompt automatically.** Caller passes the raw prompt; pipeline prepends the minified tree. + +6. **No deploy.** Pipeline stops at "approved worktree exists." Merging to main is a separate explicit action. + +## What we're NOT building + +- No deploy integration (pipeline output is a verified worktree, not a deploy) +- No model routing (caller provides `runAgent`) +- No retry loop (one shot, one verification) +- No parallel agents (single task, single worktree) +- No new dependencies +- No config files +- No abstractions for "future" pipelines + +## Test plan + +One test file: `execution-pipeline.test.ts` (~60 lines). + +- Mock `runAgent` to return success/fail strings +- Mock verify-gate by passing build/test commands that succeed/fail +- Assert ledger records correct outcome +- Assert worktree cleaned on fail +- Assert interrupt-gate default-allow on non-TTY + +## Acceptance criteria + +- [ ] `execution-pipeline.ts` exports `runPipeline`, `PipelineSpec`, `PipelineResult` +- [ ] All 291 existing tests still pass +- [ ] New pipeline tests pass +- [ ] `npx tsc --noEmit` clean +- [ ] Total new code: ~80 lines pipeline + ~60 lines tests = ~140 lines +- [ ] No new dependencies in package.json