CB-617: deliver a member's role contract as an agent definition file, not as argv — and keep the model in config #120

Open
opened 2026-08-22 11:55:49 +02:00 by ltms · 0 comments
Owner

Design settled with the operator on 2026-08-22. Fixes #119 (CB-616) at the root and replaces the
mechanism by which a member learns its role.

Measured facts this rests on

All six were run on this host on 2026-08-22, not taken from documentation.

Behaviour Claude Code OpenCode
Loads a file agent from the project dir yes — .claude/agents/*.md yes — .opencode/agent/*.md
Reads the other tool's directory no no
--agent <name> binds the whole session yes yes
The file body is used as the system prompt yes yes
model: in the file is honoured when no flag is passed yes yes
A launch flag outranks the file's model: yes, --model yes, -m

Evidence for the two that decide the design:

  • A .claude/agents/probe.md pinned to haiku with the body "Reply with exactly one word:
    FILEAGENT"
    , run as claude -p "hi" --agent probe, answered FILEAGENT.
  • The same shape as .opencode/agent/pin.md pinned to openai/gpt-5.6-terra, run with
    -m opencode/x-preview-f-free, printed > pin · x-preview-f-free. The flag won.
  • Reverse test on Claude Code: a valid model in the agent file plus a bogus --model failed with
    unrecognized_model. So the flag outranks the file rather than invalid values being dropped.

The decision

The role contract moves into agent definition files. The model stays in bridged.yaml.

.claude/agents/{architect,dev,reviewer}.md    the ROLE CONTRACT — what the job is,
.opencode/agent/{architect,dev,reviewer}.md   what to report, what never to do.
                                              NO `model:` key.

bridged.yaml profiles: + fleet pools          WHICH BACKEND and what it costs.
                                              Stays the operator's single file.

Why the model does not go in the agent file. bridged already passes --model plus
ANTHROPIC_MODEL (ClaudeCodeLauncher.java:312) and -m (OpenCodeLauncher.java:264) on every
spawn where the profile sets model:. Both outrank the file. Writing model: into an agent file
today means it is silently overridden on every spawn — no error, no warning. That is the
silent-default failure class this project keeps paying for (#113).

Why it also does not belong there on design grounds. record Slot(String profile) separates
role from backend on purpose, and the config comment says why: "a reviewer may run on the very
same profile as the dev whose diff it reviews"
. Welding a model into the role file collapses the
two axes the pool and placement system is built on. It would break the live setup where reviewers
run on sonnet while devs run on xf.

What stays where — the layering is unchanged, only the delivery moves

Layer Carries Delivery after this change
REPLY_CHARTER the one rule that must survive with no repo: end every turn with bridge_reply unchanged — still injected by the launcher, because a member with no repo checkout must still get it
role agent file the per-role contract new — a file in the member's own worktree checkout
fleet.charters.* operator override of a role contract kept for back-compat, but delivered via a file, never inline argv

Work

Unit A — launcher (Java)

  1. A configured fleet.charters.<role> must no longer travel as inline argv. Write it to a temp
    file and pass --append-system-prompt-file. Confirmed working on this host: a multi-line file
    passed that way produced the instructed output. This alone fixes #119.
  2. When an agent definition exists for the member's role, pass --agent <role> (Claude Code) and
    --agent <role> (OpenCode).
  3. REPLY_CHARTER keeps its current delivery. It is one line, it encodes fine, and it must reach a
    member that has no repo.
  4. Do not stop passing --model / -m. The profile stays authoritative for the model.

Unit B — the agent files and the docs

  1. Author .claude/agents/ and .opencode/agent/ definitions for architect, dev and
    reviewer. Same body text for both copies of a role, so the two backends get the same contract.
    Source the text from the existing fleet.charters.architect and from
    .claude/skills/{implementer,reviewer}/SKILL.md.
  2. No model: key in any of them. Add a comment saying why, pointing at this ticket.
  3. Update CLAUDE.md's layering table — the row describing where a role contract lives is now wrong.
  4. Add the wiki/11-Features.md entry: what it does, the knob, why it exists, the gotcha.

Acceptance

  • A sonnet architect spawns successfully with fleet.charters.architect configured. That spawn
    fails today (#119) and is the regression test for it.
  • A member's reported charterSha256 still identifies what it actually launched with. If the
    contract now comes from a file, the receipt must fingerprint that file, or CB-571's proof is lost.
  • Same contract text reaches both backends for the same role.
  • Tests drive the real launcher path. A test that calls the argv builder or the encoder directly
    walks around the gate this bug lives in — see #113.
  • An agent file that declares model: is reported at load rather than silently ignored. A warning
    naming the file is enough; refusing is better.

Open point, deliberately not decided here

.claude/agents/ committed to the repo also appears in the operator's own lead session as
selectable subagents. Harmless, but visible. If that turns out to be noisy, the files move under a
directory the lead's session does not scan, and the launcher points at them explicitly.

Design settled with the operator on 2026-08-22. Fixes #119 (CB-616) at the root and replaces the mechanism by which a member learns its role. ## Measured facts this rests on All six were run on this host on 2026-08-22, not taken from documentation. | Behaviour | Claude Code | OpenCode | |---|---|---| | Loads a file agent from the project dir | yes — `.claude/agents/*.md` | yes — `.opencode/agent/*.md` | | Reads the **other** tool's directory | **no** | **no** | | `--agent <name>` binds the whole session | yes | yes | | The file body is used as the system prompt | yes | yes | | `model:` in the file is honoured when no flag is passed | yes | yes | | **A launch flag outranks the file's `model:`** | yes, `--model` | yes, `-m` | Evidence for the two that decide the design: - A `.claude/agents/probe.md` pinned to haiku with the body *"Reply with exactly one word: FILEAGENT"*, run as `claude -p "hi" --agent probe`, answered `FILEAGENT`. - The same shape as `.opencode/agent/pin.md` pinned to `openai/gpt-5.6-terra`, run with `-m opencode/x-preview-f-free`, printed `> pin · x-preview-f-free`. The flag won. - Reverse test on Claude Code: a valid model in the agent file plus a bogus `--model` failed with `unrecognized_model`. So the flag outranks the file rather than invalid values being dropped. ## The decision **The role contract moves into agent definition files. The model stays in `bridged.yaml`.** ``` .claude/agents/{architect,dev,reviewer}.md the ROLE CONTRACT — what the job is, .opencode/agent/{architect,dev,reviewer}.md what to report, what never to do. NO `model:` key. bridged.yaml profiles: + fleet pools WHICH BACKEND and what it costs. Stays the operator's single file. ``` **Why the model does not go in the agent file.** bridged already passes `--model` plus `ANTHROPIC_MODEL` (`ClaudeCodeLauncher.java:312`) and `-m` (`OpenCodeLauncher.java:264`) on every spawn where the profile sets `model:`. Both outrank the file. Writing `model:` into an agent file today means it is **silently overridden on every spawn** — no error, no warning. That is the silent-default failure class this project keeps paying for (#113). **Why it also does not belong there on design grounds.** `record Slot(String profile)` separates role from backend on purpose, and the config comment says why: *"a reviewer may run on the very same profile as the dev whose diff it reviews"*. Welding a model into the role file collapses the two axes the pool and placement system is built on. It would break the live setup where reviewers run on `sonnet` while devs run on `xf`. ## What stays where — the layering is unchanged, only the delivery moves | Layer | Carries | Delivery after this change | |---|---|---| | `REPLY_CHARTER` | the one rule that must survive with no repo: end every turn with `bridge_reply` | **unchanged** — still injected by the launcher, because a member with no repo checkout must still get it | | role agent file | the per-role contract | new — a file in the member's own worktree checkout | | `fleet.charters.*` | operator override of a role contract | kept for back-compat, but delivered **via a file**, never inline argv | ## Work ### Unit A — launcher (Java) 1. A configured `fleet.charters.<role>` must no longer travel as inline argv. Write it to a temp file and pass `--append-system-prompt-file`. Confirmed working on this host: a multi-line file passed that way produced the instructed output. This alone fixes #119. 2. When an agent definition exists for the member's role, pass `--agent <role>` (Claude Code) and `--agent <role>` (OpenCode). 3. `REPLY_CHARTER` keeps its current delivery. It is one line, it encodes fine, and it must reach a member that has no repo. 4. Do **not** stop passing `--model` / `-m`. The profile stays authoritative for the model. ### Unit B — the agent files and the docs 1. Author `.claude/agents/` and `.opencode/agent/` definitions for `architect`, `dev` and `reviewer`. Same body text for both copies of a role, so the two backends get the same contract. Source the text from the existing `fleet.charters.architect` and from `.claude/skills/{implementer,reviewer}/SKILL.md`. 2. No `model:` key in any of them. Add a comment saying why, pointing at this ticket. 3. Update `CLAUDE.md`'s layering table — the row describing where a role contract lives is now wrong. 4. Add the `wiki/11-Features.md` entry: what it does, the knob, why it exists, the gotcha. ## Acceptance - A `sonnet` architect spawns successfully with `fleet.charters.architect` configured. That spawn fails today (#119) and is the regression test for it. - A member's reported `charterSha256` still identifies what it actually launched with. If the contract now comes from a file, the receipt must fingerprint that file, or CB-571's proof is lost. - Same contract text reaches both backends for the same role. - Tests drive the real launcher path. A test that calls the argv builder or the encoder directly walks around the gate this bug lives in — see #113. - An agent file that declares `model:` is reported at load rather than silently ignored. A warning naming the file is enough; refusing is better. ## Open point, deliberately not decided here `.claude/agents/` committed to the repo also appears in the operator's own lead session as selectable subagents. Harmless, but visible. If that turns out to be noisy, the files move under a directory the lead's session does not scan, and the launcher points at them explicitly.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#120