pi-finisher1.1.0
Goal-confirm autonomous runner for pi — state the goal once, and it finishes: parallel sub-agents, fresh context per loop, and one evidence-backed report.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-finisher
Goal-confirm autonomous runner for pi. You state the goal and confirm it once; the harness then spends every resource you have — many models at once, no token or cost ceiling — until the result is proven, and reports once at the end.
The one loop
goal (confirmed once)
└─ goal confirm the goal + its cases, once
└─ lap declare the plan, then close one lap at a time: LAP 1/n → LAP n/n
└─ code 2+ CODER sub-agents at once, one git worktree each
└─ verify ONE shot, refused while a lap is still open; main verifies itself
└─ a failing verify appends fix laps, never a fresh plan: the work and time already spent stay
└─► ONE report: every case + its evidence, or an honest "not proven"
Nothing verifies itself from prose: the harness records every shell command you run and the
status it really got, and a case is proven only by the command you named for it. Verification is
deterministic first — the repo's own typecheck, lint and test scripts run before any case is
judged, scoped to the paths they own, in parallel, and cached by change set; a real failure fails
every case immediately, while a check that cannot run here or was cut by its deadline is reported as
SKIP and never fakes a failure. Each case's outcome lands in .finisher/checks, one line per case.
Every extra lap is journaled to ~/.pi/finisher/laps.jsonl; /finisher gaps ranks what was
missing most often, so the plan can be made better instead of guessed.
- No gates. No spec signing, no ADR approval, no review packet, no gate switch — nothing to click between the goal and the result.
- A quiet HUD. No cost counters, no warning tally, no brand name and no "total time" label: the
top border is the lights, the lap position (
LAP 2/5) and the clock of the whole goal; the idle line is the lights and the phase (and, for a finished goal, the total, the lap count and the pit split). Hygiene and trajectory findings still block or flag the work itself — they just never cost you a column. - The plan is one click away. Click the goal row in the HUD and every lap opens in full — no lap text is ever truncated there — with the lap position and the total goal clock on its header.
- No budget ceiling. The run spends what the goal needs; it stops on evidence, not on cost.
- Nobody answers, nothing starts. Every goal dialog has a deadline
(
confirmTimeoutMsin.finisher/config.json,FINISHER_CONFIRM_TIMEOUT_MS, default 120s,0= wait forever). A timeout is a refusal: the run stops, complains once, and refuses any furthergoaluntil the human writes again — it never starts on a guess and never re-opens the dialog by itself. - The goal is the human's. A goal and its case list can only change through the confirm dialog: the same goal with the same cases resumes silently, a new case list re-opens the dialog, and a declined or unavailable dialog leaves the confirmed goal byte-identical. A verify run can never name or re-case a goal, and an open plan is never replaced behind your back.
- A vague goal is asked once, not guessed. When the human's words cannot yield a checkable case,
goalasks at most 3 questions — each with options to pick (drawn from the repo's own module names) and an "as I said is enough" escape — and folds the answers into the cases before the same confirm dialog opens. One round per goal, never a question that reading the code would answer. - Evidence or it did not happen. A case with no executed command is
UNVERIFIED, neverPASS. The loop is bounded: identical evidence stops it, rounds are counted. - Five models in parallel.
priority1..priority5in.finisher/models.jsonare the slots; the harness uses as many as you set, all at the same time.
Install
One command, no build step and no config file needed afterwards:
pi install npm:pi-finisher # latest release
pi install git:github.com/zurge-co/pi-finisher@v1.0.3 # pinned tag
pi install ./pi-finisher # a local checkout, loaded in place
The package declares what it ships, so one install brings all of it: the extension
(extensions/finisher), the two skills (skills/goal-intake, skills/verify-checks) and the
finisher theme (themes/). Nothing has to be enabled — the tools, the HUD, the two commands and the
confirm dialog are live the moment pi starts. Confirm it worked with pi list (the package is listed)
and /finisher models (the ladder, without editing anything). Remove it with pi remove npm:pi-finisher;
reconcile an update with pi update --extensions. Want to try it for a single run instead?
pi -e npm:pi-finisher.
Optional: /settings → theme → finisher.
Quick start
1. Tell the agent the goal → a confirm dialog: the goal + every case, y/enter = yes
2. Let it run → the widget shows every live agent side by side
3. Read the report → per case: PASS / FAIL / UNVERIFIED + the commands run
The dialog is the one gate and it always appears before a new goal starts: nothing runs on a goal
that was not confirmed. It shows the goal and every case in it — nothing is hidden behind a cap,
so the human is never asked to approve a list they cannot read; a list too long to weigh (more than
24 cases) is refused before anyone is shown anything. Declining (n / Esc) starts nothing and tells
the agent why; a session with no dialog at all (headless/print mode) is reported as unconfirmable
instead of self-confirming. A goal is asked once while it is open — the fix→verify rounds never
re-ask — and asked again once it has closed, because a finished goal's yes cannot start new work.
The dialog speaks the human's language, not the agent's. The script the human actually typed
(Thai, Japanese, Chinese, Korean, Arabic, Hebrew, Cyrillic, Devanagari, Greek) is detected on every
message and remembered; Latin text never overrides it, and a lang tag the agent passes only refines
it within the same script (pt-BR vs es). Every turn the system prompt names that language, so the
goal, the cases and the report are written in it — no translation step, no second copy of the goal to
drift from the evidence. Every chrome string — the confirm dialog's (chrome: title, header, note, key
hints) and the clarify round's (clarifyChrome: title, escape, later, note) — comes from the agent's
own words in that language, and anything it does not supply falls back to English: never to another
language, so a Spanish reader is never handed Thai. Anything that is not one readable line (empty, over
120 chars, or carrying a newline/control character) is dropped instead of shown. A goal that carries no letters of the
human's script is refused before the dialog opens, with a note telling the agent to write it again,
and long lines are wrapped between graphemes so a Thai or Devanagari mark is never orphaned.
What you can call
| Tool | What it does |
|---|---|
goal |
Asks the human to confirm the goal and its cases once, then the driver owns the run: rounds, evidence, fix-or-stop, and one report at the end. lang + chrome + clarifyChrome make both the dialog and the clarify round the human's language. |
steer |
Push a finding into a sub-agent that is still running — steer or follow-up, no restart. |
code |
coders only: one git worktree + branch per slice, merged back in dependency order, overlapping declared files never merged silently. Every slice names the files it owns and the depends_on slices it waits for. |
verify |
main verifies itself: it runs the repo's typecheck/lint/test scripts through a scoped, parallel, cached plan, then judges each case from the commands the harness recorded, and writes one PASS/FAIL line per case to .finisher/checks. No sub-agent is spawned to grade your work. |
Explore and planning are yours alone: read the files yourself and decide the slices, the files each
one owns and the interface they share (shared_contract). A plan that overlaps files, names an
unknown dependency or contains a cycle is refused before any worktree is created.
Watching it run
One framed HUD, drawn below the editor and measured against the real terminal width (pi hands the width to the render, so the box never overflows). While a goal is open or an agent is working it is a box: one line for the goal, one row per live agent (tag, model, slot, elapsed, live tail). When nothing is running it collapses to a single idle line, so a quiet session costs one row, not a panel.
╭─ ▚ ●●○○○ LAP 4/5 4:12 ─ 2 live · click a row · ctrl+_ pages ─╮
│ ▸ make the scheduler prove its bound, not assert it │
│ CODER ▶ coder slot1 provider/a 42s fixing the scheduler bound │
│ CODER ▶ coder slot2 provider/b 41s reading src/swarm.ts │
╰──────────────────────────────────────────────────────────────────────────────╯
The top border is the whole race: the mark, the lane lights, the lap position (LAP 2/5) and the
clock of the entire goal — no brand name on it, and no "total time" wording anywhere. Below it is
the goal row, then one row per live agent.
Idle:
▚ ○○○○○ READY
A finished goal closes back to one line, keeping its total, the lap count and the pit split:
▚ ●●●●● P1 12:36 · 5 laps · pit 0:41
The goal page
Click the goal row in the HUD and the whole plan opens — every lap in full, never truncated, with
the lap position and the total goal clock on the header. Lap marks are ✓ done, ▶ current, · todo;
↑↓ scrolls, PgUp/PgDn pages, g/G jumps to the ends, Esc closes.
▚ LAP 4/5 4:12
3/5 laps closed · laps 3:11 · pit 1:01
make the scheduler prove its bound, not assert it
✓ 1/5 read the ladder and the scheduler (0:32)
✓ 2/5 rewrite the retry policy so the bound holds (2:00)
✓ 3/5 prove it with a command that can fail (0:39)
▶ 4/5 wire the report
· 5/5 close the goal
Esc close · ↑↓ scroll · PgUp/PgDn page · g/G top/bottom
A lap that has closed carries the time it spent in parentheses; the header keeps the position
(LAP 4/5) and the clock of the whole goal, the second line splits the run into laps, verify and pit.
A plan longer than the terminal pages instead of clipping: no lap text is ever cut in half.
The report page
The moment a goal closes, verify writes the report it just gave you to
.finisher/report.md — the result first, then the goal, the lap count, the round, when it was written,
and every case with the command that proved it. The HUD always advertises it: a 📄 report row while a
closed goal is still boxed, and a tail on the idle line otherwise.
▚ ○○○○○ READY · 📄 report
╭─ ▚ ●●●●● P1 1:01 · 1 laps ─ 1 live · click a row ─╮
│ ▸ ship the report row │
│ 📄 report · verified · 2 laps · click to read │
╰───────────────────────────────────────────────────╯
Click it and the whole file opens as a page — every line, wrapped rather than cut, ↑↓ scrolls,
PgUp/PgDn pages, g/G jumps to the ends, Esc closes. The page re-reads the file on every frame and
falls back to what is on disk, so a report written by an earlier session still offers itself with
nothing in memory; a file that has since been deleted says so instead of opening an empty page. While a
goal is still open there is no report yet, so the row is hidden rather than lying about one.
One page per sub-agent
Click a row in the HUD (or press ctrl+_) and you are inside a sub-agent's own session — not a
status row: the task it was given (input), everything steered into it while it ran, and its
live output (model text, every tool call with its full command, and each tool's own output).
t folds or unfolds the tool output — the command itself is always on screen, so a folded page still
tells you exactly what the agent ran. ←/→ moves between sub-agents (live ones first, then finished
ones), 1-9 jumps straight to one, and the tab strip plus the header always say which one you are
looking at:
▚ sub-agent session — 2/3 · VERIFIER · ▶ 12s · provider/c · c2 · 1 steer
▚ pages 1:CODER s1 provider/a ▶ · [2:VERIFIER c2 provider/c ▶] · 3:CODER s3 ✓
.finisher/subagents/…-verifier.log • live — closing this page never interrupts the run
▸ task — what this agent was told
prove case c1: the scheduler bound holds for every input
▸ steered (1) — what was pushed in while it ran
→ steer: the bound is 16, not 5
▸ session — live output · 3 tool call(s) · t = show tool output
────────────────────────
reading src/swarm.ts
✓ bash — npm test -- swarm-scheduler 412ms
▸ 6 line(s) of output hidden — press t to show
✓ read — extensions/finisher/src/swarm.ts 22ms
▸ 1 line(s) of output hidden — press t to show
Press t and those blocks unfold in place:
✓ bash — npm test -- swarm-scheduler 412ms
ℹ tests 8
ℹ pass 8
ℹ fail 0
✗ bash — npm run typecheck 903ms
src/tools.ts(15,24): error TS2305: has no exported member
A tool call that failed is marked ✗, a finished one ✓, a live one ⚙ — and a large output keeps its
newest lines with … N earlier line(s) hidden, so one noisy command can never push the page off screen.
↑↓ scroll, PgUp/PgDn page, g/G jump, t folds tool output, space peeks at the chat, Esc closes — the agent keeps
running. Outside the TUI the same page degrades to a notification listing every agent with its task,
steer count and log path.
Commands
| Command | What it does |
|---|---|
/finisher models |
set the model priority ladder (priority1 … priority5) — there are no per-role overrides: every sub-agent auto-picks from the ladder |
/finisher gaps |
the extra-lap journal: every fix lap a failed verify appended, ranked so the most-often-missed kind of lap comes first — the plan gets better instead of guessed |
/finisher models opens the ladder picker in the TUI and prints the same ladder, the main model and any
ignored per-role key outside it; /finisher gaps prints the digest of ~/.pi/finisher/laps.jsonl (goal,
round, lap n/n, the added fix laps and each case result with its reason — one JSON line per replan).
Both are read-only until you pick a slot, and an unset slot is simply not used.
| Shortcut | What it does |
|---|---|
ctrl+_ |
open a sub-agent's live session page — ←/→ switches between the running agents |
There is nothing else to switch: fresh context, handoff and verification are automatic.
Nothing to configure
Install it and the fast path is already on — no key, no flag, no dialog:
- The goal comes before the work. The human's message is drafted into a goal plus its cases, and
goalis asked for before the run starts. Nothing is gated and nothing is counted: reading the code to understand the request is the agent's own call, and the harness never reads the human's words behind its back. - Verify runs checks, not the whole suite every time. The repo's own
typecheck/lint/testscripts are scoped to what changed, run in parallel, and cached by fingerprint — a repeat round with the same change set reads the recorded result instead of paying for it again. A missing toolchain (infra) or a check cut off by its deadline (deadline) is reported asSKIPand never fails a case. - The evidence is a check file, not a new test. Every case's result is recorded in
.finisher/checks/checks.jsonl—PASS/FAIL/SKIP/UNVERIFIEDper case, newest record wins — so proving a goal never means growing the repo's test suite. - The suite has a growth ceiling. A baseline is taken when the goal starts: one more test file and a few more cases at most. Past that, a new test file is refused and the evidence goes to the check file instead.
- Two skills ship with it.
goal-intakecovers the confirm-before-exploring rule,verify-checkscovers scoped checks and the check file. They load themselves; there is nothing to enable. - The human's words are
checkandresult. What runs is a check, what it came to is its result.
Configuration
.finisher/models.json — the priority ladder. Rung 1 is what a sub-agent runs; later rungs are
failover, and a gap is skipped:
{ "priority1": "anthropic/claude-sonnet", "priority2": "openai/gpt-5", "priority3": "google/gemini-pro" }
An explicit slot model (a coder slot) outranks the ladder; with no ladder at all a sub-agent falls
back to the main session model. A leftover per-role key (swarm, coder, …) never selects a model any
more — it is reported as ignored.
.finisher/config.json
{ "swarmConcurrency": 5, "subagentTimeoutMs": { "default": 1200000 }, "freshContext": true }
Unset slots are simply not used; there is no ceiling key anywhere — if one is present it is reported as ignored, never obeyed.
Verification runs once, at the end
verify is not a loop. After the implementation loops are done, it runs once, in two
stages:
- deterministic — the repo's own
typecheck,lintandtestscripts run scoped to what changed, in parallel, with a real deadline and a real exit code (and cached, so a repeat round with the same change set costs nothing). A failing check fails every case straight away, so no model is spent re-reading what the exit code already said; a missing toolchain or a cut-off check isSKIP, never a failure of the case. - evidence — each case must name the command that exercises it. A case whose command the
harness never watched succeed is
UNVERIFIED, neverPASS. A green suite alone proves nothing. Every result is recorded in.finisher/checks/checks.jsonl, one line per case.
| outcome | what happens |
|---|---|
| every case holds | one report, done |
| a case failed | the main agent gets the next loop back: fix it, use code to get coders working in parallel, then verify again (bounded, then it halts) |
| no command can prove a case | it says so and hands that case to you instead of guessing |
A verify run never creates a goal. If no confirmed goal is open, it reports its evidence and says so — it cannot mark a goal confirmed, so verifying first is never a way around the dialog.
Fresh context: a lap boundary, never a goal close
A context reset happens at exactly one moment: while a goal is still open, when a lap closes
(turn_end → at most one compaction entry per lap). Closing the goal is not a boundary — the
transcript, the verify report and the evidence that already ran stay on screen and in context to
read back, nothing fires at settle time, and the next task starts from the context you already have.
Asking a question, getting an answer, or any other reply is never a boundary either.
What a lap compaction carries is not a transcript or a statistic — it is the lap tracker: the
goal, the confirmed cases, every lap with its mark and its own spent time, and the latest status of
each recorded command. It is appended as one JSON line per turn to
.finisher/<sessionId>/tracker.jsonl while the goal is open (the newest line wins, and the file is
pruned to TRACKER_RECORDS_MAX lines and the newest 8 sessions),
removed the moment the goal closes, and appended as a hidden tracker message so the fresh context
still knows where the goal stands. Two sessions on the same repo never share a tracker: each reads
its own file per turn, so a session whose context was cleared resumes the lap from disk instead of
guessing it, and the newest 8 trackers are the ones kept.
The reset is bounded by design: once per lap, never twice for the same lap, never for a plan that has
just closed its last lap (it hands straight over to verify), and never while a halted/needs-human
report is still unread. {"freshContext": false} in .finisher/config.json turns the lap reset off;
with it off the context simply keeps growing, and goal close stays untouched either way.
The handoff stack is parked (HANDOFF_ENABLED = false): nothing is classified into it, no handoff
model is called, and it injects nothing. Sub-agents still run under RpcClient, so a finding can be
pushed into a run that is already working with steer() instead of restarting it, and every role
boots with --exclude-tools goal,lap,code,verify,steer, so a sub-agent can never re-enter the harness.
Coders prove their own slice
Each code slice gets its own git worktree with your dependencies linked in
(node_modules is symlinked from the repo and excluded from the slice's diff), so a coder runs your
test command immediately instead of installing anything. A coder must end with two lines:
TESTS: PASS|FAIL|SKIP <reason>
FILES: <paths it changed>
A FAIL comes straight back to that coder to fix before its slice is accepted.
No one asks you mid-run
There is no picker and no question tool: a worker that hits a fork declares it in one line instead.
DECISION NEEDED: should the cache be per-tenant?
assumption: yes, keyed by tenant id
The worker continues with that assumption, the harness collects every declaration, and the single
report at the end lists them (decisions made without asking you) plus .finisher/decisions.md.
Choosing the handoff model
The handoff gate needs a model; you pick it in the same ladder file as a slot of its own:
{ "priority1": "anthropic/claude-sonnet", "handoff": "openai/gpt-5-mini" }
- unset (default) → the harness detects whichever classifier your providers expose and uses it; if you have none, handoff simply stays off — nothing to configure, no nagging
- a classifier model (for example
typesafe/jev-latestorcf/clef— pi lists dozens from many providers) → usesclassify() - any chat model → asked
KEEP/DROPwith one short call
The handoff slot is never counted as a fan-out rung.
Where the work happens
The agent writes in your repo. There is no shadow checkout to get lost in: what finisher edits is
what git status shows, so a review always looks at the same tree the agent worked in. The only
worktrees left are the per-slice ones that code gives each parallel coder, so
two coders can never overwrite each other; they are merged back deterministically by slice id.
Undo: There is no accept/discard command: when you want a change undone, ask the agent to undo it (or use
git). Nothing in the harness records a rollback point for you.
Every tool call is bounded
A bash call that carries no timeout runs forever in pi — the tool simply waits. The harness
closes that hole for the main agent and for every sub-agent: a tool_call hook injects a deadline
whenever one is missing or invalid.
| call | bound |
|---|---|
bash |
injected if absent (default 900 s, override with toolTimeoutMs in .finisher/config.json) |
goal / code / steer |
wrapped, reports ⏱ … exceeded its Ns limit instead of hanging |
every RPC call to a sub-agent (start, promptAndWait, steer, getMessages, …) |
its own deadline |
every git subprocess |
120 s, SIGKILL on expiry |
{ "toolTimeoutMs": { "bash": 900000, "code": 1800000 } }
A broken config falls back to the default rather than failing a tool call.
Rules the harness enforces on the result
- No comments in code (line or block); a load-bearing reason must carry
finisher-hygiene-allow: <why>. Violations come straight back to the same turn. - A file stays at or under 500 lines — split it instead of growing past that.
- Tests are proof, not coverage — a test that would pass with the logic broken is worse than none.
Files
pi-finisher/
├── package.json # pi manifest
├── extensions/finisher/
│ ├── index.ts # pi wiring: hooks, tools, commands, TUI
│ └── src/ # pure helpers (goal driver, swarm, check runner, check file, panel, …)
├── skills/ # goal-intake + verify-checks, loaded with the package
├── test/ # node:test suites
└── themes/ # finisher dark theme (violet / cyan / mint on graphite)
A run's own state lives in .finisher/: checks/checks.jsonl (the evidence), report.md (the
report), sessions/ (one lap tracker per session) and models.json (the ladder).
Theme (optional): /settings → theme → finisher.