fable-agent/docs/specs/execution-pipeline.md

108 lines
4.0 KiB
Markdown

# 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<string>; // 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<PipelineResult>;
```
## 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