infinity-harness
A pi agent extension that runs a gated build pipeline unattended — enforces phases, validates with deterministic gates, and keeps working for hours or days without losing the plan.
Package details
Install infinity-harness from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:infinity-harness- Package
infinity-harness- Version
2.4.0- Published
- Aug 24, 2026
- Downloads
- 935/mo · 935/wk
- Author
- bakr-bagaber
- License
- MIT
- Types
- extension, skill
- Size
- 772.8 KB
- Dependencies
- 2 dependencies · 0 peers
Pi manifest JSON
{
"skills": [
"./harness/skills"
],
"extensions": [
"./extensions"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
infinity-harness
A pi extension that keeps an agent building — correctly — for hours or days without you.
Point it at a project, describe the goal, and walk away. infinity-harness drives the agent through a gated pipeline, checks the work with deterministic gates rather than the model's own judgement, and stops with a clear reason when it genuinely needs you.
╭──────────────────────────────────────────────────────────────────────────╮
│ ∞ INFINITY ──────────────────────────────────────────────── BUILD rev 42 │
│ ◈ Ship the payments rewrite behind a flag │
│ │
│ ● define ─ ● plan ─ ◉ BUILD ─ ○ verify ─ ○ review ─ ○ ship │
│ │
│ ▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱ 45% 5/11 tasks · 1/2 features │
│ ⚠ 1 blocked · ↷ 1 rework · retry 2/10 · session 7 │
│ │
│ ▤ Checkout 5/11 │
│ ▸ feature-002 · Checkout flow 2/5 │
│ ● 4 validate cart totals against catalogue prices │
│ ● 5 apply stacked discount codes with precedence rules ← #4 │
│ ◐ 6 handle partial refunds across split tenders ← #4, #5 │
│ ✓ unit tests for tender split │
│ ▸ integration test against sandbox │
│ · audit log entries │
│ ○ 7 emit refund webhook ← #6 │
│ ⚠ 8 reconcile ledger nightly ← #6 │
│ ⋯ 3 below alt+j/k scroll · alt+o expand │
╰──────────────────────────────────────────────────────────────────────────╯
All five levels of the plan — goal, sprint, feature, task, subtask — with the window
centred on the work. alt+j / alt+k scroll it, alt+o expands it, and
/infinity:dashboard shows the whole thing in a browser.
Why it exists
Left alone, a coding agent drifts. It declares work finished that isn't, forgets what it already did, re-solves the same problem three different ways, and — worst of all — keeps going long after it stopped making progress. Pair it with a small or cheap model and all of that gets worse.
infinity-harness fixes that by taking three decisions away from the model:
- When work is done. A deterministic gate decides, not the agent. Same tree, same verdict, every time. No agent marks its own homework.
- What happens next. The phase pipeline is forward-only and one step at a time. The agent cannot decide it's bored of BUILD and jump to SHIP.
- What to remember. The plan, the phase and every budget live in files, so the run starts a fresh pi session at each boundary instead of dragging the whole history of the run into every request. Small models stop drowning; long runs stop turning into summaries of summaries.
Everything else — the retries, the model routing, the escalation ladder — exists to keep those decisions honest over a very long run.
And one decision stays yours: what gets built. The gate can prove a feature has acceptance criteria; it cannot prove they are the right ones. So the run stops and asks you to sign off RESEARCH, DEFINE and PLAN — always in copilot, and in autopilot for whichever of them you choose to keep.
Install
Requires Node 22+ and pi.
# from npm — project-local, so the team shares it via .pi/settings.json
pi install npm:infinity-harness -l
# or globally
pi install npm:infinity-harness
# straight from the repo
pi install git:github.com/bakr-bagaber/infinity-harness
# or from a checkout
pi install ./infinity-harness -l
pi list # confirm it loaded
The package ships TypeScript. pi loads extensions through jiti, which
transpiles at runtime, so there is no build step and nothing to compile.
Use it
cd your-project
pi
Then, once, in that project:
/infinity:init
It detects your stack and its lint/test/build commands, then asks you five questions:
| Which workflow? | A built-in, one you saved, or build one — pick the phases, then say for each whether it stops for you |
| What are you building? | One or two sentences. Asked whatever the workflow, because a run with no goal has no business inventing one. |
| When should it start a fresh session? | Every phase (default) · every task · never |
| How much of the plan on screen? | A display template — focus, everything, overview, worklist, or level by level |
Then it writes harness/ — the config, an empty plan, the phase and role docs, and starters
for the documents the review gate will demand — and hands the model its first brief. It never
overwrites a file that already exists, and /infinity:init force restores anything you
deleted without touching what you wrote.
From then on, every session opens with a brief: phase, role, current task, acceptance criteria, the craft skills that match the work, and what to do next. Do the work, then:
/infinity:validate run the gate for this phase
/infinity:run hand it the wheel: validate → advance → re-brief, until done or stuck
/infinity:approve sign off the phase waiting for you — or send it back with a note
/infinity:status where the run is right now
/infinity:scroll move the plan widget: up · down · top · bottom · expand · follow
/infinity:handoff continue this run in a fresh session, by hand
/infinity:config change any setting, including which model runs which tier
/infinity:models what models pi has, and how they are being routed
/infinity:dashboard open the live web view
/infinity:goal state a goal and pursue it across passes
/infinity:unstuck what the escalation ladder would try next
/infinity:rework send a task and its dependents backwards
/infinity:halt take the wheel back
/infinity:run is the point of the tool. It keeps the loop turning without you.
Who decides what
"copilot" and "autopilot" used to be one switch, and one switch is the wrong shape for the question. What people actually want is per-phase: let the model define and plan on its own but show me the review; or grill me on the definition and then leave me alone until it ships. Two words cannot say that.
So the setting is a mode per phase, and the two familiar words are two named points in that space rather than the only two points in it.
| Mode | When a phase's gate passes |
|---|---|
autopilot |
it advances |
copilot |
it stops and waits for your signature |
Five workflows ship with the package, and none of them can be edited — copilot has to mean the
same thing in every conversation about this tool:
| Workflow | Stops at |
|---|---|
| copilot | DEFINE, PLAN |
| autopilot | nothing |
| spec and ship | DEFINE, SHIP — you sign the scope going in and the release coming out |
| research first | adds a RESEARCH phase, and stops on all three thinking phases |
| every gate | every phase. Slowest, and the one you want on something that matters |
Anything else you build yourself: pick the phases, then say for each whether it stops for you. Give it a name and it is saved with you, not with the project, so it is the first thing offered on your next one.
/infinity:workflow choose one, or build one
/infinity:workflow spec-and-ship switch to a named one, no menu
/infinity:workflow list what is available, and what you are on now
Any of it changes at any time and takes effect at the next gate — three phases into a run is
exactly when someone realises they do want to see the review after all. /infinity:config →
Workflow edits one phase at a time.
Signing a phase
When a phase you signed up for passes its gate, the run stops and asks you rather than advancing. Approving continues it; answering with a sentence sends the phase back carrying your words, so it is redone against your objection rather than redone identically:
/infinity:approve
/infinity:approve the criteria say nothing about refunds
A rejection is pinned to the state of the project when you made it, so the run will not ask you the same question again until the agent has actually changed something in response. If it never does, the run stops and says so instead of nagging forever.
One run, many sessions
A harness that never starts a new pi session is a harness whose context window only ever grows. By the tenth task the model is re-reading the history of the first nine in order to do the tenth — paying for those tokens on every call, compacting them into a lossy summary once the window fills, and, on a small model, simply drowning.
Nothing the harness knows lives in the conversation. The plan, the phase, the gate history, the
retry budgets and the escalation ladder are all files under harness/, so a session boundary
costs one thing: the brief — which is what the agent should have been working from anyway.
So the run hands itself to a fresh session at each boundary, and the replacement picks up exactly where the last one stopped:
| Setting | Fresh session when |
|---|---|
phase (default) |
the pipeline advances a phase, or a goal pass finishes |
task |
that, plus every time the run moves to a different task |
off |
never — one session for the whole run |
Any of them also hands off early once the context passes session.contextThreshold (0.7 by
default), because a handoff that arrives after compaction has arrived too late to be the thing
that prevented it.
The run itself — its id, its wall-clock budget, its iteration ceiling, its no-progress strikes
and its position on the escalation ladder — lives in harness/run.json and is the same run
across every session it spans. /reload, /resume, closing the terminal and reopening it all
resume the same run rather than quietly starting a new one with fresh budgets.
The widget shows session 7 once a run has spanned more than one.
The first pass through
DEFINE wants acceptance criteria on every feature, so start by telling it what you are building.
The agent writes that through infinity_plan:
{
"goal": "Ship the payments rewrite behind a flag",
"features": [
{ "id": "feature-001", "name": "Checkout flow", "criteria": ["refunds reconcile against the ledger"] }
]
}
Features carry names and criteria; tasks are a separate list keyed feature-001/task-001, and
arrive in PLAN. Omitting a task deletes it — that is the rule that keeps the plan honest — but
omitting a feature just leaves it alone, because features are inferred from task keys rather
than submitted.
Configuration
Everything is configurable from inside pi:
/infinity:config interactive menu
/infinity:config show print the whole configuration as text
The menu is generated from a single schema, so every option the file format
supports is reachable from the UI — the two cannot drift. Editing
harness/config.json and harness/model-router.json by hand stays entirely
valid; the menu is the same data with prompts and bounds checking attached.
| Group | Covers |
|---|---|
| Models | Which model runs each difficulty tier, the master model, consultation budget |
| Pipeline | Which phases run, copilot vs autopilot, role strictness, pause |
| Workflow | The mode for each phase — which of them stop and wait for your signature |
| Display | Which plan levels and which chrome the widget and the dashboard draw |
| Sessions | Fresh session per phase or per task, the context threshold, the carry note |
| Project commands | lint / test / coverage / build — what the gate actually runs |
| Gates | Enable, coverage threshold, placeholder rejection |
| Continuous run | Iteration ceiling, wall-clock budget, no-progress strikes |
| Retry budgets | Attempts per task, feature and phase |
The pipeline
[research] → define → plan → build → verify → [simplify] → review → ship
| Phase | What it's for | Gate opens when |
|---|---|---|
| research | Find out what it actually has to be (opt-in) | harness/docs/RESEARCH.md says something a human could argue with |
| define | Write down what's being built and how you'll know it's done | Every feature has acceptance criteria |
| plan | Break features into ordered, dependency-aware tasks | Tasks exist and criteria are set |
| build | Implement, one task at a time, tests alongside | Lint, tests, coverage pass; no placeholders; every task complete |
| verify | Prove behaviour; hunt what the tests miss | Tests and coverage pass on a clean tree |
| simplify | Delete more than you add (opt-in) | Tests pass, no empty dirs, clean tree |
| review | Judge it as if someone else wrote it | Rubric, README, architecture doc and decisions are real; branch level with upstream |
| ship | Tag, changelog, leave it clean | Clean tree, tagged, changelog, README, licence, no placeholders |
Enable or disable phases in harness/config.json under phases.enabled, or in the wizard.
RESEARCH and SIMPLIFY are off by default.
RESEARCH runs before anything is specified, and answers the question DEFINE assumes: is this the right thing to build at all? It writes prior art, the constraints that are real, at least two options with what each costs, a recommendation, what would make that recommendation wrong, and the questions only you can answer — which become the DEFINE interview. Turn it on when the human gave an idea rather than a specification.
When it gets stuck
Stopping safely is the easy half. The hard half is trying something else first, and that is the
escalation ladder: when a run stalls — the gate fails and the working tree has not moved, meaning
the agent produced nothing — /infinity:run climbs it before spending a strike.
| Rung | What it does |
|---|---|
| retry | One more attempt. Sometimes a run is just slow. |
| reframe | State the assumption you have been working under, say why the evidence contradicts it, then try a different approach. |
| consult | Escalate to a stronger model, one step up the difficulty ladder. |
| rework | Flip the task and everything that depends on it back to rework. Work built on a broken thing is suspect until re-proved. |
| replan | The plan is wrong: something this needed was never planned. Amend it. |
| master | Last resort. State the problem from scratch, including what has been ruled out. |
Each rung gets one turn per stall, and each is bounded — reworks and replans have budgets, consult
has a per-task limit, master fires once per run. When the ladder runs out, the run stops and names
every rung it spent. Real progress resets it: a moving tree means a new problem, and a new problem
gets a fresh ladder.
/infinity:unstuck shows what it would try next without doing it.
Goals, and knowing when you are actually done
A finished pipeline is not a met goal. The gate decides whether the work is done; it has no opinion on whether the work was the right work, because it only ever sees the plan — and the plan is just what you thought the goal needed when you wrote it.
/infinity:goal Ship the payments rewrite behind a flag
That states the goal and starts pass 1. One pass at the goal is one full trip through the pipeline. When the pipeline completes, the run does not end: it asks whether the goal is met.
- complete ends the run.
- Anything else must name what is still missing — and the pipeline rewinds to the first phase with that list carried into the brief, so the next pass plans for the remainder rather than rebuilding what the last review already accepted.
Bounded by an iteration ceiling and a wall clock, both configurable. The widget shows which pass you are on, because a second pass looks exactly like a first one otherwise.
Knowing when to stop
This is the part that makes an unattended run safe. /infinity:run halts on any of:
| Guard | Default | What it catches |
|---|---|---|
| No progress | 3 strikes | Gate keeps failing and the working tree hasn't moved — the agent is spinning, not working |
| Wall clock | 24h | A run you forgot about |
| Iterations | 2000 | Runaway loops that stay under the clock |
| Retry budget | 10/task | One impossible task eating the whole run |
| Pipeline complete | — | Final phase passed with every task done |
| Human brake | — | /infinity:halt, /infinity:pause, or touch harness/STOP |
Every stop names its reason. You come back to an explanation, not a mystery.
Tune the budgets in harness/config.json:
{
"loop": {
"maxIterations": 2000,
"maxWallClockMs": 86400000,
"noProgressLimit": 3
}
}
The plan is a file
harness/features/feature-list.json is the single source of truth. The widget, the dashboard and the
brief all read it; nothing caches a second copy.
The agent edits it by submitting the complete task list through the infinity_plan tool:
- Omission means deletion — for tasks. One unambiguous rule beats incremental edits a model
loses track of. Leaving the
tasksfield out entirely is different from sending an empty one: absent means "not touching them", empty means "delete them all". - Features are a merge, not a submission. They are inferred from task keys, so they are never
resubmitted wholesale;
featuressupplies names and acceptance criteria by id, and omitting one leaves it alone. baseRevisionguards every write. A stale revision is rejected, so parallel workers can't clobber each other.- Unknown fields survive. An update merges onto the stored task, so
difficulty,modelHint,criteriaand anything added later are never silently dropped. - The dependency graph stays sane. Cycles and dangling references are rejected at write time; a
task can't be
completewhile something it depends on isn't.
Watching it work
In the terminal — the widget updates on every turn, showing the plan as deep as you asked for: goal, sprint, feature, task, subtask. It is a window, not a truncation — the rows above and below are counted, and one keypress away:
| Key | |
|---|---|
alt+j / alt+k |
scroll the plan down / up |
alt+o |
expand — every subtask, three times the rows |
/infinity:scroll follow |
back to tracking the active task |
It is responsive down to ~58 columns, degrades to ASCII when the locale isn't UTF-8, and drops
colour under NO_COLOR.
In a browser — /infinity:dashboard serves a live page on loopback: phase rail, stacked
progress meters that show stuck work as colour rather than absence, the plan as a collapsible tree
with counts at every level, and the last gate verdict. It refreshes itself every 5 seconds and
reconnects with backoff if the run ends.
The dashboard is strictly read-only and binds to 127.0.0.1. It never writes, and never bumps
baseRevision — opening it can't perturb the run you're watching.
What they show is yours to choose
Two people watching the same run want different things on screen. One works in sprints and never opens a subtask; the next has no sprints and lives in the subtask list. So it is a setting, and the widget and the dashboard read the same one — configure how you like to read a plan once, not twice.
| Template | |
|---|---|
| focus (default) | every level, with subtasks on the task being worked |
| everything | all five levels, every subtask on every task, a taller window |
| overview | goals, sprints and features with their counts. No tasks — the shape, not the work |
| worklist | tasks only. No grouping rows, no rail — for when you already know the plan |
/infinity:display pick a template, or choose level by level
/infinity:display overview switch straight to one
/infinity:display list what is available, and what you are on now
Choosing level by level also lets you turn off the phase rail, the progress meter, the alert strip,
the done/total counts, the ← #3 dependency labels and the acceptance criteria, and set how many
rows the terminal shows before it scrolls. Name what you end up with and it is saved with you,
offered on your next project. /infinity:config → Display edits the same things one at a time.
Hiding a level never hides what is under it: turn off sprints on a plan organised into sprints and the features move up one indent, they do not vanish. A task nobody can see is a task that gets stuck forever.
Craft skills
28 short documents on how to do the work well — how to write a test worth keeping, how to debug something intermittent, how to design a module boundary. They ship with the package, so pi loads them wherever it's installed and the model can invoke any of them by name.
A model with 28 skills available and no idea which one applies reads none of them, so the brief names the ones that match the work in hand. Each skill declares what it's for:
kind: domain # process | domain | meta
phases: [plan, build, verify] # the phase it leads counts most
tags: [concurrency, race, lock, mutex, deadlock] # vocabulary a task would use
A process skill belongs to its phase — TDD is the right answer for a BUILD
task whatever the task says. A domain skill has to share vocabulary with the
task: nobody needs the database skill because they happen to be in BUILD. So a
task called "serialise plan writes so two workers can't race on the lock"
gets concurrency-async, and a bare BUILD task gets tdd.
When nothing matches, the brief has no skills section. An empty section is honest; a padded one teaches the model to skip it.
Model routing (optional)
Send cheap tasks to a small model and hard ones to a large one. Pick them with /infinity:config →
Models: the list offered is the models pi itself has configured and can authenticate, so you
choose from what you already have rather than typing ids from memory. /infinity:models shows that
list alongside the current routing.
Routing ships disabled with every slot empty, meaning "use whatever model pi is already configured with" — installing the harness never silently redirects your work to someone else's model. Any tier can be handed back to that default at any time.
Resolution order:
task.modelHint → byTask → byDifficulty → byFeature → bySprint → byPhase → byRole → default
master is never assigned directly; it's reachable only through one-step consultation after the
normal ladder is exhausted.
Reasoning models need headroom. A reasoning model emits nothing on the content channel until it has finished thinking — measured at ~370 reasoning tokens to answer "reply with one word". If you route a tier to one, give its workers a generous timeout; a tight budget returns an empty completion that looks like a broken endpoint but is only a small cap.
Tools the agent gets
| Tool | Purpose |
|---|---|
infinity_init |
Create the harness in this project |
infinity_brief |
What am I supposed to be doing right now? |
infinity_plan |
Read or rewrite the plan — tasks, features, criteria, goal |
infinity_validate |
Run the gate for this phase |
infinity_advance |
Move to the next phase (refuses on a failing gate) |
infinity_dashboard |
Start/stop/query the web view |
infinity_unstuck |
What should I try next? (recommends; does not act) |
infinity_rework |
Send a task and its dependents back to rework |
infinity_replan |
Add what the plan was missing, mid-run |
infinity_spawn_worker |
Attempt one task in a clean-room worker |
infinity_goal |
State a goal, review it, or check which pass it is on |
Layout
infinity-harness/
├── extensions/infinity-harness/ pi lifecycle adapter — thin, no logic of its own
├── src/
│ ├── core/ types · paths · fsx · config · phases · gates · brief
│ │ · featureList (the SSOT) · lock · exec
│ │ · skills (match) · skillsAudit (guard)
│ ├── ui/ theme · planTree (the five levels, once)
│ │ · display (what to draw, and the templates)
│ │ · widget (terminal) · dashboard (web) · wizard · config
│ ├── loop.ts the continuous-run driver and its stop conditions
│ ├── runState.ts is a run armed, and which run is it — on disk, across sessions
│ ├── handoff.ts when to continue in a fresh session, and what to tell it
│ ├── workflow.ts a mode per phase, the built-ins, and the ones you save
│ ├── approval.ts · intake.ts human sign-off · what the start-up wizard's answers mean
│ ├── escalate.ts the ladder's actuator: chooses a rung and takes it
│ ├── goal.ts the outer loop: is the thing asked for actually done?
│ ├── taskList.ts atomic plan editor
│ ├── worker.ts isolated per-task workers
│ ├── modelRouter.ts difficulty ladder + consultation
│ ├── rework.ts · replan.ts backward rework with BFS impact · mid-build amendment
│ ├── unstuck.ts · review.ts escalation strategy matrix · review bounce guard
│ └── goalLoop.ts · goalState.ts · goalSpec.ts goal state machine and its store
├── harness/
│ ├── features/feature-list.json the plan
│ ├── config.json pipeline state and settings
│ ├── run.json the armed run — survives every session it spans
│ ├── model-router.json optional routing
│ ├── docs/ architecture · decisions · phase and role docs
│ └── skills/ 28 craft skills the brief points at
│
│ ~/.pi/agent/infinity-harness/ the workflows and display templates you saved —
│ they belong to you, not to a project
├── tests/ 33 files, plain node:assert
└── scripts/
├── run-tests.mjs
├── e2e.mjs 16 scenarios, including one against a real pi process
└── rig/ the real-pi driver: a scripted model + the RPC protocol
The extension is deliberately thin. Every decision lives in src/, where it's typed and tested —
there is one implementation, and the adapter calls it.
Development
npm install
npm run check # tsc --noEmit, strict
npm test # 33 test files
npm run e2e # 16 end-to-end scenarios
npm run e2e -- --only realpi # just the ones that drive a real pi process
npm run e2e -- --list # what the scenarios are
Tests are plain node:assert run under --experimental-strip-types. No framework, no build step.
The scenario worth knowing about is realpi. Everything else drives our own modules, or drives
the adapter against a fake pi — a fair test of our contracts and a poor test of pi's. Every bug
that has reached a user so far lived in the gap between the two: a BOM that made every config read
fail, a run that ended at its first session handoff, a brief queued in a delivery mode that
deadlocks pi -p.
realpi closes the gap. scripts/rig/ starts a real pi --mode rpc against a scripted model
server and speaks the RPC protocol to it — typing prompts and slash commands, answering wizard
dialogs, and reading back the widget and notifications a human would actually see. It covers
startup, the wizard, a run spanning several real sessions, real auto-compaction, an approval
round-trip, and pi -p not hanging. When something is wrong in the product rather than in a
module, this is the scenario that notices.
Licence
MIT