Skip to content

Commit 22ee856

Browse files
WatchAndyTWclaude
andcommitted
feat: /team --writable worktree mode (v0.8.0)
Add an opt-in writable mode to /team: CLI/native agents make REAL file changes in isolated git worktrees, merged into one local integration branch. The read-only default is unchanged and now explicit — it never creates a branch; the orchestrator applies edits to the current branch directly. - backends.mjs: invoke() threads opts.cwd (run the CLI in the subtask's worktree) and opts.writable (use the backend's writable_extra/full-auto instead of the read-only `extra`); runChild + runPty both honor cwd. invocationExtra() warns loudly if writable is requested but a backend has no writable_extra (never silently runs read-only). - roster.json + config.mjs: per-backend `writable_extra` (codex --dangerously-bypass-approvals-and-sandbox; agy keeps its flags, differing only by the worktree cwd), passed through config normalization. - run.mjs: --cwd / --writable flags (also readable from the call file; the explicit flag wins). Inert when absent — default/read-only runs unchanged. - workflows/team.mjs: WRITABLE mode. Deterministic slug + git-ref/path-safe safeLabel (no .., no .lock, no leading/trailing ./-). Setup stage creates the integration branch + a per-subtask worktree + a DEDICATED integration worktree, all off HEAD and idempotent (resume-safe via show-ref||branch). Dispatch passes --cwd --writable. Integrate stage merges each subtask branch INSIDE the integration worktree (never the user's checkout), --no-ff, aborts + reports conflicts (keeps conflicted worktrees), removes only clean/empty ones. User's branch is untouched; no gh PR. Returns mode + writable.{integration_branch, integration}. - commands/team.md + CLAUDE.md: document both modes + the read-only no-branch rule; pass writable through the Workflow args. - Tests: 99/99 offline (was 93) — end-to-end cwd-write isolation + writable vs read-only flag selection (real subprocess), --cwd/--writable (incl --cwd=VALUE) parsing, writable-mode workflow markers, and a safeLabel git-ref edge-case test. Docs/badge -> 99. Codex-verified: codex reviewed the diff (FAIL), I fixed every real finding (dedicated integration worktree so the user's tree is never touched; loud writable-without-config warning; safeLabel ref-safety; idempotent setup; keep conflicted worktrees; string-form writable), disproved one false positive (--cwd=VALUE parsing), and a focused re-review returned PASS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 77ca5d4 commit 22ee856

14 files changed

Lines changed: 454 additions & 37 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
33
"name": "multi-model-team",
44
"displayName": "Multi-Model Team",
5-
"version": "0.7.5",
5+
"version": "0.8.0",
66
"description": "Delegates token-heavy, self-contained tasks to local CLI backends chosen by task size and type, with credit-exhaustion fallback through the backend chain to native Claude and a glanceable statusline HUD. Routing and shipped backend behavior are configured in config/roster.json; adding a new backend kind requires invoker and health support in code. /team runs a decompose → dispatch → verify → fix → synthesize pipeline; an optional config-gated UserPromptSubmit hook can proactively nudge delegation.",
77
"author": {
88
"name": "WatchAndyTW",

CLAUDE.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ glanceable statusline HUD.
99
(the agy lane runs under a real pseudo-terminal — ConPTY on Windows, forkpty on POSIX); everything
1010
else is Node stdlib. Cross-platform (Windows/Linux/macOS). `package.json` `"type":"module"`.
1111

12-
**Status:** built, adversarially reviewed, and green. `npm test` passes **93/93** offline
12+
**Status:** built, adversarially reviewed, and green. `npm test` passes **99/99** offline
1313
(plus live agy + codex smoke tests under `MMT_LIVE=1`). Two live backends: **agy** (Gemini)
1414
and **codex** (OpenAI Codex CLI); opencode remains a config-only stub. codex also serves as the
1515
**`/team` verifier**. See `README.md` (user-facing), `PROBES.md` (grounded CLI findings), and
@@ -178,12 +178,25 @@ agy; `premium` pulls standard-coding up to Sonnet (keeps agy for its categorical
178178

179179
## /team — multi-model team pipeline (v0.3)
180180

181-
`/team [N:gemini,M:claude] <task>` runs a task through a staged **plan → exec → verify → fix**
181+
`/team [--writable] [N:gemini,M:claude] <task>` runs a task through a staged **plan → exec → verify → fix**
182182
pipeline: **native, agy and codex are equal, configurable tools** — the decomposer assigns each
183183
subtask to its best-fit backend, native Claude plans/synthesizes, and any backend (default codex)
184184
verifies. Stages: **decompose → dispatch (dependency-aware) → verify → fix (bounded) → synthesize**.
185185
Flow (in `commands/team.md`):
186186

187+
**Two modes (`--writable`).** *read-only (default):* CLI agents stay read-only (`-s read-only`), return
188+
text, and the **orchestrator applies edits directly to the CURRENT branch — no branch, no worktree, no
189+
PR** (back-compat). *writable (`--writable`):* each subtask gets its **own git worktree + branch** off
190+
HEAD; the agent makes **real changes** there (CLI **full-auto** via `run.mjs --cwd <wt> --writable`,
191+
selecting the backend's `writable_extra` instead of `extra`); then a deterministic **Setup → Integrate**
192+
pair of Bash sub-agents (the Workflow runtime has no fs/git) create the worktrees and **merge each into
193+
one integration branch `mmt/team-<slug>`** off HEAD — **conflicts reported, never auto-resolved**; the
194+
user's branch is untouched (no auto-merge) and **no `gh` PR** is created. Worktrees live under
195+
`.mmt/worktrees/<slug>/` (gitignored). Enable per-invocation (`--writable` / Workflow `args.writable`)
196+
or via roster `team.mode:"writable"`. The full-auto sandbox is config-tunable: each backend's
197+
`writable_extra` in `roster.json` (codex `--dangerously-bypass-approvals-and-sandbox`; agy keeps its
198+
`--dangerously-skip-permissions`, differing from read-only only by the worktree cwd).
199+
187200
1. Parse the optional cap spec via `src/lib/team-spec.mjs``{gemini, claude}` (caps = max
188201
agents per backend; `gemini`=agy, `claude`=native; defaults 4/2; aliases + clamp).
189202
2. Claude decomposes the task, then **writes `.mmt/plans/plan.json`** (array of
@@ -335,7 +348,7 @@ fallback hop.
335348
## Testing
336349

337350
```bash
338-
npm test # offline: 93/93 routing + unit tests (no backend calls)
351+
npm test # offline: 99/99 routing + unit tests (no backend calls)
339352
MMT_LIVE=1 npm test # + live agy + codex smoke tests (network required)
340353
```
341354

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Multi-model orchestration for Claude Code. Route by task, fan out in parallel, f
88

99
![Node](https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white)
1010
![Type](https://img.shields.io/badge/module-ESM-f7df1e)
11-
![Tests](https://img.shields.io/badge/tests-93%2F93%20passing-3fb950)
11+
![Tests](https://img.shields.io/badge/tests-99%2F99%20passing-3fb950)
1212
![Platforms](https://img.shields.io/badge/platform-win%20%7C%20linux%20%7C%20macOS-555)
1313
![Deps](https://img.shields.io/badge/runtime%20deps-1%20(node--pty)-blue)
1414

@@ -276,7 +276,7 @@ docs/REASONING.md design contract for the /reasoning Fusion pipeline
276276
## 🧪 Testing
277277

278278
```bash
279-
npm test # offline: 93/93 routing + unit tests (no backend calls)
279+
npm test # offline: 99/99 routing + unit tests (no backend calls)
280280
MMT_LIVE=1 npm test # also run live agy + codex smoke tests (network required)
281281
```
282282

commands/team.md

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
description: Run a task through the multi-model team pipeline — decompose into backend-assigned subtasks (commodity → parallel agy/Gemini, judgment/hard-line → native Claude), dispatch dependency-aware, verify each result, fix failures in a bounded loop, then synthesize. Optional caps like "5:gemini,2:claude".
3-
argument-hint: "[N:gemini,M:claude] <task>"
2+
description: Run a task through the multi-model team pipeline — decompose into backend-assigned subtasks (commodity → parallel agy/Gemini, judgment/hard-line → native Claude), dispatch dependency-aware, verify each result, fix failures in a bounded loop, then synthesize. Optional caps like "5:gemini,2:claude". Add --writable to give each agent its own git worktree and merge into an integration branch.
3+
argument-hint: "[--writable] [N:gemini,M:claude] <task>"
44
allowed-tools: Bash, Write, Task
55
---
66

@@ -52,6 +52,25 @@ MMT_ARGS_EOF
5252
`"spec"`** (the user actually typed a spec); on `"default"`, omit caps so the roster `team.caps`
5353
applies. If `.note` is non-empty, surface it.
5454

55+
### Mode: read-only (default) vs `--writable`
56+
Before decomposing, detect the mode from the raw input. If the input contains the token **`--writable`**
57+
(strip it from the task text first — it is a mode flag, not part of the task), run in **writable mode**;
58+
otherwise **read-only mode** (the default).
59+
60+
- **read-only (DEFAULT):** the CLI agents (agy/codex) stay **read-only** — they return text, not edits.
61+
Any file changes are applied by **you (the orchestrator) directly to the CURRENT branch**. Do **NOT**
62+
create a branch, a worktree, or a PR in this mode. This is the back-compat behaviour.
63+
- **writable (`--writable`):** each subtask gets its **own git worktree + branch** off the current HEAD;
64+
the assigned agent (CLI **full-auto** via `run.mjs --cwd <worktree> --writable`, or a native solver
65+
writing in the worktree) makes **real file changes** there; then the orchestrator **merges every
66+
subtask branch into one integration branch `mmt/team-<slug>`** off HEAD, **reports conflicts** (never
67+
blind-resolves them), and leaves that branch for you. Your current branch is **untouched** (no
68+
auto-merge), and **no GitHub PR is created** — you inspect/merge/PR the integration branch yourself.
69+
70+
In the Ultracode path, pass `writable: true` in the Workflow args for writable mode (the workflow runs
71+
the Setup → Dispatch → Integrate stages). In the Task-agent path, follow the writable steps marked
72+
**(writable mode)** below; in read-only mode skip them and apply edits to the current branch directly.
73+
5574
## 1.5 · Load the team roles config (and honor in-session overrides)
5675
The pipeline's roles are **config-driven**, not hardcoded. Read the merged team config (roster
5776
`team` over built-in defaults) — this never touches the task text, so it's safe to run plainly:
@@ -231,6 +250,7 @@ Workflow({
231250
// optional top-level in-session overrides — these WIN over teamConfig (omit if none):
232251
caps: <{ gemini, codex, claude } ONLY if the cap spec source=="spec">,
233252
verifier: "<any backend, or 'native'>",
253+
writable: <true ONLY if the user passed --writable; omit/false otherwise>,
234254
verify: true, maxFixLoops: 1 }
235255
})
236256
```
@@ -242,8 +262,12 @@ back if it's unavailable), runs a bounded fix loop on failures, and synthesizes.
242262
`args.teamConfig` (the roster `team` section): `dispatch_backends` (the equal set), `verifier`
243263
(`"native"` = Claude judgment), `caps`, `tier_models`, `verify`, `max_fix_loops`. Per-invocation args
244264
override (`verifier`, `caps`, `verify`, `maxFixLoops`). With no `teamConfig`, the built-in defaults
245-
apply (all three backends eligible; codex verify). Read its returned `{ plan, backends, caps,
246-
counts:{byBackend,…}, verifier, results, final }` and present `final`, noting `counts.failed` if
247-
non-zero. (`args.task` is passed as a JSON value, not shell — injection-safe.)
265+
apply (all three backends eligible; codex verify). Read its returned `{ plan, mode, backends, caps,
266+
counts:{byBackend,…}, verifier, writable, results, final }` and present `final`, noting `counts.failed`
267+
if non-zero. **In writable mode** (`mode:"writable"`), also surface `writable.integration_branch` and
268+
`writable.integration` — tell the user the changes are on that branch (their current branch is
269+
untouched), list any `writable.integration.conflicts` that still need manual resolution, and how to
270+
inspect it (`git switch <integration_branch>` / `git log <integration_branch>`). (`args.task` is passed
271+
as a JSON value, not shell — injection-safe.)
248272

249273
A trivial single task needs no fan-out: one subtask reduces this to a plain verified dispatch.

config/roster.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
"oneshot_flag": "--print",
4040
"model_flag": "--model",
4141
"extra": ["--dangerously-skip-permissions", "--print-timeout", "5m"],
42+
"_writable_note": "Flags used INSTEAD of `extra` in /team --writable mode (full-auto: agy may write files + run commands in its worktree cwd). agy's --dangerously-skip-permissions already lets it act; the writable lane only differs by running in the worktree cwd. Same flags here = no sandbox swap needed for agy, just cwd.",
43+
"writable_extra": ["--dangerously-skip-permissions", "--print-timeout", "5m"],
4244
"models": { "cheap": "Gemini 3.5 Flash (Low)", "standard": "Gemini 3.1 Pro (Low)" },
4345
"health": "--version",
4446
"list_models": "models",
@@ -68,6 +70,8 @@
6870
"oneshot_flag": "exec",
6971
"model_flag": "-m",
7072
"extra": ["-s", "read-only", "--skip-git-repo-check", "--color", "never"],
73+
"_writable_note": "Flags used INSTEAD of `extra` in /team --writable mode. Full-auto: `--dangerously-bypass-approvals-and-sandbox` lets codex write files, run commands, and use the network — confined to the worktree cwd the orchestrator passes via --cwd. Drop --skip-git-repo-check since a worktree IS a git repo.",
74+
"writable_extra": ["--dangerously-bypass-approvals-and-sandbox", "--color", "never"],
7175
"models": { "cheap": "gpt-5.4-mini", "standard": "gpt-5.5" },
7276
"health": "--version",
7377
"add_dir_flag": "--add-dir",

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "multi-model-team",
3-
"version": "0.7.5",
3+
"version": "0.8.0",
44
"type": "module",
55
"private": true,
66
"description": "Delegates token-heavy, self-contained tasks to local CLI backends (agy/Gemini, codex) chosen by task size and type, with credit-exhaustion fallback to native Claude and a statusline HUD. Node ESM, zero-build, cross-platform (Windows/Linux/macOS).",

src/bin/run.mjs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const COMPACT_PROMPT = (task) => `${COMPACT_CONTRACT}\n\n${task}`;
2929

3030
// ---- arg parsing -----------------------------------------------------------
3131
function parseArgs(argv) {
32-
const o = { preset: '', tags: '', roster: '', decision: '', callFile: '', taskFile: '', decisionFile: '', sandbox: false, addDir: '', task: '' };
32+
const o = { preset: '', tags: '', roster: '', decision: '', callFile: '', taskFile: '', decisionFile: '', sandbox: false, addDir: '', cwd: '', writable: false, task: '' };
3333
let i = 0;
3434
const positional = [];
3535
while (i < argv.length) {
@@ -54,6 +54,11 @@ function parseArgs(argv) {
5454
case '--task-file': o.taskFile = next() ?? ''; break;
5555
case '--decision-file': o.decisionFile = next() ?? ''; break;
5656
case '--add-dir': o.addDir = next() ?? ''; break;
57+
// /team --writable mode: run the backend CLI IN this dir (the subtask's git worktree) and use
58+
// the backend's writable_extra (full-auto) so it can actually write files there. Both are inert
59+
// when absent — default/read-only runs are unchanged.
60+
case '--cwd': o.cwd = next() ?? ''; break;
61+
case '--writable': o.writable = true; break;
5762
case '--sandbox': o.sandbox = true; break;
5863
case '--': positional.push(...argv.slice(i + 1)); i = argv.length; break;
5964
default:
@@ -227,6 +232,11 @@ async function main() {
227232
if (!task) task = (await readStdin()).trim();
228233
if (!task) { process.stderr.write('run.mjs: no task text\n'); process.exit(2); }
229234

235+
// Writable-mode inputs: a --cwd (the subtask's worktree) + --writable (full-auto). The relay may
236+
// carry them in the call file too; the explicit flag wins. Both inert when absent (default lane).
237+
const cwd = opts.cwd || (callPayload && typeof callPayload.cwd === 'string' ? callPayload.cwd : '');
238+
const writable = opts.writable || !!(callPayload && callPayload.writable);
239+
230240
// --roster flag wins; else shared resolver: .mmt/roster.json (cwd) > ~/.claude/mmt-roster.json > plugin default.
231241
const rosterPath = opts.roster || resolveRosterPath(MMT_ROOT);
232242
const tagsPath = opts.tags || path.join(MMT_ROOT, 'config', 'tags.txt');
@@ -350,7 +360,7 @@ async function main() {
350360
const stopHeartbeat = startHeartbeat({ callId, statusFile, backend: be, model, tier: D_tier, rule: D_rule, startMs });
351361
let res;
352362
try {
353-
res = await invoke(invokeCfg, fullPrompt, { model, tier: D_tier, addDir: opts.addDir });
363+
res = await invoke(invokeCfg, fullPrompt, { model, tier: D_tier, addDir: opts.addDir, cwd, writable });
354364
} catch (e) {
355365
res = { ok: false, stdout: '', stderr: String(e && e.message || e), code: 1, quota: false };
356366
}

0 commit comments

Comments
 (0)