@computesdk/pi-loops1.0.9
Hill-climbing loops as Pi units: one `loop` tool, style agents, forked attempt sessions in git worktrees, a judge keeps the best.
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
@computesdk/pi-loops
Hill-climbing loops for coding agents, built from Pi's own units.
Your Pi session is the parent. One tool, loop, runs a round: three attempts fork your session into their own git worktrees, each driven by a style agent (safe, bold, wild), and a judge keeps the best. Call it again with from to climb from the winner.
> /loop make search() in search.mjs faster; check is node test.mjs; bench is node bench.mjs and prints ms
loop-20261029T141200 r1 · judging
safe done 14 tools · 61k tok — Replaced the DP table with two rolling rows
bold done 22 tools · 98k tok — Early-exit Levenshtein with a k-best bound
wild done 19 tools · 84k tok — Bit-parallel Myers distance
Round 1 — kept R1-bold
attempt style result judge
------- ----------- -------- ------------------------------------------------
CURRENT best so far 422.6 ms 422.6 ms re-measured
R1-safe safe 188.0 ms 188 ms (2.25× better) · same algorithm, less allocation
R1-bold bold 11.5 ms 11.5 ms (36.75× better) · bounded search, identical output
R1-wild wild vetoed vetoed: memoises distances across calls
R1-bold is best at 11.5 ms vs 422.6 ms now (36.75×); tests pass. 1 attempt(s) failed tests or were vetoed.
Best so far: R1-bold (3f2a9c1, branch loops/loop-20261029T141200/r1-bold) at 11.5 ms
search.mjs | 48 ++++++++++++++++++++++++++++--------------------
Apply (tracked files only): git checkout 3f2a9c1 -- search.mjs
Next round: loop({ from: "loop-20261029T141200" })
Install
pi install npm:@computesdk/pi-loops # or: pi -e /path/to/pi-extensions/packages/pi-loops/extensions/loop.ts
Node ≥ 22.18 (TypeScript extensions are stripped, not compiled) and a git repo with at least one commit. Attempts use whatever model your TUI is on (override per role in settings).
Providers
Morph and Particle ship as their own packages, so you can install either one without this one:
| Provider | Package | Key | Models |
|---|---|---|---|
| Particle AI | pi-particle |
PARTICLE_API_KEY (pai-…) |
particle-1, deepseek-ai/deepseek-v41-flash-1017, glm-5.3-flash |
| Morph | @computesdk/pi-morph |
MORPH_API_KEY |
morph-kimik3, morph-kimik3-fast, morph-glm53-744b, morph-glm53flash, morph-dsv41flash, morph-dsv41flash-1017, morph-dsv4flash |
They live in this repo under packages/pi-morph and packages/pi-particle; install them with pi install npm:@computesdk/pi-morph (or pi install npm:pi-particle). Loops itself is model-agnostic — it runs on whatever your TUI is on.
The one tool
loop({
goal, // what to improve
check?, // command that must pass (tests)
bench?, // command that prints the metric → bench mode
metric?, // { key: "ms", unit: "ms", better: "lower" }
protect?, // files attempts may not change; restored before judging
build?, shots?, // visual mode: build writes screenshot files, a judge panel scores them
rounds? = 1, // rounds to run in this call
from?, // run id (or "last") to continue from its last judged round
rules?, // rules every attempt must follow; the judge vetoes breaches
})
/loop <ask> is the shortcut: the model passes your sentence on as these fields and calls the tool. The extension registers the command itself (from prompts/loop.md), so it is there with pi -e packages/pi-loops/extensions/loop.ts too, not only after pi install.
/loop init once, then /loop <goal> writes the brief. /loop init has your Pi session read package.json and the config files (read tools only, no bash) and call loop_init with the project's mechanics — serve (starts the app on $PORT), check, bench + metric, protect — shown in a dialog (Save / Change… / Discuss) and written once to .pi/loops.json. The first /loop <goal> in a repo without them does that step first. Each /loop <goal> then only writes the brief the attempts get — goal (what good looks like), rules, directions (a goal-specific angle for safe / bold / wild), routes to screenshot for visual goals — and calls the tool, which shows the brief with Approve / Change… / Discuss. Nothing runs until you approve (headless runs as given; confirm: false skips the dialog). Same split as the original Loops: the project's spec once, one sentence per run; check / bench / build / shots / protect on the tool are overrides.
Bench mode (bench set): a worktree is a fresh checkout, so each attempt sets the project up the way the repo's docs say, makes its change, and runs check and bench itself. Then the judge — an agent with read/bash, no edit — re-measures every candidate where it lives (CURRENT in your checkout in round 1, in the previous winner's worktree after that; each attempt in its own worktree), bench repeat times with the best sample counting, protected files put back first, reads each diff and vetoes rule breaks and cheating (special-cased inputs, caching across calls, skipped work, weakened checks). It returns the numbers; pickWinner applies the check gate, the veto and minGain. Loops itself knows nothing about installing or running a project.
Visual mode (no bench): each attempt runs build itself and must leave the shots files (PNG/JPEG) in its worktree; a judge panel scores them (median of 3) and the top two go head-to-head against CURRENT, both orders; a candidate wins with a clear majority and a score not below CURRENT's. CURRENT's shots are taken with the same build in your checkout (round 1) or the previous winner's worktree. Uncommitted edits in your checkout are part of the baseline in both modes: round 1 forks from a commit of your working tree (git stash create, your checkout is untouched), so attempts and CURRENT start from the same state.
Screenshots come from lib/shot.ts: headless Chrome driven over the Chrome DevTools Protocol, no dependencies (CDP over Node's built-in WebSocket). The default is the whole page, not the 900px fold — a judge scoring a screenshot should see all of it — with --viewport for the fold and --selector '#card' for one element. Any Chrome/Chromium works, including chrome-headless-shell, the headless-only binary that has no --screenshot flag at all and is what npx playwright install chromium-headless-shell puts in ~/.cache/ms-playwright; LOOPS_CHROME overrides discovery. Loops looks for one before the round starts and, when there is none, fails once with the install hint instead of three build failed lines. A route that 404s or refuses the connection is an error, never a picture of the browser's error page. The sheet is montage (ImageMagick) when installed.
The pick is yours. After every judged round you get the round card: when the run has shots (either mode) a side-by-side sheet of CURRENT and the attempts with their numbers (.pi/loops/<run>/r<n>.png, also shown inline in terminals with image support), and a picker — keep the judge's pick, take another attempt instead, show an attempt's diff, or mark an attempt's session to open afterwards (/loop-open R1-bold, /loop-open judge). The result ends with the climb: one line per round, CURRENT, each attempt with its delta, what was kept.
What a round spends. Every nested model call — the attempts, each judge sample, the council — is counted: the result carries usage, and the card ends with Spent 68.8k tok · $0.0000. A round started from the TUI returns before it finishes, so there is no tool result to carry that; instead Loops writes one type: "usage" entry per model to the session (kind loops, note naming the run), which is the entry kind Pi sums into its token and cost totals and shows in the footer. So a background round lands in /cost and the footer like any other model call, and /resume of that session still adds up.
Steering while it runs. In the TUI the tool returns as soon as the round has started and the card arrives as a message when the judge is done, so the conversation stays open: loop_status (progress), loop_talk("R1-bold", "…") (a steering message into that attempt's session), loop_rule("…") (told to every running attempt, enforced by the judge from this round on, kept for later rounds), loop_pick("wild") (keep that attempt, or CURRENT, whatever the judge scores), loop_stop() (abort; worktrees stay, loop({ from }) resumes). After the card, loop_apply() takes the run's best into the checkout — the winner's changed tracked files from its commit, as one commit; files it added or deleted are listed, not applied. Headless (pi -p) or with wait: true the tool blocks until the round is done, as before.
Before round 1 of a bench run a council (two proposals + a synthesis) writes the rubric the judge judges against; your rules become decisive criteria in it.
The tool never applies the winner itself: the result includes a git checkout <commit> -- <files> line for the files the winner changed that are tracked in your baseline, and lists new files the attempt left in its worktree (scratch checks, helpers) separately so you decide. Headless (pi -p) there is no picker: the judge's pick stands and the result says so.
Pi units
| What | Where |
|---|---|
| Style agents | agents/safe.md, bold.md, wild.md — frontmatter tools/model, body is appended to the attempt's system prompt |
| Judge | agents/judge.md — read, grep, find, ls, bash, never edit; in bench mode a real session (listed next to the attempts) that measures and reviews in the candidates' directories; in visual mode a one-shot panel |
| Council | agents/council.md — no tools; a one-shot agent session so it gets Pi's auth, providers and session header |
| Prompt | prompts/loop.md → /loop |
| Attempts | SessionManager.forkFrom(<your session or the winner's>, worktree, sessionDir) + createAgentSession — pi --session lists them next to yours; /resume one to see what it did |
| Worktrees | .pi/loops/<run>/r<n>-<style> on branch loops/<run>/r<n>-<style>; the winner's stays as CURRENT for the next round |
| Timeout / budget | attempts are told their limits up front and to land a working change early; there is no token cap by default (tokenBudget counts every turn's whole context, so 1M was a dozen tool calls on a big repo — set it only if you want one); one cut off by timeoutMin or tokenBudget is still committed and judged if it changed anything (the judge runs the check anyway); it is only dropped when it left nothing |
| State | Pi durable entries: a loops-state snapshot plus one loops-round per judged round. .pi/loops/<run>.json is a rebuildable index (status, winner, rounds, worktree paths) written beside it and rebuilt from the session when missing. Added to .gitignore on first write. |
Override an agent by putting <role>.md in <repo>/.pi/agents/ or ~/.pi/agent/agents/.
Settings
One command. /loop <goal> runs a round; bare /loop shows this repo's card — models per role, rounds, budget, timeout, the last run — with a picker (models / rounds / budget / timeout; esc leaves); /loop models (the model picker), /loop rounds 3, /loop budget 1.5m, /loop timeout 20, /loop open R2-bold (or judge: resume that session) and /loop runs (every run in this repo with its climb lines) set or show directly. A goal that happens to start with one of those words still works as a goal when more follows.
Config is Loops' own file, the way Pi extensions keep theirs: <repo>/.pi/loops.json for this repo (what /loop … writes) and ~/.pi/agent/loops.json for your defaults everywhere; merged, the repo wins per key. Tool arguments (loop({ rounds: 3 })) override both for one call.
{
"models": { "safe": "opencode-go/kimi-k3", "bold": "opencode-go/claude-haiku-5-5", "wild": "opencode-go/gpt-5.6-luna", "judge": "opencode-go/deepseek-v4-pro", "council": ["opencode-go/kimi-k3", "opencode-go/gpt-5.6-luna"] },
"serve": "npm run dev -- --port $PORT", "check": "npm test", "protect": ["test/"],
"rounds": 1, "timeoutMin": 15, "tokenBudget": null, "minGain": 0.05,
"attempts": 3, "repeat": 2, "judgeSamples": 3, "judgePairs": 2, "council": true
}
Judging. A visual round is judged on screenshots, so the judge — and every panel member, when the panel is on — must be a model that can see images; one that cannot is refused before the round starts (it would score every candidate the same, which reads as a tie), with the fix in the message. The one-shot sessions the judge and council run in load no extensions, so Loops copies the host's provider registrations into them: providers added by an extension (rather than built into Pi) can judge just as the attempts can, and nothing else about that session changes — no tools, no extension commands. Visual and code-only rounds are scored by a panel: every council model (by default the attempts' models) scores every candidate 0–10 against the goal and rules, labels blind, median across models and samples; the judge model keeps the vetoes and the head-to-head against CURRENT that decides the round. The card says when the panel split (Panel split on R1-wild: kimi-k3 3 · qwen3.8-flash 8 · glm-5.3 8). "judgePanel": false in .pi/loops.json goes back to the judge alone; bench rounds are measured, not scored.
Models. A round is also a comparison of models: the three attempts and the council members should each run on a different one. The first /loop in a repo (interactive, nothing set yet, more than one model available) asks four times — a model for each attempt, one for the judge (the council is the attempts' models) — and writes them to .pi/loops.json; /loop models re-opens that picker. models keys are the roles — safe, bold, wild, judge, scout, and council (a string or a list, one member per model). Roles left unset are spread over the pool: models.pool (a list of provider/id, id or provider/* patterns), else Pi's own enabledModels, starting from your current model; with no pool everything runs on your current model and the result says so. The round table and climb show each attempt's model. "scout": false turns the scout off (a bare goal then runs in visual mode on the goal alone).
(loops inside Pi's settings.json still works as the lowest-precedence source.)
Stopping and resuming
Ctrl+C aborts the attempts and leaves their worktrees and sessions in place. loop({ from: "<run>" }) (or from: "last") re-runs the unfinished attempts of that round, judges it, and continues; a judged round is never redone. Delete .pi/loops/<run>/ and the loops/<run>/* branches (git worktree prune) when you are done with a run.
Library
lib/measure.ts (shell runner, protect restore, diff), lib/judge.ts (pickWinner over the judge's numbers, the visual panelJudge), lib/rubric.ts (council synthesis, human rules), lib/shot.ts (serve + headless-Chrome screenshot CLI over CDP: full page, viewport or one selector), lib/sheet.ts (the labelled sheet, via ImageMagick's montage when installed). All model calls are injected, so pnpm test runs them against stubs — no model needed. The whole package is TypeScript: Pi loads the .ts files directly (no build step), and pnpm check (or pnpm -r check from the repo root) checks them. It ships no main/exports on purpose — Node refuses to strip types inside node_modules, so a programmatic import "@computesdk/pi-loops" would need a build; Pi loads the files through jiti and does not.
test/fixtures/fuzzy/bench is the test repo: cp -r test/fixtures/fuzzy/bench /tmp/fuzzy && cd /tmp/fuzzy && git init && git add -A && git commit -m base, then run the /loop above.