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.