Skip to content

Commit 56e4963

Browse files
committed
chore: release v8.0.0
Major, for one breaking change: `team(mode:"run")` no longer waits for the models. It starts them and returns a slot map; the caller polls `mode:"status"`. The deadline it replaces was actively harmful. In session team-20260827-0015 it killed three of five slots that were all working — 45, 13 and 24 tool calls, up to 30,557 output tokens at the moment they died — because its only progress signal was token flow, which stops for the entire duration of a local tool call. A model running `go test ./...` was indistinguishable from a hung one. Every kill landed within 133ms of a 60s multiple: the watcher's poll cadence, not a deadline at all. `timeout` is removed with it, from the tool schema, `TeamRunOptions` and every `runModels` call site. It controlled nothing once the reaper was gone. Consumers must migrate. `/multimodel:team` in the magus marketplace repo reads per-model results straight from the `run` response and passes `timeout`; both break. Migration guide with before/after for every changed call: ai-docs/reports/team-mcp-breaking-change-plugin-migration.md Also ships two latent bugs found by comparing team's child supervision against the channel's — non-ASCII answers measured wrong, and upstream-error capture dead for every team child since the feature existed — plus `bun --cwd` script ordering that had silently skipped packages/macos-bridge in test, lint and format. Full detail in CHANGELOG.md. Verified: typecheck clean; build clean; `bun run test:safe` green — 3017 pass / 17 skip / 0 fail across 193 files (packages/cli) plus 20 pass / 0 fail (packages/macos-bridge), exit 0. Co-Authored-By: Magus <magus@madappgang.com> Crafted with agentic harness Magus (https://github.com/MadAppGang/magus)
1 parent a310e14 commit 56e4963

5 files changed

Lines changed: 290 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,70 @@
22

33
All notable changes to [Claudish](https://github.com/MadAppGang/claudish).
44

5+
## [8.0.0] - 2026-08-27
6+
7+
### BREAKING CHANGES
8+
9+
- **`team(mode:"run")` no longer waits for the models.** It starts them and returns a slot
10+
map immediately; poll `mode:"status"` for progress and results. A team slot is a full
11+
Claude Code session and may work for a long time, and holding the tool call open made the
12+
run's duration the client's problem — a real run was aborted at exactly 1800s of client
13+
idle timeout. `mode:"run-and-judge"` is unchanged and still blocks.
14+
- **The `timeout` parameter is removed** from the `team` tool, `TeamRunOptions`, and the
15+
`runModels` call sites. It controlled nothing once the deadline was deleted. `--timeout`
16+
survives in `team-cli` for the magmux grid path, which has its own.
17+
- Consumers must migrate. See `ai-docs/reports/team-mcp-breaking-change-plugin-migration.md`.
18+
19+
### Features
20+
21+
- **Nothing terminates a team slot on a timer.** The stall reaper killed three of five
22+
productive slots in session `team-20260827-0015`; its only progress signal was
23+
`stats/<id>.json`, which advances on token flow and therefore freezes for the whole
24+
duration of a local tool call. Every kill landed within 133ms of a 60s multiple — the
25+
watcher's poll cadence, not a deadline. Claude Code already bounds every tool call itself
26+
(Bash default 120s, max 600s), so the detector guarded a condition the harness prevents.
27+
- `team(mode:"cancel", slot?)` — the only thing that kills a slot now, and it kills the
28+
process group. A cancelled slot records `reason: "cancelled"`, distinct from
29+
`nonzero_exit`: it is a decision, not a defect.
30+
- `mode:"status"` reports `idle_seconds_by_slot` and `activity_by_slot`. Read together,
31+
they separate a slot inside a build from one that stopped mid-answer. Reported, never
32+
acted on. Once settled, `status` also carries `summary`, the result card `run` used to
33+
return.
34+
- `input_file` on the `team` tool, preferred over `input`. A vote prompt is typically a
35+
200-line brief, and inline text is echoed verbatim in the caller's terminal. Contained to
36+
the working directory on the same terms as `path`.
37+
- Channel sessions report `idleSeconds` on `list_sessions`, `get_output` and
38+
`get_diagnostics`.
39+
40+
### Bug Fixes
41+
42+
- **`team` mangled non-ASCII answers.** It read child stdout with `chunk.toString()`, which
43+
replaces a codepoint split across a pipe read boundary with U+FFFD. Measured: a 4-byte
44+
emoji became three replacement characters AND the byte count inflated 19 → 24 — and
45+
`outputSize` feeds `minOutputBytes` and the empty check, so answers were MEASURED wrong,
46+
not merely displayed wrong. Both subsystems now share `stdio-decode.ts`.
47+
- **`team`'s upstream-error capture never ran.** `captureUpstreamError` is gated on
48+
`CLAUDISH_UPSTREAM_ERROR_LOG`, which `team` never set — a guaranteed no-op for every team
49+
child since the feature existed. The provider body that separates a retryable rate limit
50+
from a hard quota wall was discarded as soon as it was classified. Now set per slot, and
51+
`ModelError.upstreamErrorLogPath` names the file only when one was written.
52+
- **`bun --cwd <dir> run <script>` runs nothing and exits 0.** Bun prints usage. All four
53+
occurrences sat on the right of an `&&`, so `test`, `lint` and `format` silently skipped
54+
`packages/macos-bridge` — 20 tests that had never run through the root scripts.
55+
56+
### Refactor
57+
58+
- One stream-json parser. `team` fed `createAssistantTextCapture()` directly and hand-rolled
59+
the supervision; `StreamJsonReducer` is now its only production consumer. The parsers
60+
disagreed on one case — valid JSON that is not stream-json vocabulary — which is now the
61+
documented `keepUnrecognizedJson` option rather than one rule imposed on both.
62+
63+
### Documentation
64+
65+
- `ai-docs/architecture/team-lifecycle.md` — why nothing kills a slot, the measurement that
66+
removed the deadline, and the `SessionManager` adoption route that was evaluated and
67+
rejected.
68+
569
## [7.67.1] - 2026-08-27
670

771
### Bug Fixes
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "claudish-monorepo",
3-
"version": "7.67.1",
3+
"version": "8.0.0",
44
"private": true,
55
"description": "Monorepo for Claudish - Run Claude Code with any model",
66
"type": "module",

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "claudish",
3-
"version": "7.67.1",
3+
"version": "8.0.0",
44
"description": "Run Claude Code with any model - OpenRouter, Ollama, LM Studio & local models",
55
"type": "module",
66
"main": "./dist/index.js",

packages/cli/src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
// Auto-generated by scripts/generate-version.ts — do not edit
2-
export const VERSION = "7.67.1";
2+
export const VERSION = "8.0.0";

0 commit comments

Comments
 (0)