A CLI built on the pi SDK (@earendil-works/pi-coding-agent) that connects to a
local pi agent session and drives the repository's reference-intake loop —
ported from donutloop/gusty tools/pi-loop
and specialised: the AGENTS.md feature loop became the raw_links.md →
prompts/process_url_reference.md pipeline, with the same fresh-session-per-round
and resume-on-stop semantics.
1. claim the harness takes the FIRST pending URL out of raw_links.md,
removes that line (tmp file + rename) and journals the claim in
.pi-loop-queue.json ← the agent never sees the queue step
2. session a brand-new pi session is created (new id, new session log) and
prompted: read AGENTS.md, then execute
prompts/process_url_reference.md for `URL parameter is <URL>`
3. tools read/edit/write/bash/grep/find/ls + `webfetch` (this dir's fetcher)
4. verify git HEAD moved? annotated tag present? queue line committed?
5. persist journal status + .pi-loop-state.json, dispose the session, wait
PI_LOOP_DELAY_SECONDS (default 60 s) and start round N+1
Rules that make the loop safe to leave running:
- raw_links.mdis harness-owned. A link is removed the moment it is picked — never by the agent, never after the fact. The removed line is kept in the journal (- lineText) so it can be put back with- --requeue-failed.
- A link is never dispatched twice. The journal is written before the file is rewritten, so even a crash between the two writes cannot re-run a URL.
- Already-indexed links cost no round. URLs found in references.md/reference_coverage.md(step 1 of the workflow aborts on those anyway) are dequeued asduplicatewithout waking the model. Canonical matching ignoresutm_*,hl, fragments,www., trailing slashes and the default port;--known-level=2additionally ignores the query string.
- Progress is monotone. If a round commits nothing, the harness commits the
dequeue itself (chore(loop): dequeue <host> from raw_links.md) and journals the round asdequeued-no-commit, so one broken round cannot stall or loop the run.
Identical to upstream pi-loop: a config in the exact format pi reads from
<agentDir>/models.json lives in the repo's setup/ dir, and pointing the loop at
a new model is a file drop there — no code change.
- Discover: --models-config=PATH>PI_LOOP_MODELS_CONFIG/PI_MODELS_CONFIGnewest named profile setup/pi_*.json(setup/pi.jsonis the generic fallback) inPI_LOOP_SETUP_DIR,<cwd>/setup, thentools/../setup.
- Select: --provider/--model>PI_LOOP_PROVIDER/PI_LOOP_MODEL> first model of the first provider.
- Publish to <agentDir>/models.json(merged, unrelated providers andmodelOverridespreserved, legacy top-levelmodelskey dropped).
- Resolve through the SDK's own ModelRuntime, falling back to an inline model.
- Probe <baseUrl>/modelsand fail fast (exit 1) when the server is down or does not serve the selected id (PI_LOOP_SKIP_MODEL_CHECK=1to skip).
agentDir defaults to <cwd>/.pi/agent (upstream used the repo root) so that
models.json, the auth copy and the session logs stay out of the paper commits —
.gitignore covers .pi/, .pi-loop-state.json and .pi-loop-queue.json.
Inside another pi session: pi exports PI_MODEL/PI_PROVIDER into every command it
runs; pi-loop ignores those two when PI_CODING_AGENT=true and says so. Use
--model=ID / PI_LOOP_MODEL to be explicit. Any setting also accepts the
clash-free PI_LOOP_<NAME> spelling, which always wins.
node tools/pi-loop/pi-loop.mjs # loop over raw_links.md forever
node tools/pi-loop/pi-loop.mjs . --once # exactly one link
node tools/pi-loop/pi-loop.mjs . --rounds=5 # five links
node tools/pi-loop/pi-loop.mjs . --force-reset # ignore saved progress, restart at round 1
node tools/pi-loop/pi-loop.mjs . --push # allow rounds to publish (default: never)
node tools/pi-loop/pi-loop.mjs . --next # JSON: the link the next round would claim
node tools/pi-loop/pi-loop.mjs . --status # JSON: queue + journal summary
node tools/pi-loop/pi-loop.mjs . --requeue-failed # put lost links back into the queue
node tools/pi-loop/pi-loop.mjs . --retry-failed # also re-dispatch aborted/no-commit links
node tools/pi-loop/pi-loop.mjs . --dry-run # connectivity round, no tools, no queue, no state
node tools/pi-loop/pi-loop.mjs . --describe # resolved config as JSON (pipe to jq)
node tools/pi-loop/pi-loop.mjs --help # full flag list (upstream + loop flags)Env (each also accepted as PI_LOOP_<NAME>, which wins; --flag beats both):
A stop — SIGINT, error, or the round target — persists { round, lastCommit } to
.pi-loop-state.json before exiting. On the next run:
- committed progress, lastCommit == HEAD, clean tree → RESUMES at the next round;
- otherwise (no state, lastCommit != HEAD, dirty tree) → RE-EXECUTES AGENTS.md at round 1.
Claims survive both paths: a URL that was dispatched is never dispatched again until
you explicitly --requeue-failed / --retry-failed it.
Unchanged from upstream (sdk-discovery.mjs): $PI_LOOP_SDK_PATH → a node
dependency → the pi managed install ($PI_MANAGED_INSTALL_ROOT, ~/.pi/agent/install)
→ the pi binary on PATH → global npm prefixes, with every candidate printed on a
miss.
webfetch.mjs is the same code the round's webfetch tool calls, so a script and a
round always see the same extraction.
node --test tools/pi-loop/*.test.mjs # or: npm test --prefix tools/pi-loop- link-queue.test.mjs— claiming rules: FIFO pick + line removal, canonical dedup (- www.,- /,- hl=,- utm_), duplicate lines, journal blocking of re-dispatch,- --retry-failed, known-index pre-filter, atomic write,- --next,- --requeue-failed.
- webfetch.test.mjs— HTML → text, entity decoding, truncation budget, PDF / binary / HTTP-error / network-failure paths, citation URL canonicalisation.
- round-prompt.test.mjs— the round prompt carries exactly one URL, forbids touching- raw_links.md, names every artefact, keeps push off by default.
- models-config.test.mjs,- sdk-discovery.test.mjs— inherited from upstream.
End-to-end:
node tools/pi-loop/pi-loop.mjs . --dry-run # expects PI_LOOP_OK, queue untouched
node tools/pi-loop/pi-loop.mjs . --once # one real link: commit + tag- pi installed (managed install, global npm package, or PI_LOOP_SDK_PATH).
- The local model server up on the baseUrlin the chosensetup/config (setup/boot_agent.sh);--describetells you whether it is reachable.
AGENTS.md (repo root) is read by every round and defines the quality bar:
merge-only edits, English/German parity, source-before-claims, count reconciliation,
one commit + one annotated tag per link.