This repository was archived by the owner on Jul 24, 2026. It is now read-only.
a harness is a record of capabilities, not an enum member - #97
Merged
schickling-assistant merged 2 commits intoJul 21, 2026
Merged
Conversation
Two constraints blocked retiring the ordinal launcher aliases. They turned out to be one shape problem: `harness` was an enum whose every consumer branched `if (codex) … else <the claude form>`, so an unlisted harness was silently claude. Codex account selection was mostly already there — decision 0004 put credentials in the spec's `env`, and `env` is spread verbatim into the derived session, so `CODEX_HOME` already reached a codex agent. What was missing is that convoy did not KNOW the config moved: `configDir`, the projection the CLI and pre-trust read, was hardcoded to CLAUDE_CONFIG_DIR. So `--config-dir` on codex set a variable codex does not read (selecting nothing, silently), and pre-trust seeded the ambient config while the agent read the relocated one — and codex's bypass flag does not skip the trust prompt, so that stalls rather than fails. opencode and pi could not be declared. 0005 offered `bin` + "the nearest CLI-compatible flavor" for that, which does not survive contact with the real binaries: opencode's positional is a PROJECT PATH, so the boot prompt would be consumed as a directory name — a wrapper can swallow a flag but cannot rescue that. Neither accepts --permission-mode or --dangerously-bypass-approvals-and- sandbox; --model is not portable either. So: one HarnessDescriptor table (src/harness.ts) carrying session key, config env var, MCP/doctor/auth support, and argv derivation. doctor and auth import the canonical type instead of shadowing it, and their records are Partial because they are partial in fact. Union widened to claude|codex|opencode|pi. `bin` also exposed on `convoy run`, which replaces aliases that were themselves wrappers. opencode and pi are partial citizens BY DECLARATION: they launch with verified flags, and get no account selection, no doctor checkup, no auth probe, and no MCP. harnessLimitations() derives that list from the table so it cannot drift. Flag surfaces read from `opencode 1.18.3 --help` and `pi 0.80.10 --help`. Each of the six defects was reproduced against a clean origin/main checkout first; tests marked LOCK pass on main and pin behavior that was reported missing but was already present. 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
… launch it
The first pass wired every TYPE-coupled harness site but left four plain string
literals alone — and those are exactly the ones neither tsc nor the launch tests
can catch:
usedHarnesses() sniffed "[sessions.claude]"/"[sessions.codex]", so an
opencode-only network matched neither and fell back to reporting claude.
`convoy remove` matched sessions against a literal ["claude","codex","ding"].
agentShort() and the teardown normalizer stripped /-(claude|codex)$/i, so
`alpha-opencode` kept its suffix and the derived session id disagreed with
identity matching.
Net effect: an opencode/pi agent would have been launchable but not reliably
operable — misreported by `ls`, missable by `remove`/`down`. That is precisely
the partial-citizen-presented-as-full failure this change is meant to avoid, so
it is fixed rather than documented.
All four now derive from the harness table (HARNESS_SESSION_KEYS,
HARNESS_SUFFIX_RE, harnessesInPtyToml). Verified on a clean origin/main
checkout that agentShort("alpha-opencode") returns "alpha-opencode" there.
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 21, 2026 08:54
schickling-assistant
deleted the
schickling-assistant/2026-07-21-harness-widen
branch
July 21, 2026 08:55
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.
Unblocks retiring the ordinal launcher families (
cl1/cl2,co1–co4,oc,pi) per schickling/dotfiles#1326. Draft — please read the "what this does NOT give you" section before the diff.The premise was half wrong, and that changed the fix
The brief said codex agents have no way to select an account. That is already solved for the declarative path and has been since decision 0004: credentials ride in the spec's
env, andenvis spread verbatim into the derived harness session. A catalog spec with[env] CODEX_HOME = …reaches a codex agent today. There is a stale comment inagentFileToSpecclaiming "envis not yet materialized" that says otherwise; it is wrong, and it is fixed here. The#1326status line ("co1–co4needCODEX_HOMEin the spec") is stale for the same reason.What was actually broken is that convoy did not know the config had moved.
configDir— the projection ofenvthat the imperative CLI and pre-trust both read — was hardcoded toCLAUDE_CONFIG_DIR.Six defects, each reproduced on a clean
origin/maincheckout first--config-diron a codex session setsCLAUDE_CONFIG_DIRCODEX_HOMEis not lifted toconfigDircodexConfigPath()ignores a relocatedCODEX_HOME~/.codex/config.tomlwhile the agent reads$CODEX_HOME/config.toml. codex's--dangerously-bypass-approvals-and-sandboxdoes not skip the directory-trust prompt, so this stalls an unattended agent on a dialog rather than erroring.opencode/picannot be declared at allclaude | codex.harnessCommand("opencode", …)silently emits claude's flags[sessions.undefined]usedHarnesses()sniffs[sessions.claude]/[sessions.codex];convoy removematches["claude","codex","ding"];agentShort()and the teardown normalizer strip/-(claude|codex)$/i. An opencode agent would be misreported bylsand missable byremove/down— launchable but not reliably operable. No type coupling, so neither tsc nor the launch tests catch these.Why
bincould not close gap 2 (I initially thought it could)Decision 0005 rejected widening the union and offered an escape hatch: set
binto the wrapper, keepharnessas "the nearest CLI-compatible flavor", accepting that "a wrapper must accept the harness's flags". For a wrapped claude or codex that holds, and it is what dev3's CoS uses today. It does not hold here, checked against the real binaries rather than docs:exec opencode '<boot prompt>'does not pass a prompt opencode ignores — it asks opencode to start in a directory named after the entire boot ritual. A wrapper script can swallow a flag; it cannot rescue a prompt already consumed as a path. The prompt must ride--prompt.--permission-modeor--dangerously-bypass-approvals-and-sandbox.--modelis not portable: opencode wantsprovider/model, pi wants a pattern plus--provider.So "nearest compatible flavor" has no referent for these two. 0005 is narrowed, not reversed —
binstill replaces only the binary and every flag is still derived.The change
One
HarnessDescriptortable (src/harness.ts): session key, config-relocation env var (null= none), MCP/doctor/auth support, and argv derivation. Every consumer reads it.configDirnow means "the harness's own config dir" —CLAUDE_CONFIG_DIRfor claude,CODEX_HOMEfor codex, refused whereconfigEnvisnull.doctorandauthkept their own shadow copies of the union, so widening would not even have failed to compile there. They now import the canonical type, and their per-harness records arePartial— because they are partial in fact — so an unsupported harness is absent rather than defaulted into another harness's entry.HARNESS_SESSION_KEYS,HARNESS_SUFFIX_RE,harnessesInPtyToml), so a new harness is recognised by the code that manages agents, not only the code that launches them.--binis exposed onconvoy run. That path replaces the launcher aliases, and those aliases were wrappers; without this, the ad-hoc path is the one place a convoy session escapes the deployment's boundary.convoy pretrust's codex refusal was justified as "--config-dirapplies only to claude". That was true of the implementation, not of codex. The refusal now falls where it is actually true: a harness with no config-relocation variable.What a non-claude/codex agent does and does NOT get
opencode and pi are partial citizens, by declaration. They get:
opencode 1.18.3 --helpandpi 0.80.10 --helpST_AGENT/ST_ROOT/PTY_ROOT), ding sidecar, persona, context dir, crash-ding tierconvoy ls, and targetable by identity forconvoy remove/convoy down(defect 7)bin,model,prefix,workspace,retired, and verbatimenvThey do not get:
--config-diris refused rather than injected under a guessed name.convoy doctorcheckup — no version floor, no distill probe. A greenconvoy doctordoes not cover them. This is a real reduction in what a clean doctor run means.convoy doctorcannot tell you whether they are logged in.convoy pretrustrefuses rather than writing the claude file and reporting a success the agent never reads.permissionModedoes not reach them. opencode's--autois close to convoy's bypass posture; pi's--approveis not — it trusts project-local files, not tool calls, so a pi agent may still stall on a tool prompt where a claude/codex agent under bypass would not.harnessLimitations()derives that list from the table, so the documentation of a limitation cannot drift from the limitation.Does this actually unblock the retirement?
cl1/cl2— already worked; unchanged.co1–co4— yes. Declaratively this already worked (defect 2/3 made it unreliable, not impossible — trust was seeded in the wrong file). Imperatively,convoy run --harness codex --config-dir …now actually selects the account instead of silently setting a variable codex ignores.oc/pi— declarable and launchable, with the caveats above. The intended shape is--binpointed at the existingoc/piwrapper (absolute store path), which is what supplies persona/policy/pty setup. Retiring them means accepting that those two families lose doctor and auth coverage. That is a deployment decision, not a code one, and it should be made explicitly.Tests
src/harness.test.ts, plain vitest per repo idiom. Tests markedLOCKpass on main — they pin behavior that was reported missing but was already present, and are not evidence of a fix. Every other test fails on main. The seven defects above were each reproduced against a cleanorigin/mainworktree before being fixed — includingagentShort("alpha-opencode") === "alpha-opencode"on main.488 passed(up from466). The 4 failures on this branch are pre-existing onorigin/mainand untouched here: 2 indoctor/hooks.test.ts, and 2 git-worktree tests inlaunch.test.ts/network-config.test.tsthat appear to fail when run from inside a worktree.Known gap, deliberately not fixed here
$HOME-relative env values are never expanded — not by convoy, and I found no expansion in pty either. The spec doc comment says values should be written$HOME-relative "so one spec is machine-agnostic", soCODEX_HOME = "$HOME/.codex-fabric"likely lands as a literal$HOME/…string. The realco1–co4wrappers use absolute paths and so dodge this. Flagged rather than fixed because the fix would changeCLAUDE_CONFIG_DIRbehavior too and deserves its own change. Worth confirming before anyone writes a$HOME-relative spec.Reviewer attention
--help, not from running an agent to completion under convoy.--autoand--approveare my judgment calls about posture equivalence, and--approveis explicitly not equivalent.🤖 Generated with Claude Code