# 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