Phase-loop build spec for a new extension in this monorepo. The builder and verifier follow this file literally. Adapted from
CLAUDE-VERIFY-PLAN.md(superseded/removed). Not shipped in the npm tarball.
Build a general async agent-dispatch primitive for the Pi coding agent:
pi hands a task to a headless agent CLI and the result is relayed back into
the live session mid-turn, non-blocking. The flagship consumer is phase
verification (verify_phase) — on a locked 9-case benchmark only subscription
Opus via claude -p clears the bar clean (6/6 catch, 0 false-merge, 0
false-fail, 3/3 audit; billed to the Claude subscription via oauthAccount, not
the API). A thin generic dispatch tool rides the same substrate so the neutral
name is earned. A driver/adapter seam (AgentDriver, sole impl claudeDriver)
keeps the core backend-agnostic.
- D1 Verify backend = subscription Opus via
claude -p(oauthAccount) — never the Anthropic API, never a local model. No API-key code path in the verify consumer. - D2 Scoped
--allowedTools "Bash Read Grep Glob". Never--dangerously-skip-permissions. Read-only verify. - D3
--model opus,--output-format json; verdict = last/VERDICT:\s*(PASS|FAIL)/imatch in the JSON envelope's.result. - D4 Async —
execute()returns immediately (PENDING); verdict arrives viasendMessage(…, { triggerTurn: true }). - D5
peerDependenciesper the template:@earendil-works/pi-coding-agent,@earendil-works/pi-tui,typebox— all"*". Notpi-agent-core; not the@mariozechnerfork. - D6 Wall-cap backstop (default 600 s, configurable) +
signal→child.kill. On cut / no-verdict pushUNVERIFIED, never auto-PASS. - D7 The verify tool reports verdict + evidence only — never merges or ticks.
- D8 Re-entrancy guard:
process.envsentinel at the top of the factory so it does not re-register inside a spawned child. - D9 Error signalling.
AgentToolResultsupports onlycontent,details,terminate— there is noisErrorfield, and a returnedisErroris silently ignored:pi-agent-core/dist/agent-loop.jsexecutePreparedToolCallhardcodesisError:falseon the success path, andfinalizeExecutedToolCallrebuilds the result as{content,details,terminate}only. A tool result is flaggedisError:trueonly whenexecute()throws (harness wraps it viacreateErrorToolResult), arg-validation/beforeToolCallblocks, or anafterToolCallhook overrides it. Therefore relay's synchronous setup-error path MUSTthrow(neverreturn { …, isError }). Async dispatch errors are delivered via thesendMessagepushback as anUNVERIFIEDverdict, independent of tool-resultisError. (grok-search:88's returnedisErroris the same latent no-op bug — tracked in its own issue, out of scope for this PR.) - D10 (seam)
AgentDriverinterface;claudeDriverthe sole implementation. Core (spawn, pushback, wall-cap, signal) is driver-agnostic. Verdict parsing lives in theverify_phaseconsumer, not the driver. Backend tool-name mapping is a driver function — the resolver stays backend-neutral (persona + skills + pi tool list); each driver maps that to its agent (claudeDriver→--allowedTools,codexDriver→-s). - D11 (use pi's APIs — never reinvent) Use pi's public extension APIs; never mirror,
fork, or hand-roll pi internals. Import pi types/helpers from the official
@earendil-works/*packages (e.g.createAssistantMessageEventStream()from@earendil-works/pi-ai). Adding an official pi package as a peer-dep is permitted and is not the forbidden@mariozechnerfork (D5). Re-implementing a pi contract by hand is a defect. - D12 (read-only by declaration + detect/present, NOT sandbox) relay does not OS-sandbox
dispatched agents. A role's read-only posture is expressed by its declared tools — relay maps
only those to the backend (withholding Edit/Write). A hard filesystem sandbox is rejected: it
breaks the verifier's own mandated gates (
vitestwrites scratch undernode_modules/inside cwd) and buys no verdict integrity — a bad verifier returns a wrong verdict without writing a byte, so write-blocking is the wrong lever. Instead, tree-hygiene is enforced by detection, not prevention: the verifier runs in the real git-tracked tree; after verify the orchestrator diffs the tree, and if the verifier changed anything it presents the diff to the human and asks discard-or-keep before acting on the verdict. Containment already holds — the verifier can't merge/tick (D7), its changes are git-visible + uncommitted, and merge is human-gated. (Supersedes the Phase-5 Seatbelt-sandbox spike, reverted.)
- Single feature branch
feat/relayholds Phase 0 (assets, already committed)- Phase 1 (scaffold) → one PR against
main. This supersedes phase-build's branch-per-phase default — it is an explicit project decision; the verifier must treat the single-branch layout as compliant, not a hygiene FAIL.
- Phase 1 (scaffold) → one PR against
- From Phase 2 on, each phase gets its own branch → its own PR (
feat/relay-phase2, …), per the standard branch-per-phase model; Phase 0+1 sharing one branch was a one-time bootstrap exception. - Conventional Commits, scope
relay.commitlint(header-max-length100) enforced by thecommit-msghook.biomeruns on staged files via thepre-commithook. - The builder commits its work to
feat/relayand STOPS. No PR, no merge, no tick. The PR is opened by the orchestrator only after verifier PASS + explicit human approval, then merged in the separate human-approved merge step. - Commit trailer:
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>. - No build artifacts committed (
.gitignorealready coversdist/,coverage/,node_modules/; jiti loads.tsdirectly — there is no build step). - Adding a workspace package requires regenerating the root
package-lock.json(npm install) and committing it in sync — CI's first step isnpm ci, which hard-fails on an out-of-sync lockfile before any check runs.
- Chosen: A1 "Boxed round-trip". Assets committed on
feat/relay@d33d7c7underassets/relay/(preview.{svg,png},logo/relay-mark*.svg,relay-icon.svg+-512.png,relay-favicon-32.png).
Entry phases: Phase 0 (assets) — done.
A working, private @jmcombs/pi-relay extension that registers verify_phase
and a generic dispatch tool, dispatches to a headless agent through the driver
seam, returns PENDING immediately, and relays the result back asynchronously via
sendMessage(…, { triggerTurn: true }).
All of D1–D10 above apply. In particular: no API-key path (D1); scoped tools,
never skip-permissions (D2); async, non-blocking execute() (D4); template peer-deps
(D5); fail-safe UNVERIFIED, never auto-PASS (D6); re-entrancy guard (D8); driver
seam with verdict-parse in the consumer (D10).
-
packages/relay/package.json— copypackages/_template/package.json; setname@jmcombs/pi-relay, keep"private": true;descriptioncredits Claude nominatively ("…dispatches to headless Claude Opus viaclaude -p"); peer-deps per D5;pi.image→assets/relay/preview.png;filesmust includedrivers/(it is imported at runtime) — e.g.["index.ts","drivers/","README.md","LICENSE"]; do not shipscripts/orindex.test.ts; topicalkeywords. -
packages/relay/tsconfig.json— copy frompackages/_template/tsconfig.json. -
packages/relay/LICENSE— MIT, copied from the template. -
packages/relay/README.md— what it does + usage; trademark disclaimer verbatim: "Not affiliated with or endorsed by Anthropic. Claude and Opus are trademarks of Anthropic, PBC."; note the verify backend is Claude-Opus-only. -
packages/relay/drivers/claude.ts— export theAgentDriverinterface andclaudeDriver(name:"claude",bin:"claude",buildArgsper D2/D3,parseResultreading the--output-format jsonenvelope{ type:"result", result, is_error }→.result). No verdict parsing here (D10). -
packages/relay/index.ts— default-exported factory: re-entrancy guard at the top (D8); registersverify_phase(TypeBoxType.Object({ phase, cwd?, prompt? })) anddispatch(TypeBoxType.Object({ prompt, cwd? })); both spawn viaclaudeDriverwithout awaiting and returnPENDING(D4); on child close, parse (verdict regex forverify_phase, D3) and push back viasendMessage(…, { triggerTurn: true })+events.emit; wall-cap +signal→kill withUNVERIFIEDon cut (D6); synchronous setup errorsthrow(never returnisError), async errors ride the pushback (D9). -
packages/relay/index.test.ts— registration smoke test (assertsverify_phaseanddispatchregister against a stubExtensionAPI; no live API, no network) plus a test asserting the synchronous setup-error path THROWS (D9 — proves the error is real, not a no-opisError). Repo convention (mirrorpackages/grok-search/index.test.ts). -
packages/relay/scripts/harness.mjs— standalone approach-B proof: stubExtensionAPI+ realclaude -p, reproducing Appendix A. Prints the 5 checks. Run manually; not part ofnpm run check.
- Gate 1 — repo quality gate.
Command:
npm run check(repo root). Expected: exit 0;biome check .clean,node scripts/typecheck.mjs0 errors,vitest runall pass includingpackages/relay/index.test.ts,check:versionspasses,security(secretlint +npm audit --omit=dev) passes. - Gate 2 — async approach-B proof (manual, real Opus).
Command:
node packages/relay/scripts/harness.mjs(withclaudeauthed viaoauthAccount). Expected: real stdout showing 5 OK lines — (1) tool registered; (2) duplex receive+reply; (3)execute()returnsPENDINGnon-blocking (< 15 s); (4) async pushback delivers a verdict; (5) pushback triggers a turn. - Gate 3 — types against earendil (call-out of Gate 1).
Command:
node scripts/typecheck.mjs. Expected: 0 type errors — confirms D5 peer-deps resolve and the tool returns onlyAgentToolResultfields (content/details/terminate; noisError, per D9). - Gate 4 — lockfile in sync (CI parity).
Command:
npm ci(repo root). Expected: exit 0 — the rootpackage-lock.jsonincludes the new@jmcombs/pi-relay@0.0.0workspace entry and installs cleanly. CI runsnpm cias its first step, so an out-of-sync lockfile fails every required check before any test/lint runs. (npm installmust produce no further lockfile diff.)
Entry phases: Phase 1 (merged to main @ 80e0be5). Own branch feat/relay-phase2
→ own PR.
Prove the real pi runtime delivers the async verdict as a follow-up turn — the
one thing the Phase-1 mock (stub ExtensionAPI) could not: real
sendMessage(…, { triggerTurn: true }) delivery. Answer Q1 (does triggerTurn
fire immediately, or queue until the orchestrator is idle / ctx.isIdle()), and if
delivery is not reliable, mirror pi-intercom's idle flush-queue so the verdict always
lands.
- Locked Decisions D1–D10 continue to hold; the
verify_phase/dispatchcontract and the driver seam do not change shape. - Any Q1 fix is additive/behavioral (queue-and-flush on idle) — it must not weaken
D4 (non-blocking
execute()) or D6 (fail-safeUNVERIFIED, never auto-PASS). - No new runtime dependencies (peer-deps stay = D5; lockfile stays in sync, Gate 4).
-
packages/relay/scripts/live-session.mjs— a scriptable live-session integration harness: launch a realpisession with--extension ./packages/relayin a programmatic mode (--mode rpc, falling back to--mode json), send a user turn that invokesverify_phase, keep the session alive, and capture from the real runtime: (a) the tool returnsPENDINGimmediately, (b) an async follow-up turn carryingVERDICT: PASS|FAILarrives via the pushback, (c) the timing oftriggerTurn(immediate vs. after idle) = the Q1 answer. Print OK/FAIL checks likeharness.mjs; exit 0 iff async delivery is observed. Not part ofnpm run check. -
packages/relay/index.ts— only if Q1 requires it: iftriggerTurnqueues until idle, add a pi-intercom-stylectx.isIdle()flush-queue so the verdict is delivered reliably. If Q1 shows immediate delivery, make no code change and record that in the report. Any change stays within D4/D6. -
packages/relay/README.md— a short "Live-session behavior" note documenting the Q1 finding (immediate vs. idle-queued) so users know when the verdict lands.
- Gate 2.1 — live async delivery (real runtime).
Command:
node packages/relay/scripts/live-session.mjs(realpi+claudeauthed viaoauthAccount). Expected: real output showing the tool returnedPENDING, then an async follow-up turn withVERDICT: PASS|FAILdelivered by the live runtime, plus the Q1 timing line. UNVERIFIED (never faked) if the environment genuinely cannot drive an rpc/interactivepisession — escalate to the orchestrator instead. - Gate 2.2 — regression.
Command:
npm run check→ exit 0 (vitest green; if a Q1 idle-flush change was made, add/extend a unit test covering it). - Gate 2.3 — lockfile parity.
Command:
npm ci→ exit 0;npm install→ no further diff (Gate 4 carries forward). Also re-run Phase-1node packages/relay/scripts/harness.mjsifindex.tschanged.
Appendix D items 1–8 hold; Gate 2.1 proven (or UNVERIFIED + escalated, never faked);
Q1 documented in the README; if index.ts changed, the async substrate still proves
out via harness.mjs.
Entry phases: Phase 2 (merged @ cf2add8). Own branch feat/relay-phase3 → own PR.
A relay role is an existing pi-subagent (persona .md + its referenced SKILL.mds)
executed on an external coding agent instead of a local pi model. The subagent
definition is the single source of truth; a per-agent driver adapts its standardized
fields into that agent's invocation. Relay registers a pi provider, so a subagent runs
externally simply by setting its model field — nothing changes but the processor.
- Trigger + model:
model: relay-claude/opus→ pi's nativeresolveModelroutes to relay's registered provider →claudeDriver→claude -p … --model opus. - Persona + skills: the driver resolves the persona body + each
SKILL.mdand injects them via--system-prompt-file(systemPromptMode: replace) /--append-system-prompt-file. Deterministic — our code writes the file; no model re-echo, no drift (this fixes the ~22% inline-prompt drift found in the Phase-4 baseline). - Tools: subagent
tools→--allowedToolsvia a rename map (read→Read, bash→Bash, edit→Edit, write→Write, grep→Grep, glob→Glob); pi-only tools with no external equivalent are dropped;thinking/ context-inherit fields are N/A. - The relayed subagent is single-turn (no pi-side tools; the external agent runs its own
tool loop) — one provider completion = one full
claude -prun returning the final text. pi's native subagent-async layer delivers the result, superseding relay's bespokeverify_phasetool + custom pushback (Phases 1–2).
Make any pi-subagent runnable on an external coding agent via the driver seam, and re-express the verifier as a relayed subagent — no bespoke tool, no inline prompt.
- Gate-zero spike — prove pi's
registerApiProvider/registerProvidercontract can represent "one completion = one fullclaude -prun" (single-turn, no pi-side tools, returns the final assistant text). If it cannot → STOP and escalate; fall back to Option A (relay stays a tool that resolves the subagent def) — a human decision, not the builder's. -
packages/relay/provider.ts— register providerrelay-claude;model: relay-claude/*routes here; a completion runs the driver and returns the external agent's final text. -
packages/relay/roles/resolver.ts— resolve a pi-subagent by name (persona.md+ itsSKILL.mds from~/.pi/agent), assemble the system-prompt file; apply the tool map. -
packages/relay/drivers/claude.ts— evolveclaudeDriver: fields →claude -p --system-prompt-file <assembled> --allowedTools <mapped> --model <model> --output-format json; preserve D1 (Opus for verify) / D2 (read-only). -
packages/relay/drivers/codex.ts— seam-only stub + documented mapping (deferred build; no codex account):codex exec, persona+skills → AGENTS.md /-cinstructions,-m,-s read-only,--json+-o <file>. - Retire
verify_phase's inline-promptpath; the verifier runs as a relayed subagent. Keep D1/D2/D6 semantics.
- No fork of pi core — use its extension APIs (
registerProvider/registerApiProvider). - D1/D2 preserved for the verify role (Opus, read-only); "roles" is a superset above the Locked Decisions. Fail-safe (D6) and driver seam (D10) preserved; D4 non-blocking now comes from pi's subagent-async layer.
- The subagent definition is unchanged whether run locally or via relay (single source).
- Gate 3.0 (spike) — a minimal
registerApiProviderproof that arelay-claude/*model yields oneclaude -prun's final text as the completion. Expected: real external output captured; or a documented escalation to Option A. - Gate 3.1 — the
verifierpi-subagent (defined once) run viamodel: relay-claude/opusthrough pi's native subagent system on one real phase returns a real Opus verdict (VERDICT: PASS|FAIL), with persona+skills provably injected (byte-exact system-prompt file). Proven live. - Gate 3.2 —
npm run check→ exit 0; lockfile parity (Gate 4).
- Inline skill content (fidelity). pi injects a subagent's skills as
<available_skills>references (name/desc/<location>); relayed toclaude -p, the external agent gets only pointers. The role resolver must read each referencedSKILL.mdand inline its full content into the assembled system prompt so the methodology is guaranteed present (prefer a pi API to expand skills → content if one exists per D11, else read the files). - Tool-name map → driver (D10). Move the pi→backend tool map out of
roles/resolver.tsintoclaudeDriver. Use pi's real tool names:read→Read, bash→Bash, grep→Grep, write→Write, edit→Edit, find→Glob,ls→Bash/drop; there is no piglob— drop the phantom entry. - Use pi's stream API (D11). Delete
stream.ts; usecreateAssistantMessageEventStream()from@earendil-works/pi-ai(add as peer-dep; write the ADR — official pkg, not the fork). - Real single-source verifier. Update
~/.pi/agent/agents/verifier.md:model: relay-claude/opus,tools: read, bash, grep, find(dropedit→ D2), wire the relay extension. Re-prove Gate 3.1 with the real verifier.md (not a stand-in). Report the dotfiles diff for the human to commit — do not auto-commit the user's dotfiles. - biome-ignore
.pi-subagents/inbiome.json(subagent-run artifacts tripnpm run check).
Appendix D items 1–8 hold; Gate 3.0/3.1 proven live (or spike-fail escalated, never faked); the verifier runs as a relayed subagent with no inline prompt (real verifier.md, skills inlined); the corrective refinements above are all applied; codex seam documented. The full 9-case accuracy re-benchmark is Phase 4, not this phase.
Re-run the locked 9-case verifier benchmark against the role-based verifier — the
relayed verifier subagent (model: relay-claude/opus, {phase,cwd}-only, no inline
prompt, methodology from its 4 skills) — reproducing the accuracy bar the old inline-prompt
design already cleared. Remove docs/prompts/ from the benchmark sandbox (the methodology
now lives in the role + skills, not a prompt file the case carries).
- Host / orchestrator seat = LOCAL
qwen3.6-35b-a3b@localhost:11439(llama.cpp). The benchmark's hostingpisession runs on qwen and dispatches the verifier subagent per case. - Verifier =
relay-claude/opus(external subscription Opus viaclaude -p) — the only external call. No local verifier seat (:11436stays down); no Haiku / API / non-local driver anywhere. (Last accuracy attempt wrongly used a Haiku driver — do not repeat.) - Builder seat (
ornith@:11437) is not exercised (cases are pre-built), may be up.
The benchmark is the PRIVATE jmcombs/verifier-benchmark repo (macprefs IP). ALL case
internals, the runner, and per-case verdict text stay in the private repo
(~/.cache/verifier-bench). Only the numeric summary may enter the public pi-extensions
repo. No macprefs token — path, brand hex, theme name, filename — in any pi-extensions file,
commit, or table. The through-the-role runner lives in the private repo.
- (private repo) Point the runner at the relayed verifier role (dispatch the
verifiersubagent atrelay-claude/opus) instead ofdocs/prompts/verify-phase.md; removedocs/prompts/from the sandbox cases. - (private repo) Run all 9 cases (3 correct + 6 defects incl. 3 audit-only) with the
host on qwen
:11439; score verdicts vsanswer-key.json(false-merge = dangerous, false-fail = over-strict, audit-catch); re-lock the private results. - (public repo, this branch) Record only the numeric summary in the results table below.
- Gate 4.1 (accuracy): 9/9 — 0 false-merge, 0 false-fail, 3/3 audit-catch via the role
(
relay-claude/opus), host on qwen. - Gate 4.2 (topology): captured invocations show the verifier at
--model opus(D1), read-only tools (D2); host session model =qwen3.6-35b-a3b; no non-local driver present. - Gate 4.3 (no leak + removal):
git grepinpi-extensionsfinds no macprefs token;docs/prompts/removed from the sandbox; only the numeric summary is public. - Gate 4.4 (repo):
npm run checkgreen (public repo gains only the summary doc).
| metric | baseline (inline-prompt) | Phase 4 (through the role) |
|---|---|---|
| correct | 9/9 | 9/9 |
| false-merge | 0 | 0 |
| false-fail | 0 | 0 |
| audit-catch (spec-only defects) | 3/3 | 3/3 |
| avg wall / case | ~150 s | ~149 s |
Baseline = the old inline-prompt/verify_phase design. Phase 4 filled the right column via the
role — host on local qwen3.6-35b-a3b (:11439), verifier on relay-claude/opus (Opus,
read-only) — with docs/prompts/ removed from the sandbox. No macprefs specifics — numeric only.
9/9 reproduced through the role with the correct local-host / Opus-verifier topology;
docs/prompts/ removed from the sandbox; private results re-locked; only the numeric summary in
the public repo; no IP leak (Gate 4.3 clean). Appendix D full-repo regression holds.
Wire the real pi phase loop so the orchestrator dispatches the relay verifier
(verifier.md, already relay-claude/opus) and routes PASS → human merge-gate, FAIL →
remediation — and harden the two Phase-4 findings per the locked architecture. Scope: demonstrate,
then cut over (Q1) — prove the wiring end-to-end this phase; the real loop fully self-hosts on
relay only after Gate B (Phase 6). Spans two repos: packages/relay (enforcement code) and the
user's dotfiles orchestration (phase-orchestrate skill, verifier.md, merger.md) — dotfiles
changes are reported as diffs for the human to commit (as in Phase 3/4), never auto-committed.
- Revert the driver OS sandbox (the
--settingsSeatbeltdenyWrite:[cwd]+--disallowedToolsenforcement from the reverted spike). It broke the verifier's own mandated gates (vitestwrites scratch tonode_modules/.vite-tempinside cwd → EPERM, zero tests collected) and buys no verdict integrity (a bad verifier returns a wrong verdict without writing a byte). A read-only role is read-only by declaration: relay maps only its declared read-only tools to the backend (no Edit/Write) and does not OS-sandbox. The read-only verifier MUST be able to run the repo's fullnpm run check(incl.vitest) in-tree.
- Enforcement that "the verifier didn't tamper with the tree" is by detection, not prevention: after verify, the orchestrator diffs the working tree; if the verifier changed anything it presents the diff to the human and asks whether to discard or keep, and does not act on the verdict until the human decides. Containment already holds — the verifier can't merge/tick (D7), its changes are git-visible + uncommitted, and merge is human-gated.
phase-orchestrateskill instructs the orchestrator to dispatch verify exactly once, then wait for the verdict. relay stays a clean per-completion provider (no dedup state); D8 re-entrancy guard is the process-level backstop. No relay code for this — it's the caller's contract.
- Orchestrator routes the relay verdict: PASS → stop at the human merge-gate (never auto-merge/
tick — D7); FAIL → remediation (back to builder with the evidence).
merger.mdunchanged except to consume the relay verdict.
- A read-only verifier makes no file edits, so pi-subagents flips its exit 0 → 1 via the
completion/no-edits guard (
execution.ts:838→ "completed without making edits for an implementation task"); a failed run's inline output is then replaced by a[failed]summary, burying the verdict (the orchestrator must artifact-grep to recover it → risks misrouting a real PASS/FAIL as an execution failure). Fix (set once, robust):completionGuard: falseinverifier.mdfrontmatter — the agent loader honors it (agents.ts:1156coerces the string), andexecution.ts:838then skips the guard entirely. Do NOT setacceptanceon the dispatch: the acceptance gate only flips the exit when acceptance is explicit (execution.ts:1099); left inferred (explicit=false) it never fails the run.⚠️ The earlieracceptance: { level: "none", reason }approach is rejected on two counts (both observed live): (a) the local orchestrator can't emit the nested object — qwen serialized it as a JSON string → tool-validation error → forced afalsefallback; (b) it targeted the wrong gate (the completion guard, not acceptance, is what failed the run). WithcompletionGuard:false+ no acceptance param the verify run exits 0 and itsVERDICTflows into the subagent tool result inline (pi defaultoutputMode:"inline") — no artifact-grep. Not a relay issue — a local read-only verifier hits the identical guard.
- Gate 5.1 (no sandbox breakage): the read-only verifier runs the repo's full
npm run check(incl.vitest) to a real pass/fail in-tree — no EPERM, tests actually collected. Proves the reverted sandbox unbroke verification. - Gate 5.2 (routing) — CITE REAL ARTIFACTS: end-to-end via the real orchestrator (qwen
:11439) → relay verifier (Opus): one PASS halts at the human merge-gate (no auto-merge/tick, D7); one FAIL → remediation. Commit/cite the run transcripts (distincttoolCallIds) — a claim is not proof (the first attempt cited none). - Gate 5.3 (dispatch cardinality): exactly one verify dispatch per scenario, re-derivable from the cited artifacts.
- Gate 5.4 (tree-hygiene detect+present): a verify that mutates the tree is detected and surfaced to the human (keep/discard) before the verdict is acted on — demonstrated.
- Gate 5.5 (repo):
npm run checkgreen; lockfile in sync; ADR 0002 rewritten to the detect model + indexed (Appendix B). - Gate 5.6 (verify exits 0 + verdict in-result, VERBATIM skill): with
completionGuard: falseonverifier.md(and NOacceptanceparam on the dispatch), the verifier run exits 0, and itsVERDICTappears in the subagent tool result (not only an artifact). Gates 5.2–5.4 routing re-proven with the orchestrator driven by the verbatim shippedphase-orchestrateskill — NO hand-injected grep/outputPath logic in the prompt.
Driver sandbox reverted; the read-only verifier runs the full npm run check in-tree (Gate 5.1);
orchestrator dispatches verify exactly once, routes PASS/FAIL correctly, and presents any verifier
tree-change to the human (Gates 5.2–5.4) — all proven with cited artifacts; real-loop cutover
deferred to post-Gate-B; D12 + ADR rewritten; dotfiles diffs reported for the human to commit;
Appendix D regression holds.
- Phase 6 — Gate B. ✅ DONE. Gate B measured through the shipped
verifierrole (relay-claude/opus + its real skills), not bareclaude -p— the only lens that catches a role-level false-merge. Full 9-case pass: 6/6 gate-defect catch, 0 false-merge, 3/3 audit-catch, 3/3 correct→PASS, 0 UNVERIFIED; audit-stability 9/9. A stochastic false-merge on a committeddist/artifact was root-caused to thegit-hygieneskill and fixed at source. Then flipped"private": false+ registered Release Please;@jmcombs/pi-relay@1.0.0published (bootstrap 0.0.0 → OIDC-managed 1.0.0).verifier.mdrepointed tonpm:@jmcombs/pi-relay. - Phase 7 — (optional) Duplex escalation. Intercom-broker ask-reply for human escalation (true pi↔pi / cross-session channel).
Non-blocking spawn + parse + push-back. The shippable version lives behind
claudeDriver (TypeScript, @earendil-works types); scripts/harness.mjs
reproduces this proof against a real claude -p.
function runClaudeAsync(prompt, cwd, onDone, signal) {
const child = spawn("claude", [
"-p", prompt, "--output-format", "json", "--model", "opus",
"--allowedTools", "Bash Read Grep Glob", "--max-turns", "80",
], { cwd, stdio: ["ignore", "pipe", "pipe"] });
let out = "";
child.stdout.on("data", d => (out += d));
signal?.addEventListener?.("abort", () => child.kill("SIGTERM"), { once: true });
child.on("close", () => {
let verdict = "UNKNOWN", result = "";
try { result = String(JSON.parse(out).result ?? ""); } catch {}
const m = /VERDICT:\s*(PASS|FAIL)/i.exec(result);
if (m) verdict = m[1].toUpperCase();
onDone({ verdict, result });
});
}The 5-step proof harness.mjs must print: (1) verify_phase registered;
(2) duplex receive+reply; (3) execute() returned PENDING non-blocking (< 15 s);
(4) async pushback delivered a verdict; (5) pushback triggered a turn.
| ADR | Slug | Phase | Status |
|---|---|---|---|
| 0001 | pi-ai-peer-dependency | 3 | Accepted |
| 0002 | per-role-execution-posture (read-only by declaration; tree-hygiene by detection — sandbox rejected) | 5 | Accepted |
If a deviation from a literal TODO or a Locked Decision is genuinely required,
create docs/decisions/000N-<slug>.md (MADR-lite: Context / Decision /
Consequences), add a row here, and route the deviation to the orchestrator for
human decision. Do not self-approve. (ADR 0001 records the D11-mandated addition of
the official @earendil-works/pi-ai peer-dep + deletion of the hand-rolled
stream.ts — an alignment with a Locked Decision, not a deviation from one.)
- Phase 0 — Logo & brand assets
- Phase 1 — Scaffold + driver seam + unit-prove
- Phase 2 — Live-session integration
- Phase 3 — Relay Roles (provider seam)
- Phase 4 — Accuracy regression (through the role)
- Phase 5 — Wire into the phase loop
- Phase 6 — Gate B (published
@jmcombs/pi-relay@1.0.0) - Phase 7 — (optional) Duplex escalation
npm run checkexit 0 across the whole workspace — no predecessor package broken (regression is a FAIL even if Phase 1's own gates pass).- Every Phase-1 Actionable TODO path exists exactly as written; no relocation or rename without an ADR.
- Locked Decisions D1–D10 upheld — spot-check: no API-key path (D1); tools scoped,
no skip-permissions (D2);
execute()non-blocking (D4); peer-deps exactly D5; fail-safeUNVERIFIEDon cut (D6); re-entrancy guard present (D8); no returnedisErroranywhere — synchronous setup errorsthrow(D9); singleclaudeDriverimpl, verdict-parse in the consumer (D10). - README trademark disclaimer present verbatim.
package.jsonfilesincludesdrivers/; excludesscripts/and tests;"private": true.- Git hygiene: work committed on
feat/relay(single-branch convention above); Conventional Commits; no build artifacts; cleangit statusafter commit. - Gate 2 (
harness.mjs) proven with realclaude -pstdout, or explicitly marked UNVERIFIED with the reason if the environment cannot reachclaude(never PASS an unproven gate). - Root
package-lock.jsonin sync —npm ciat repo root exits 0 andnpm installyields no further lockfile diff (Gate 4; CI parity — CI runsnpm ci).