Skip to content
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 into
mainfrom
schickling-assistant/2026-07-21-harness-widen
Jul 21, 2026
Merged

a harness is a record of capabilities, not an enum member#97
schickling-assistant merged 2 commits into
mainfrom
schickling-assistant/2026-07-21-harness-widen

Conversation

@schickling-assistant

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

Copy link
Copy Markdown
Contributor

Unblocks retiring the ordinal launcher families (cl1/cl2, co1co4, 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, and env is spread verbatim into the derived harness session. A catalog spec with [env] CODEX_HOME = … reaches a codex agent today. There is a stale comment in agentFileToSpec claiming "env is not yet materialized" that says otherwise; it is wrong, and it is fixed here. The #1326 status line ("co1co4 need CODEX_HOME in 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 of env that the imperative CLI and pre-trust both read — was hardcoded to CLAUDE_CONFIG_DIR.

Six defects, each reproduced on a clean origin/main checkout first

# Defect on main Why it is bad
1 --config-dir on a codex session sets CLAUDE_CONFIG_DIR codex does not read it. The flag reports success and selects nothing — the agent runs as whatever account was ambient.
2 A codex spec's CODEX_HOME is not lifted to configDir The env var reaches the process, but convoy does not know the config moved, so pre-trust targets the wrong file.
3 codexConfigPath() ignores a relocated CODEX_HOME Trust is seeded in ~/.codex/config.toml while the agent reads $CODEX_HOME/config.toml. codex's --dangerously-bypass-approvals-and-sandbox does not skip the directory-trust prompt, so this stalls an unattended agent on a dialog rather than erroring.
4 opencode / pi cannot be declared at all The union was claude | codex.
5 harnessCommand("opencode", …) silently emits claude's flags See below — this is the one that decided the design.
6 An unlisted harness renders [sessions.undefined] Adding a member was silent: exactly one site was typed tightly enough to fail the build.
7 Four string-literal sites are claude/codex-only usedHarnesses() sniffs [sessions.claude]/[sessions.codex]; convoy remove matches ["claude","codex","ding"]; agentShort() and the teardown normalizer strip /-(claude|codex)$/i. An opencode agent would be misreported by ls and missable by remove/down — launchable but not reliably operable. No type coupling, so neither tsc nor the launch tests catch these.

Why bin could not close gap 2 (I initially thought it could)

Decision 0005 rejected widening the union and offered an escape hatch: set bin to the wrapper, keep harness as "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:

  • opencode's positional argument is a project path, not a prompt. 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.
  • Neither opencode nor pi accepts --permission-mode or --dangerously-bypass-approvals-and-sandbox.
  • --model is not portable: opencode wants provider/model, pi wants a pattern plus --provider.

So "nearest compatible flavor" has no referent for these two. 0005 is narrowed, not reversed — bin still replaces only the binary and every flag is still derived.

The change

One HarnessDescriptor table (src/harness.ts): session key, config-relocation env var (null = none), MCP/doctor/auth support, and argv derivation. Every consumer reads it.

  • configDir now means "the harness's own config dir" — CLAUDE_CONFIG_DIR for claude, CODEX_HOME for codex, refused where configEnv is null.
  • doctor and auth kept 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 are Partial — because they are partial in fact — so an unsupported harness is absent rather than defaulted into another harness's entry.
  • The four string-literal sites above now derive from the table (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.
  • --bin is exposed on convoy 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-dir applies 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:

  • A correct launch command with flags verified against opencode 1.18.3 --help and pi 0.80.10 --help
  • Their own pty session section, bus wiring (ST_AGENT/ST_ROOT/PTY_ROOT), ding sidecar, persona, context dir, crash-ding tier
  • Full operability: correctly reported by convoy ls, and targetable by identity for convoy remove / convoy down (defect 7)
  • bin, model, prefix, workspace, retired, and verbatim env

They do not get:

  • No account selection. Neither harness has a config-relocation variable, so convoy cannot select an account for it. --config-dir is refused rather than injected under a guessed name.
  • No convoy doctor checkup — no version floor, no distill probe. A green convoy doctor does not cover them. This is a real reduction in what a clean doctor run means.
  • No auth probe. convoy doctor cannot tell you whether they are logged in.
  • No MCP transport — coerced to the ding sidecar.
  • No pre-trust. convoy has no verified trust-store shape for them; convoy pretrust refuses rather than writing the claude file and reporting a success the agent never reads.
  • No permission posture. permissionMode does not reach them. opencode's --auto is close to convoy's bypass posture; pi's --approve is 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.
  • co1co4yes. 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/pideclarable and launchable, with the caveats above. The intended shape is --bin pointed at the existing oc/pi wrapper (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 marked LOCK pass 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 clean origin/main worktree before being fixed — including agentShort("alpha-opencode") === "alpha-opencode" on main.

488 passed (up from 466). The 4 failures on this branch are pre-existing on origin/main and untouched here: 2 in doctor/hooks.test.ts, and 2 git-worktree tests in launch.test.ts / network-config.test.ts that 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", so CODEX_HOME = "$HOME/.codex-fabric" likely lands as a literal $HOME/… string. The real co1co4 wrappers use absolute paths and so dodge this. Flagged rather than fixed because the fix would change CLAUDE_CONFIG_DIR behavior too and deserves its own change. Worth confirming before anyone writes a $HOME-relative spec.

Reviewer attention

  • The opencode/pi flag choices are the least-verified part. They are derived from --help, not from running an agent to completion under convoy. --auto and --approve are my judgment calls about posture equivalence, and --approve is explicitly not equivalent.
  • Convoy now encodes flag opinions for harnesses it cannot test in CI — the exact cost 0005 named. It is bounded to one reviewable table, and the alternative was encoding a wrong opinion (the claude branch) silently.
  • Decision 0008 records all of this.

🤖 Generated with Claude Code

schickling-assistant and others added 2 commits July 21, 2026 10:15
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
schickling-assistant marked this pull request as ready for review July 21, 2026 08:54
@schickling-assistant
schickling-assistant merged commit 71469fe into main Jul 21, 2026
1 check passed
@schickling-assistant
schickling-assistant deleted the schickling-assistant/2026-07-21-harness-widen branch July 21, 2026 08:55
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