omp packages

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.

zurge-co Published 7 days ago MIT ~836 downloads/mo

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.

Did it work for you?

Result

One report per person. A new report replaces your old one.

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 (confirmTimeoutMs in .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 further goal until 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, goal asks 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, never PASS. The loop is bounded: identical evidence stops it, rounds are counted.
  • Five models in parallel. priority1..priority5 in .finisher/models.json are 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 goal is 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/test scripts 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 as SKIP and 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 / UNVERIFIED per 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-intake covers the confirm-before-exploring rule, verify-checks covers scoped checks and the check file. They load themselves; there is nothing to enable.
  • The human's words are check and result. 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:

  1. deterministic — the repo's own typecheck, lint and test scripts 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 is SKIP, never a failure of the case.
  2. evidence — each case must name the command that exercises it. A case whose command the harness never watched succeed is UNVERIFIED, never PASS. 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-latest or cf/clef — pi lists dozens from many providers) → uses classify()
  • any chat model → asked KEEP/DROP with 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.