4.0 KiB
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
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)
-
One function, one file. No class. No factory. No config object with 20 optional fields. One async function with a clear spec.
-
runAgentis a callback. The pipeline doesn't know about Pi, subagents, or Claude. Caller providesasync (cwd, prompt) => string. This keeps the pipeline testable without a live model. -
Ledger records every outcome. Pass, fail, denied — all go to the JSONL ledger. No hidden state.
-
Worktree cleaned on fail OR deny. Only survives if verify passes AND human approves.
-
Repo map injected into prompt automatically. Caller passes the raw prompt; pipeline prepends the minified tree.
-
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
runAgentto 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.tsexportsrunPipeline,PipelineSpec,PipelineResult- All 291 existing tests still pass
- New pipeline tests pass
npx tsc --noEmitclean- Total new code: ~80 lines pipeline + ~60 lines tests = ~140 lines
- No new dependencies in package.json