This repository was archived by the owner on Jul 24, 2026. It is now read-only.
Implement the agent spec as convoy's declaration contract, plus convoy's VRS - #89
Merged
schickling-assistant merged 12 commits intoJul 21, 2026
Merged
Conversation
schickling-assistant
added a commit
that referenced
this pull request
Jul 20, 2026
…ing them `convoy add` is declare-only — it writes a catalog entry and launches nothing — so checkDevTask's add-loop declares cos/supervisor/worker and then polls all three for `available` against sessions that were never spawned. It fails by construction on every machine, and runReadinessSuite returns 0 only if EVERY check passes, so this alone reds a newcomer's first `convoy doctor`. One `up --once` pass launches all three declared tiers. SCOPE: this is the FIFTH of five declare-then-assert checks missing the reconcile. PR #89 already inserts the same `up --once` into the other four (checkTmpNetwork, checkDings, checkStateExternalization, checkExactlyOnce) and owns those hunks, so this commit deliberately does NOT duplicate them — it closes the one #89 missed, in a region #89 does not touch. The suite is only fully green once both land. Adds a source-level guard for the declare→assert seam: the checks spawn real agents (minutes, real auth) so they can't run in a unit test, but the invariant "a check that declares agents reconciles them, BEFORE it asserts liveness" is structural and checkable. The guard carries a shrink-only exception list naming the four checks whose reconcile lands in #89; whoever merges #89 deletes the list and the guard covers all five. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
This was referenced Jul 20, 2026
…entity against the bus Three formats, one semantics path: each decodes to the same canonical object (src/spec-format.ts) and exactly one parser applies field rules, so "identical semantics" is structural rather than a promise three parsers have to keep. KDL needed a node->table mapping the spec leaves implicit; the load-bearing rule is that a node with one argument AND children names a sub-table, which is what gives `[pty.<name>]` a KDL spelling. Identity validation now DERIVES its grammar from smalltalk (`isAgent`) instead of convoy's own regex. The two disagreed: `worker_fodfix` passed convoy and was rejected by the bus, which is the worst failure ordering — the name reaches the synced catalog, propagates to every peer, and only fails on first write. The length bound is derived from pty's socket (`<PTY_ROOT>/<prefix>.<id>.ding.sock` against sun_path 104), so it is contextual rather than a taste constant. Adds the spec's agent-level fields (supervisor, prefix, pty tasks, render) plus `bin`, which replaces the bare harness name when deriving the command so wrapped deployments are not bypassed. Credentials ride in `env` — no `account` field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
Discovery scans `catalog/**/*.{kdl,toml,json}` and keeps what declares an agent,
replacing the flat `<identity>.toml` listing. Identity comes from content, so a
filename no longer names an agent; path segments supply host/identity defaults
and a path-vs-content disagreement is a warning, because erroring would strand
an agent over a directory name and silently preferring the path would make one
spec mean different things in different folders.
One deviation from the spec, deliberately: the spec recognises files "containing
identity and role", which makes a spec that omits one silently NOT an agent —
declared-looking, never running, no error. Recognition here is EITHER field (a
claim to be a spec) and completeness is a validation error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ
agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15
agent-tool: Claude Code
agent-tool-version: 2.1.215
agent-model: claude-opus-4-8
agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json
agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json
tooling-profile: dotfiles@unknown-dirty
…ectable `convoy add` checked identity for truthiness only and never parsed, so a name the bus rejects was written into the SYNCED catalog and propagated to every peer before anything noticed. It now validates with the real context — bus grammar, this network's socket budget, and the identities already declared. Adds `convoy rename`, which moves the catalog entry AND the whole bus folder, so in-flight mail survives by virtue of the move being a move. The tombstone left behind is honest about its scope: convoy reads it, smalltalk does not — smalltalk has no redirect and its send path mkdir -p's the inbox, so a stale peer sending to the old name after a rename creates an unread folder. It is written as a bare dotfile with no inbox/archive/status so `st agents` cannot resurrect the old name. Also refuses durable context/ under a counter-named identity (`worker-2`): the counter re-derives per parent lifetime, so a restart would let an agent read a stranger's now.md as its own memory. The refusal is narrow — the agent still launches, it just gets no durable context to misattribute. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
…suite runnable The reported defect was that `up --once` does not pre-trust. It does — the batch at up.ts is unconditional and nativeLaunch pretrusts per-agent before spawn. The real defect is narrower and worse: the batch seeded only from RUNNING sessions, i.e. exactly the agents that had already launched and did not need it. A first-ever bring-up of N freshly-declared agents contributed zero dirs and raced the trust lost-update exactly as if the batch weren't there. It now also seeds from the catalog — desired state names what is about to be spawned. doctor's readiness suite ran init -> add -> then polled for a live session, but `convoy add` is declare-only and launches nothing, so all five checks failed by construction on every machine. Inserts the missing `up --once` reconcile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
…vergence Establishes context/ as convoy's VRS per the meta-spec: vision, requirements, spec, ontology, intuition, open questions, roadmap, seven decision records, and four deltas. The ontology earns its place immediately — "spec" named three different objects in existing prose (the catalog file, the in-memory compiled form, and pty's manifest), which is most of why the layering was hard to talk about. Decisions record the tradeoff and the rejected alternative, not just the choice. Deltas record what the contract asks for and the implementation does not do: no recovery after provider death, post-rename stale sends, specs convoy reads but cannot rewrite, and declared pty/render blocks that are parsed but inert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
…ransports, doctor Each node owns its own requirements and spec and references upstream constraints rather than restating them: 01-network (layout, roots, isolation, and why the network path consumes an agent's name budget), 02-agent-spec (format, discovery, identity, lifecycle), 03-supervision (reconcile, adoption, recovery, flapping cap), 04-transports (ding vs mcp and why the derivation cannot be bypassed), 05-doctor (the readiness contract, including that a check must exercise the real lifecycle rather than assert a state its own steps cannot produce). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
The published examples are not structurally identical to one another: the KDL nests every field under an `agent "<identity>"` node while the JSON and TOML are flat, and the JSON names the task table `ptys` where the others say `pty`. So a decoder built from any one example cannot read the other two. Both spellings are accepted rather than one being declared correct — convoy is an implementation, not the spec's editor, and picking a winner unilaterally would invalidate catalogs written against the other published example. Tests parse the spec's KDL example verbatim, raw-string command included. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
The published field table names this role `root`; convoy's internal name is chief-of-staff and is threaded through personas, permission tiers, and the crash-ding tier tag. Accepted as an alias rather than renamed underneath all of that — without it, a spec written to the published table is rejected outright. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
The spec's TOML render blocks name the destination with `dest`; its KDL blocks give it as a positional argument. Read either. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
… records networkLayout() does not own the catalog child; catalogDir() supplies it. Rationale prose in the subsystem specs is replaced by references to the decision records that own it, per the VRS smell rule. Records the open question of which artifact is authoritative as the recovery manifest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
…ually do The guard was documented as preventing a counter-named agent from having durable context. It does not, and cannot: `st context write` creates `context/` itself, unconditionally, exactly as the bus's send path creates an inbox. Verified directly — writing context as a counter-named identity produces context/now.md whether or not convoy seeded the directory. What convoy delivers is narrower and still worth having: it does not hand a counter-named agent a pre-made place to keep memory, which removes the case where a renumbered successor finds one already waiting. Preventing the agent from creating its own needs enforcement where the directory is created, which is the bus. Recorded as DELTA-005; the refusal message, decision 0006, the intuition, and SPEC-R16 now all say the weaker true thing instead of the stronger false one. Also stops a role-named grouping directory (catalog/workers/...) from landing in the host slot and producing a spurious path-vs-content warning. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
schickling-assistant
force-pushed
the
schickling-assistant/2026-07-20-noble-turing-59
branch
from
July 20, 2026 23:07
f36bea6 to
e5533c5
Compare
schickling-assistant
added a commit
that referenced
this pull request
Jul 20, 2026
…omment Two ways this PR's own fixes could be reverted with the suite green. 1. THE WIRING WAS UNTESTED. The `emitWrites`/`initExitCode` tests cover the pure DECISION; nothing covered the code that acts on it. Restoring the doubled-log bug verbatim — re-inlining `process.stdout.write(... human)` in `up()` — left the suite BYTE-IDENTICAL (294 passed / 4 failed) and `tsc` clean. `convoy init`'s tail had the same shape. `up()`'s emit closure is extracted to an exported `makeEmit(json, out, err)` with injectable sinks, so a test drives the REAL emitter and counts what each stream received. The last hop — that `up()` uses it and never writes to stdout itself — plus `cmdInit`'s (TTY-gated, so unreachable at runtime) rc propagation are held as source guards. 2. THE DOCTOR GUARD WAS A SUBSTRING MATCH. Replacing the reconcile with `// TODO: runConvoy(box, ["up", box.net, "--once"]) -- disabled` left 8/8 guard tests passing with the bug fully reopened; a comment above `pollUntil(` likewise satisfied the ordering check. Its hardcoded check list could only guard the checks someone remembered to list — `checkFullOrg` was already a sixth declaring check missing from it — and the PR's own instruction to delete the exception list when #89 lands BROKE the `expect(covered).toEqual(["checkDevTask"])` snapshot it shipped with. New `src/source-guard.ts` gives source guards a real tokenizer: `stripComments` (a guard can only be satisfied by code), brace-matched `functionBody`, and `exportedAsyncFunctions`. It handles the shapes that silently break a naive scanner and make every downstream guard vacuous — `//` inside a string, and a regex literal containing quotes (up.ts's `/ST_AGENT\s*=\s*"([^"]+)"/`). The doctor guard now DERIVES its declaring checks from suite.ts instead of hardcoding them, accepts either legitimate reconcile form (`up --once` or a background `convoy up` — how checkFullOrg reconciles), asserts the ordering non-vacuously (absence is -1, which sailed past `toBeLessThan`), and states the exception list as a shrink-only property so DELETING it when #89 lands widens coverage instead of reddening the suite. Evidence, each revert applied and restored: • re-inline the buggy emit in up() → 2 red (was: 0) • makeEmit writes human to both streams → 2 red (was: n/a) • cmdInit falls through to `return 0` → 1 red (was: 0) • reconcile replaced by a comment → 1 red (was: 0, 8/8 green) • a new declaring check, no reconcile → 1 red (was: invisible) • delete the exception list (#89 lands) → only the 4 genuine #89 gaps, no bookkeeping breakage (was: +1) 294 → 329 passing; the same 4 pre-existing failures, unchanged. tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
schickling-assistant
marked this pull request as ready for review
July 20, 2026 23:47
schickling-assistant
deleted the
schickling-assistant/2026-07-20-noble-turing-59
branch
July 21, 2026 00:21
schickling-assistant
added a commit
that referenced
this pull request
Jul 21, 2026
…ap; checkDevTask never launches its tiers (#90) * fix(up): print each supervisor log line once, not twice `convoy up`'s emit() wrote the human line to stderr AND, in the non-json branch, the same human line to stdout. Both streams land on the same terminal for a plain `convoy up`, so EVERY supervisor line printed twice — the doubled reconcile log. stdout carrying a copy of the human text was never useful: a caller that wants to parse the stream passes --json (stdout = JSONL), and a caller that wants to read it already has it on stderr. So non-json now leaves stdout empty; --json is unchanged. Reproduced live before the fix (`convoy up <net> --once` on a throwaway network printed all four lines twice) and after (once each; --json stdout still a clean JSONL stream). The stream routing is extracted to a pure `emitWrites` so the contract is unit-testable in convoy's style (cf. workerCrashed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty * fix(init): exit nonzero when the root agent fails to bootstrap `convoy init` printed "CoS bootstrap did not complete" on a nonzero `cmdCos` rc and then fell through to an unconditional `return 0`. So a scripted `convoy init && convoy up` saw green and moved on to a network whose catalog is empty — the root agent it just asked for never came up. A failed bootstrap must not report success. init now propagates cmdCos's rc, and on failure replaces the cheerful "✓ Network … is ready / next: run convoy doctor" tail with an honest line: the structure exists, it has no agents, here is how to retry. Declining the CoS prompt (and every non-interactive run, where the branch never executes) still exits 0 — the structure IS the deliverable there. The CoS branch is TTY-gated (`interactive && askYesNo(...)`), so this is reproduced by TEST, not by a live run: the decision is extracted to a pure `initExitCode` and asserted red-then-green (with the old unconditional `return 0` restored, the two new acceptance tests fail). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty * fix(doctor): reconcile the declared tiers in checkDevTask before polling them `convoy add` is declare-only — it writes a catalog entry and launches nothing — so checkDevTask's add-loop declares cos/supervisor/worker and then polls all three for `available` against sessions that were never spawned. It fails by construction on every machine, and runReadinessSuite returns 0 only if EVERY check passes, so this alone reds a newcomer's first `convoy doctor`. One `up --once` pass launches all three declared tiers. SCOPE: this is the FIFTH of five declare-then-assert checks missing the reconcile. PR #89 already inserts the same `up --once` into the other four (checkTmpNetwork, checkDings, checkStateExternalization, checkExactlyOnce) and owns those hunks, so this commit deliberately does NOT duplicate them — it closes the one #89 missed, in a region #89 does not touch. The suite is only fully green once both land. Adds a source-level guard for the declare→assert seam: the checks spawn real agents (minutes, real auth) so they can't run in a unit test, but the invariant "a check that declares agents reconciles them, BEFORE it asserts liveness" is structural and checkable. The guard carries a shrink-only exception list naming the four checks whose reconcile lands in #89; whoever merges #89 deletes the list and the guard covers all five. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty * test: cover the WIRING, and make the doctor guard undefeatable by a comment Two ways this PR's own fixes could be reverted with the suite green. 1. THE WIRING WAS UNTESTED. The `emitWrites`/`initExitCode` tests cover the pure DECISION; nothing covered the code that acts on it. Restoring the doubled-log bug verbatim — re-inlining `process.stdout.write(... human)` in `up()` — left the suite BYTE-IDENTICAL (294 passed / 4 failed) and `tsc` clean. `convoy init`'s tail had the same shape. `up()`'s emit closure is extracted to an exported `makeEmit(json, out, err)` with injectable sinks, so a test drives the REAL emitter and counts what each stream received. The last hop — that `up()` uses it and never writes to stdout itself — plus `cmdInit`'s (TTY-gated, so unreachable at runtime) rc propagation are held as source guards. 2. THE DOCTOR GUARD WAS A SUBSTRING MATCH. Replacing the reconcile with `// TODO: runConvoy(box, ["up", box.net, "--once"]) -- disabled` left 8/8 guard tests passing with the bug fully reopened; a comment above `pollUntil(` likewise satisfied the ordering check. Its hardcoded check list could only guard the checks someone remembered to list — `checkFullOrg` was already a sixth declaring check missing from it — and the PR's own instruction to delete the exception list when #89 lands BROKE the `expect(covered).toEqual(["checkDevTask"])` snapshot it shipped with. New `src/source-guard.ts` gives source guards a real tokenizer: `stripComments` (a guard can only be satisfied by code), brace-matched `functionBody`, and `exportedAsyncFunctions`. It handles the shapes that silently break a naive scanner and make every downstream guard vacuous — `//` inside a string, and a regex literal containing quotes (up.ts's `/ST_AGENT\s*=\s*"([^"]+)"/`). The doctor guard now DERIVES its declaring checks from suite.ts instead of hardcoding them, accepts either legitimate reconcile form (`up --once` or a background `convoy up` — how checkFullOrg reconciles), asserts the ordering non-vacuously (absence is -1, which sailed past `toBeLessThan`), and states the exception list as a shrink-only property so DELETING it when #89 lands widens coverage instead of reddening the suite. Evidence, each revert applied and restored: • re-inline the buggy emit in up() → 2 red (was: 0) • makeEmit writes human to both streams → 2 red (was: n/a) • cmdInit falls through to `return 0` → 1 red (was: 0) • reconcile replaced by a comment → 1 red (was: 0, 8/8 green) • a new declaring check, no reconcile → 1 red (was: invisible) • delete the exception list (#89 lands) → only the 4 genuine #89 gaps, no bookkeeping breakage (was: +1) 294 → 329 passing; the same 4 pre-existing failures, unchanged. tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ePNMmLYa7qVT3h7bRCWUJ agent-session-id: 0abcedc7-6b71-4046-9e7c-f645268c0b15 agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the agent spec as convoy's declaration contract, and adds convoy's own VRS under
context/. Tracking issue: #88.Draft on purpose — items 1–6 of #88 are here and tested; item 7 (
convoy run) is not, and four known divergences are recorded rather than fixed. Details at the bottom.Why this shape
The spec's payoff is continuity, and continuity has one prerequisite: a name that outlives the session. Everything here follows from taking that seriously.
If identity is declared and durable, then durable state can hang off it — and a session becomes disposable, because a fresh one converges on the same goal by reading what the last one wrote. That makes restart recovery a testable property instead of an aspiration. But it also puts a real cost on naming: the name is what peers address and what memory is keyed by. A cost on naming is only acceptable if a wrong choice is cheaply correctable, which is why
renameis in the spine rather than in a follow-up, and why a name that convoy accepts must be one every component downstream can actually use.What landed
Catalog in the spec format (#88 item 1). KDL, TOML, and JSON, discovered by slurping
catalog/**/*.{kdl,toml,json}.The identical-semantics claim is made structural rather than promised: all three formats decode to one canonical object, and exactly one function applies field rules. A format is supported by definition once its decoder is right. The equivalence is tested by decoding the same document three ways and asserting the results are equal — not by three parser suites that can each be individually right and collectively inconsistent.
KDL needed a node→table mapping the spec never states. The load-bearing rule: a node with one unnamed argument and children names a sub-table, so
pty "agent" { … }means[pty.agent]. Without it the spec's central construct has no KDL spelling.The spec's three examples also aren't structurally identical to each other, which I only found by trying to parse all of them: its KDL nests every field under
agent "<identity>" { … }while its JSON and TOML are flat; its JSON saysptyswhere the others saypty; its KDLrendergivesdestpositionally where TOML usesdest =. My first implementation followed the TOML/JSON shape and could not parse the spec's own KDL example. Convoy now accepts both spellings rather than picking a winner unilaterally — that would invalidate catalogs written against the other published example — and tests parse the published KDL verbatim, raw-string command included. Reconciling it is the lead item of the spec PR.Identity comes from content; path segments supply defaults; a path↔content disagreement warns and content wins. Erroring would strand an agent over a directory name; silently preferring the path would make one spec mean different things in different folders.
Declare-time identity validation (item 2). The grammar is now imported from smalltalk (
isAgent), not restated. The two had drifted:worker_fodfixpassed convoy and was rejected by the bus, as did anything ending in-,., or_. A second regex would just be a second chance to drift.convoy addpreviously checked identity for truthiness only and never parsed, so a bad name was written into the synced catalog and propagated to every peer before anything rejected it — the failure surfaced on a different machine than the mistake, at an unrelated time. It now validates with real context: bus grammar, this network's socket budget, and the identities already declared.Length is derived, not chosen: pty binds
<PTY_ROOT>/<prefix>.<identity>.ding.sockagainst a 104-bytesun_pathlimit, so the budget is104 − len(ptyRoot) − len(prefix) − len(".ding.sock") − 2. A network on a long path genuinely affords shorter names; the error says so rather than quoting a constant. The longest derived form is used, because a bound admitting a name the ding sidecar can't bind admits a half-working agent.convoy rename(item 3), which moves the catalog entry and the whole bus folder.Moving the folder wholesale is what makes in-flight mail survive — messages in
inbox/at rename time travel with it and need no special handling, precisely because it's a move and not a re-creation.The tombstone is scoped honestly. The intent was that stale references resolve. Investigating smalltalk shows convoy cannot deliver that: smalltalk has no alias/redirect concept anywhere, and its send path validates the name then unconditionally
mkdirs the inbox. So a peer holding the old name that sends after a rename doesn't fail and doesn't redirect — it manufactures a folder nobody reads. The tombstone is therefore documented as read by convoy (uniqueness, listings, rename idempotency), not by the bus, and the residual gap isDELTA-002rather than a claimed guarantee.Two structural details fall out of smalltalk's behavior, not taste: the tombstone is a bare dotfile with no
inbox//archive//statusbeside it, becausest agentslists a folder when any of those exist and a tombstone carrying them would resurrect the renamed-away agent in every listing; and the folder moved is the host-prefixed<host>.<identity>, since moving the bare name would move nothing and report success. Ordering is bus-first so an interrupted rename completes on re-run.bin(item 4). Replaces the bare harness name while every derived flag survives, so a deployment that wraps its harness isn't bypassed — a convoy-managed session was running outside the boundary every other session in that deployment runs inside. Also sidesteps the closedHarnessunion. Charset-validated (it lands unquoted insh -c); arguments deliberately not permitted.Counter-discriminator guard (item 6). Refuses to seed
context/under<role>-<n>. The counter re-derives per parent lifetime, so after a restartworker-2names a different agent and would read its predecessor'snow.mdas its own memory — silent, delayed, and indistinguishable from having done the work. Refused rather than warned, because a warning is read when the name is chosen and not when the wrong file is opened weeks later.fabric-2is unaffected.Scoped honestly, after checking: this is a strong default, not an invariant.
st context writemkdir-p'scontext/itself, so an agent that externalizes state makes its own regardless — verified directly. What convoy removes is the case where a renumbered successor finds a directory already waiting. Real enforcement belongs where the directory is created, which is the bus (DELTA-005).Credentials in
env, noaccountfield. Selecting an account is selecting a config dir; a second field is a second thing to disagree.CLAUDE_CONFIG_DIRis read back out ofenvso the declaration and the derived manifest can't diverge. Specenvmerges under derived wiring so a declared key can't repoint an agent at another bus.Two premises I'd correct
up --oncealready pre-trusts (item 5). The batch is unconditional andnativeLaunchpre-trusts per-agent before spawn, so the reported one-shot hang doesn't reproduce as described. There is a real defect underneath: the batch seeded only from running sessions — exactly the agents that had already launched and didn't need it. A first-ever bring-up of N freshly-declared agents contributed zero dirs and raced the trust lost-update as if the batch weren't there. It now also seeds from the catalog, which is what names the workspaces about to be spawned.convoy doctor's diagnosis was right, and the structural defect is fixed. It raninit→add→ then polled for a live session, butaddis declare-only. All five checks failed by construction on every machine. The missingup --onceis inserted, making declare→reconcile→assert coherent. This depended on the item-5 finding: inserting it only helps becauseup --onceis genuinely headless. Caveat: the suite needs live signed-in harness auth to run, so I've confirmed the structural defect is gone, not that the five checks now pass end-to-end.VRS
context/per the meta-spec: vision, requirements, spec, ontology, intuition, open questions, roadmap, five subsystem nodes (01-network,02-agent-spec,03-supervision,04-transports,05-doctor), seven decision records, five deltas. Consumer-agnostic — it says what convoy IS and guarantees, not how any deployment composes it.The ontology earned its place immediately: "spec" named three different objects in existing prose — the catalog file (portable, authored), the in-memory compiled form (machine-specific), and pty's manifest (a build artifact). That conflation is most of why the layering was hard to talk about. They're now agent spec / launch spec / session manifest.
Decision records carry the tradeoff and the rejected alternative, not just the choice.
Not implemented — deliberately
convoy run(item 7). The biggest greenfield piece, and it's not obviously additive: an ad-hoc session has no declaration, so it can't be reconciled, recovered, or adopted — it sits outside every property this VRS promises. Whether that's an acceptable escape hatch or whether it should be an ephemeral declaration is captured inopen-questions.md; that fork should be settled before code.DELTA-001. Detection (the hard half) works; the response is missing.DELTA-002, above. Needs redirect resolution in smalltalk, which owns the send path. A convoy-side narrowing (forward resurrected tombstone folders during reconcile) is inroadmap.md.DELTA-003. A KDL/JSON spec can be discovered and run but not retired through convoy;renamerefuses explicitly,removecurrently reports it as absent, which is a misleading answer about a spec it found.DELTA-005, above. Needs bus-side enforcement.ptyandrenderblocks are parsed but inert —DELTA-004. They round-trip so specs authored against the published format don't lose fields, but declaring a task that never runs with no error is the same silent-divergence failure mode this work exists to prevent. At minimum this wants a discovery warning.catalog/<identity>.tomlrather than the spec's recommended nested layout. It reads both; the nested write path touches retire/render and wasn't worth bundling.$HOME/src(noteCONVOY_PERSONAS_DIRalready exists), doubled supervisor log lines, andconvoy initexiting 0 after failing to create the root agent.Testing
npx vitest run— 73 new tests, all green. Four failures on this branch are pre-existing onmainand environment-dependent (two bash-3.2 hook-parsing assertions, onegit statuscleanliness check, one megarepo worktree check); they fail identically on a cleanmaincheckout here.New suites:
spec-format.test.ts(three-format equivalence + the KDL mapping),identity.test.ts(bus agreement, the socket-derived boundary, counter recognition),catalog.test.ts(tree discovery, path defaults, duplicates),rename.test.ts(durable move, in-flight mail, tombstone invisibility, re-runnability),agent-spec-file.test.ts(bin,env,pty/renderparsing).Verified end-to-end against a real network:
addrefusesworker_fodfixwith rc=2 and names the fix; KDL and JSON specs in the nested layout are discovered withhostdefaulted from the path;rename --dry-runreports the move and the tombstone caveat.Adds one runtime dependency,
@bgotink/kdl— chosen for having no transitive dependencies (the alternative pulls a parser generator) in a package that otherwise has one.Spec feedback
Implementing surfaced things worth changing upstream; happy to open a PR on
agent-specif these look right:Opened as compoundingtech/agent-spec#1 (draft). Each item is something the implementation couldn't do, or could only do by guessing:
[pty.<name>]has no specified KDL spelling, so implementations will differ and each still claim conformance. Adds the mapping table.identity+roleas the recognition rule is a footgun. A spec that omits or misspells one is silently not an agent — declared-looking, never running, no error. Changed to: either field is a claim, incomplete is a reported error.envexists only at the pty level, but credential selection is agent-level and must reach every derived task; with ptys derived there's nowhere to putCLAUDE_CONFIG_DIR.binfield — the wrapped-harness case, and the only escape from the closedHarnessunion.worker_fodfixdrift as evidence), the socket-derived length formula, and the counter-name hazard.$CONVOY_NETvsCONVOY_NETWORK— flagged, not changed; as written the examples aren't copy-pasteable.One convoy-side bug this surfaced:
role = "root"— the name the spec's own field table uses — was rejected outright by convoy's role parser. Now accepted as an alias forchief-of-staff; whether the two names should coexist is left to the spec.🤖 Generated with Claude Code