Skip to content
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 into
mainfrom
schickling-assistant/2026-07-20-noble-turing-59
Jul 21, 2026
Merged

schickling-assistant merged 12 commits into
mainfrom
schickling-assistant/2026-07-20-noble-turing-59

Conversation

@schickling-assistant

@schickling-assistant schickling-assistant commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

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 rename is 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 says ptys where the others say pty; its KDL render gives dest positionally where TOML uses dest =. 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_fodfix passed 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 add previously 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.sock against a 104-byte sun_path limit, so the budget is 104 − 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 is DELTA-002 rather 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//status beside it, because st agents lists 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 closed Harness union. Charset-validated (it lands unquoted in sh -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 restart worker-2 names a different agent and would read its predecessor's now.md as 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-2 is unaffected.

Scoped honestly, after checking: this is a strong default, not an invariant. st context write mkdir-p's context/ 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, no account field. Selecting an account is selecting a config dir; a second field is a second thing to disagree. CLAUDE_CONFIG_DIR is read back out of env so the declaration and the derived manifest can't diverge. Spec env merges under derived wiring so a declared key can't repoint an agent at another bus.

Two premises I'd correct

up --once already pre-trusts (item 5). The batch is unconditional and nativeLaunch pre-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 ran initadd → then polled for a live session, but add is declare-only. All five checks failed by construction on every machine. The missing up --once is inserted, making declare→reconcile→assert coherent. This depended on the item-5 finding: inserting it only helps because up --once is 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 in open-questions.md; that fork should be settled before code.
  • Supervisor never replays the manifest after provider death (Supervisor detects provider death but never replays the manifest — no recovery #82) — DELTA-001. Detection (the hard half) works; the response is missing.
  • Post-rename stale sendsDELTA-002, above. Needs redirect resolution in smalltalk, which owns the send path. A convoy-side narrowing (forward resurrected tombstone folders during reconcile) is in roadmap.md.
  • Reads three formats, writes oneDELTA-003. A KDL/JSON spec can be discovered and run but not retired through convoy; rename refuses explicitly, remove currently reports it as absent, which is a misleading answer about a spec it found.
  • The counter-context guard is a default, not an invariantDELTA-005, above. Needs bus-side enforcement.
  • pty and render blocks are parsed but inertDELTA-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.
  • Convoy still writes flat catalog/<identity>.toml rather than the spec's recommended nested layout. It reads both; the nested write path touches retire/render and wasn't worth bundling.
  • Cross-machine rename. The catalog syncs; the bus folder move is local to the machine running the command.
  • Not addressed from Implement the agent spec as convoy's declaration contract #88's defect list: personas cloning into $HOME/src (note CONVOY_PERSONAS_DIR already exists), doubled supervisor log lines, and convoy init exiting 0 after failing to create the root agent.

Testing

npx vitest run73 new tests, all green. Four failures on this branch are pre-existing on main and environment-dependent (two bash-3.2 hook-parsing assertions, one git status cleanliness check, one megarepo worktree check); they fail identically on a clean main checkout 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/render parsing).

Verified end-to-end against a real network: add refuses worker_fodfix with rc=2 and names the fix; KDL and JSON specs in the nested layout are discovered with host defaulted from the path; rename --dry-run reports 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-spec if 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:

  1. The three examples aren't structurally identical (above). Proposes the flat shape as normative; explicitly happy to go the other way, the value is in picking one.
  2. The KDL mapping is unstated. [pty.<name>] has no specified KDL spelling, so implementations will differ and each still claim conformance. Adds the mapping table.
  3. identity + role as 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.
  4. env exists only at the pty level, but credential selection is agent-level and must reach every derived task; with ptys derived there's nowhere to put CLAUDE_CONFIG_DIR.
  5. No bin field — the wrapped-harness case, and the only escape from the closed Harness union.
  6. Identity constraints unstated — the bus grammar (with the "import, don't restate" recommendation and the worker_fodfix drift as evidence), the socket-derived length formula, and the counter-name hazard.
  7. $CONVOY_NET vs CONVOY_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 for chief-of-staff; whether the two names should coexist is left to the spec.

🤖 Generated with Claude Code

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
schickling-assistant and others added 12 commits July 21, 2026 01:04
…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
schickling-assistant force-pushed the schickling-assistant/2026-07-20-noble-turing-59 branch from f36bea6 to e5533c5 Compare July 20, 2026 23:07
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
schickling-assistant marked this pull request as ready for review July 20, 2026 23:47
@schickling-assistant
schickling-assistant merged commit 68fd8e7 into main Jul 21, 2026
@schickling-assistant
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>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant