|
| 1 | +# Migration: `/multimodel:team` against the new `team` MCP contract |
| 2 | + |
| 3 | +**For:** whoever maintains `plugins/multimodel/commands/team.md` in the magus |
| 4 | +marketplace repo. |
| 5 | +**Why:** claudish's `team` tool changed in a way that breaks that command. The |
| 6 | +two must ship together. |
| 7 | +**File to change:** `plugins/multimodel/commands/team.md` (Step 2 and Step 3). |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## What broke, in one sentence |
| 12 | + |
| 13 | +`team(mode:"run")` no longer waits for the models and no longer returns their |
| 14 | +results. It starts them and returns a slot map immediately. |
| 15 | + |
| 16 | +The command currently calls `run` and reads per-model results straight out of |
| 17 | +the response. Those results are not there any more, so vote parsing gets a JSON |
| 18 | +object with no votes in it and the panel reports INCONCLUSIVE every time. |
| 19 | + |
| 20 | +There is a second, quieter break: `timeout` was removed from the tool schema. The |
| 21 | +schema does not set `additionalProperties: false`, so passing it is not an error |
| 22 | +— it is silently ignored. The command will look like it still sets a deadline |
| 23 | +when nothing reads it. |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Why it changed |
| 28 | + |
| 29 | +A `team` slot is a full Claude Code session and can legitimately work for a long |
| 30 | +time. The old shape held the MCP tool call open for the whole run, which made the |
| 31 | +run's duration the client's problem — a real run was aborted at exactly 1800s of |
| 32 | +client idle timeout. |
| 33 | + |
| 34 | +The deadline that existed to bound it was worse: in session |
| 35 | +`team-20260827-0015` it killed three of five slots that were all actively |
| 36 | +working, because its only progress signal was token flow, which stops during a |
| 37 | +local tool call. A model running `go test ./...` looked identical to a hung one. |
| 38 | + |
| 39 | +Nothing terminates a slot on a timer now. The caller polls, looks at the |
| 40 | +evidence, and decides. Full rationale: `ai-docs/architecture/team-lifecycle.md` |
| 41 | +in the claudish repo. |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## Step 2 — the call |
| 46 | + |
| 47 | +### Before |
| 48 | + |
| 49 | +``` |
| 50 | +claudish team(mode="run", path=SESSION_DIR, |
| 51 | + models=[...ALL resolved models, "internal" included...], |
| 52 | + input=VOTE_PROMPT, timeout=180, |
| 53 | + require_pattern="```vote", agent=RESOLVED_AGENT) |
| 54 | +``` |
| 55 | + |
| 56 | +### After |
| 57 | + |
| 58 | +``` |
| 59 | +# 1. Write VOTE_PROMPT to SESSION_DIR/input.md first. |
| 60 | +# 2. Then: |
| 61 | +claudish team(mode="run", path=SESSION_DIR, |
| 62 | + models=[...ALL resolved models, "internal" included...], |
| 63 | + input_file="SESSION_DIR/input.md", |
| 64 | + require_pattern="```vote", agent=RESOLVED_AGENT) |
| 65 | +``` |
| 66 | + |
| 67 | +Three changes: |
| 68 | + |
| 69 | +- **`timeout` is gone.** Remove it. |
| 70 | +- **`input_file` replaces `input`.** Both still work and passing BOTH is a hard |
| 71 | + error, but prefer the file. A vote prompt is 100+ lines and passing it inline |
| 72 | + echoes the whole thing verbatim in the user's terminal, burying every other |
| 73 | + argument in the tool call. The path must be inside the working directory. |
| 74 | +- **`require_pattern` is unchanged** and still the point. Keep it. |
| 75 | + |
| 76 | +### What `run` returns now |
| 77 | + |
| 78 | +```json |
| 79 | +{ |
| 80 | + "started": true, |
| 81 | + "team_session_id": "team-20260827-0015", |
| 82 | + "session_path": "/abs/path/to/SESSION_DIR", |
| 83 | + "slots": { "gpt-5.6-sol": "01", "grok-4.6": "02", "internal": "03" }, |
| 84 | + "next": { "status": "...", "cancel": "...", "judge": "..." }, |
| 85 | + "note": "..." |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +`slots` maps the display model name to its anonymised slot id. That slot id |
| 90 | +addresses everything else on disk for that model: |
| 91 | + |
| 92 | +| Path | Contents | |
| 93 | +|---|---| |
| 94 | +| `<session_path>/response-<slot>.md` | the model's answer — parse votes from here | |
| 95 | +| `<session_path>/stats/<slot>.json` | tokens, cost, tool counts | |
| 96 | +| `<session_path>/errors/<slot>.log` | stderr and diagnostics, on failure | |
| 97 | +| `<session_path>/errors/<slot>-upstream.jsonl` | raw provider error bodies, when any | |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +## Step 2b — the new polling step (this is the part that did not exist) |
| 102 | + |
| 103 | +Between starting the run and parsing votes, poll until every slot has left |
| 104 | +`RUNNING`: |
| 105 | + |
| 106 | +``` |
| 107 | +claudish team(mode="status", path=SESSION_PATH) |
| 108 | +``` |
| 109 | + |
| 110 | +Returns the full `TeamStatus` plus three added fields: |
| 111 | + |
| 112 | +```json |
| 113 | +{ |
| 114 | + "startedAt": "...", |
| 115 | + "models": { |
| 116 | + "01": { "state": "COMPLETED", "exitCode": 0, "outputSize": 10988, ... }, |
| 117 | + "02": { "state": "RUNNING", "exitCode": null, ... } |
| 118 | + }, |
| 119 | + "idle_seconds_by_slot": { "02": 94 }, |
| 120 | + "activity_by_slot": { "02": "tool_executing" }, |
| 121 | + "note": "...", |
| 122 | + "summary": "<rendered result card — present ONLY once the run has settled>" |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +**Settled means: no slot in `models` has `state === "RUNNING"`.** That is the |
| 127 | +loop condition. |
| 128 | + |
| 129 | +**`summary` is the string the old `run` used to return.** Once the run settles, |
| 130 | +`status` carries the same rendered result card — `N/M succeeded`, |
| 131 | +`reason=shape_mismatch`, and the rest. If the command's Step 3 was matching on |
| 132 | +that text, it can keep doing so; it just reads it from a settled `status` |
| 133 | +instead of from `run`. |
| 134 | + |
| 135 | +Bound the loop, and fail loudly rather than looping forever. There is no |
| 136 | +server-side deadline any more, so an unbounded poll is an unbounded wait. |
| 137 | + |
| 138 | +--- |
| 139 | + |
| 140 | +## Step 2c — deciding whether a quiet slot is stuck |
| 141 | + |
| 142 | +This is the capability the deadline used to take away from you. |
| 143 | + |
| 144 | +- `idle_seconds_by_slot` — seconds since that slot's child last wrote anything. |
| 145 | +- `activity_by_slot` — what it is doing: `running`, `tool_executing`, |
| 146 | + `waiting_for_input`, or a terminal state. |
| 147 | + |
| 148 | +**Read them together.** Ninety seconds of silence in `tool_executing` is a build |
| 149 | +or a test suite running, and is completely normal. The same ninety seconds in |
| 150 | +`running` is a model that stopped mid-answer. The old reaper had no state at all, |
| 151 | +which is exactly why it killed the first kind. |
| 152 | + |
| 153 | +Nothing cancels on your behalf. If you decide a slot is wedged: |
| 154 | + |
| 155 | +``` |
| 156 | +claudish team(mode="cancel", path=SESSION_PATH, slot="02") # one slot |
| 157 | +claudish team(mode="cancel", path=SESSION_PATH) # the whole run |
| 158 | +``` |
| 159 | + |
| 160 | +A cancelled slot is recorded with `error.reason === "cancelled"`, which is |
| 161 | +distinct from `nonzero_exit`. It is not a defect — it is a decision, and the |
| 162 | +report should say so rather than showing the model as crashed. |
| 163 | + |
| 164 | +Suggested default for a vote panel: do not cancel automatically. Report the slot |
| 165 | +as still running and let the user decide. Losing a vote to an impatient |
| 166 | +auto-cancel is the same failure the deadline used to cause. |
| 167 | + |
| 168 | +--- |
| 169 | + |
| 170 | +## Step 3 — parsing votes |
| 171 | + |
| 172 | +Unchanged in substance, different in where the text comes from. |
| 173 | + |
| 174 | +**Before:** per-model results were read from the `run` response. |
| 175 | + |
| 176 | +**After:** read `<session_path>/response-<slot>.md` for each slot in the map |
| 177 | +returned by `run`, after `status` reports settled. The vote regex is unchanged: |
| 178 | + |
| 179 | +``` |
| 180 | +/```vote\s*\n([\s\S]*?)\n\s*```/ |
| 181 | +``` |
| 182 | + |
| 183 | +Failure handling is unchanged too: a slot whose `models[<slot>].state` is |
| 184 | +`FAILED` or `EMPTY` did not vote. `error.reason` tells you why — |
| 185 | +`shape_mismatch` means it answered but never produced the required block, which |
| 186 | +still must NOT be counted as a vote. |
| 187 | + |
| 188 | +--- |
| 189 | + |
| 190 | +## What did NOT change |
| 191 | + |
| 192 | +- `require_pattern` and `min_output_bytes` — same semantics, still recommended. |
| 193 | +- `agent` — same, still applies to every model in the run. |
| 194 | +- `mode:"judge"` — unchanged. |
| 195 | +- `mode:"run-and-judge"` — still BLOCKING, and still returns the verdict. If the |
| 196 | + command would rather not implement polling at all, this mode is the drop-in |
| 197 | + path. The trade is that it holds the tool call open for the whole run, which is |
| 198 | + the shape that hit the client's 1800s idle abort. |
| 199 | +- Native Claude names (`internal`, `default`, `opus`, …) are still ordinary |
| 200 | + slots and still belong in `models`. |
| 201 | + |
| 202 | +--- |
| 203 | + |
| 204 | +## Minimum viable change |
| 205 | + |
| 206 | +If you want the smallest possible diff and are willing to keep the blocking |
| 207 | +behaviour: |
| 208 | + |
| 209 | +1. Remove `timeout` from the call. |
| 210 | +2. Change `mode:"run"` to `mode:"run-and-judge"`, or keep `run` and add the poll. |
| 211 | + |
| 212 | +If you want the shape the tool is now designed around, implement the poll in |
| 213 | +Step 2b — it is what lets the panel survive a slow model instead of losing its |
| 214 | +vote. |
| 215 | + |
| 216 | +--- |
| 217 | + |
| 218 | +## Version |
| 219 | + |
| 220 | +Ships in claudish **v8.0.0**. The major bump is for this change specifically. |
| 221 | +Pin or require `>=8.0.0` once the command is updated, and note that a plugin |
| 222 | +updated for v8 will not work against v7.67.x — `input_file` does not exist there |
| 223 | +and `run` still blocks. |
0 commit comments