Skip to content

Commit 2955356

Browse files
WatchAndyTWclaude
andcommitted
feat(team): writable integration cherry-picks raw commits — no merge commits, clean per-subtask messages (v0.8.5)
/team --writable used to integrate each subtask worktree with `git merge --no-ff -m "mmt(<SLUG>): merge <label>"`, which produced noisy merge commits whose message embedded the 32-char task SLUG (e.g. `mmt(audit-the-design-consistency-of-): merge detail-ui-polish`). The user wanted a clean linear history: only raw per-subtask commits + conflict resolution folded in, with well-formed messages. Integration stage (workflows/team.mjs) now: - replays each subtask branch onto the integration branch with `git cherry-pick "$B".."<branch>"` (range from the merge-base) instead of `git merge --no-ff` — NO merge commits, one raw commit per subtask commit. Using the RANGE (not the bare branch tip) means a multi-commit branch — a native solver or a fix re-dispatch may leave several — is fully replayed, not silently truncated to its tip. - commits each worktree with a clean conventional message scoped to the short SUBTASK LABEL: `<type>(<label>): <summary>` (feat/fix/refactor/…), explicitly NOT the task SLUG and never a "merge ..." message. - decides "empty" by the branch's commit count ahead of the merge-base (`rev-list --count`), NOT worktree dirtiness — so a subtask the agent already committed itself (clean tree, non-empty branch) is integrated, not dropped. - resolves conflicts in place and folds the fix into the SAME raw commit via `GIT_EDITOR=true git cherry-pick --continue` (no separate merge/resolution commit, no editor hang in the TTY-less sub-agent); handles empty-after-pick duplicates with `cherry-pick --skip`; `--abort` cleanly backs out the whole range for an unreconcilable conflict (→ unresolved). Docs (commands/team.md + workflow comments + INTEGRATE_SCHEMA descriptions) updated to match. Verified end-to-end on throwaway repos (multi-commit range, empty branch, empty-after-pick, conflict-resolved → all land as raw commits, ZERO merge commits) and by codex through the live relay (3 rounds: FAIL→FAIL→ PASS, fixing a real multi-commit-drop bug and a contradictory empty-gate it caught). 108/108 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e8fc728 commit 2955356

6 files changed

Lines changed: 49 additions & 39 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
{
1111
"name": "multi-model-team",
1212
"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.",
13-
"version": "0.8.4",
13+
"version": "0.8.5",
1414
"author": {
1515
"name": "WatchAndyTW",
1616
"email": "watchandytw@gmail.com"
@@ -29,5 +29,5 @@
2929
]
3030
}
3131
],
32-
"version": "0.8.4"
32+
"version": "0.8.5"
3333
}

.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.8.4",
5+
"version": "0.8.5",
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",

commands/team.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,12 @@ otherwise **read-only mode** (the default).
6262
create a branch, a worktree, or a PR in this mode. This is the back-compat behaviour.
6363
- **writable (`--writable`):** each subtask gets its **own git worktree + branch** off the current HEAD;
6464
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 and **resolves any merge
67-
conflicts itself** (reading both sides, editing to a correct combined result, completing the merge) —
68-
so you get **one finished, conflict-free branch**, not a pile of worktrees. (Only a conflict it
65+
writing in the worktree) makes **real file changes** there; then the orchestrator **cherry-picks every
66+
subtask's commit onto one integration branch `mmt/team-<slug>`** off HEAD (**no merge commits — one
67+
clean raw commit per subtask**, message scoped to the subtask label) and **resolves any conflicts
68+
itself** (reading both sides, editing to a correct combined result, folding the fix into that same raw
69+
commit) — so you get **one finished, conflict-free branch with a clean linear history**, not a pile of
70+
worktrees and no noisy `merge <label>` commits. (Only a conflict it
6971
genuinely can't reconcile is left `unresolved` for you.) Your current branch is **untouched** (no
7072
auto-merge onto it), and **no GitHub PR is created** — you merge/PR the integration branch when ready.
7173

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.8.4",
3+
"version": "0.8.5",
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).",

workflows/team.mjs

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const meta = {
77
{ title: 'Dispatch', detail: 'dependency-ordered waves: each subtask on its assigned backend (CLI relay or native)' },
88
{ title: 'Verify', detail: 'score each result against its acceptance criterion' },
99
{ title: 'Fix', detail: 'bounded re-dispatch of failed subtasks with verifier feedback' },
10-
{ title: 'Integrate', detail: 'writable mode only: merge each worktree into the integration branch, orchestrator resolves any conflicts' },
10+
{ title: 'Integrate', detail: 'writable mode only: cherry-pick each worktree commit onto the integration branch (no merge commits), orchestrator resolves any conflicts' },
1111
{ title: 'Synthesize', detail: 'merge verified results into one answer' },
1212
],
1313
}
@@ -28,10 +28,11 @@ export const meta = {
2828
// Modes: read-only (DEFAULT) — CLI agents return text, the orchestrator applies edits to the CURRENT
2929
// branch, no branch/worktree/PR. writable (A.writable) — each subtask gets its own git worktree+
3030
// branch off HEAD, the agent writes real changes there (CLI full-auto via run.mjs --cwd --writable),
31-
// and a deterministic Setup/Integrate pair of Bash sub-agents create the worktrees and merge them
32-
// into one integration branch `mmt/team-<slug>` off HEAD; the orchestrator RESOLVES any merge
33-
// conflicts itself and completes the merge, so the user gets one finished, conflict-free branch (the
34-
// user's branch is untouched; no gh PR). git runs in sub-agents because the Workflow runtime has no
31+
// and a deterministic Setup/Integrate pair of Bash sub-agents create the worktrees and cherry-pick
32+
// each subtask commit onto one integration branch `mmt/team-<slug>` off HEAD (NO merge commits — one
33+
// clean raw commit per subtask); the orchestrator RESOLVES any conflicts itself and folds the fix
34+
// into that same raw commit, so the user gets one finished, conflict-free branch with a clean linear
35+
// history (the user's branch is untouched; no gh PR). git runs in sub-agents because the runtime has no
3536
// fs/git; the workflow only orchestrates them deterministically (slug/labels, no Date/random).
3637
// =============================================================================
3738

@@ -85,9 +86,10 @@ const MAX_FIX = Math.max(0, Math.min(3, Number(A.maxFixLoops ?? TC.max_fix_loops
8586
// edits directly to the CURRENT branch. No branch, no worktree, no PR (back-compat).
8687
// writable (A.writable===true / TC.mode==='writable'): each subtask gets its OWN git worktree +
8788
// branch off current HEAD; the agent (CLI full-auto, or native) writes real changes there; then
88-
// a deterministic integration stage merges every agent branch into ONE integration branch
89-
// `mmt/team-<slug>` off current HEAD, the orchestrator RESOLVING any conflicts so the branch is
90-
// finished + conflict-free (no auto-merge onto their branch, no gh PR). The Workflow runtime has
89+
// a deterministic integration stage cherry-picks every agent's commit onto ONE integration branch
90+
// `mmt/team-<slug>` off current HEAD (no merge commits — one clean raw commit per subtask), the
91+
// orchestrator RESOLVING any conflicts so the branch is finished + conflict-free (no auto-merge
92+
// onto their branch, no gh PR). The Workflow runtime has
9193
// no fs/git, so the worktree lifecycle is run by sub-agents (Bash tool); the workflow
9294
// orchestrates them deterministically.
9395
// Accept boolean true OR the strings "true"/"writable"/"1"/"yes" (args/roster values may arrive as
@@ -242,18 +244,18 @@ const SETUP_SCHEMA = {
242244
required: ['ok'],
243245
}
244246

245-
// Writable-mode integration: the merge-into-integration-branch report.
247+
// Writable-mode integration: the cherry-pick-onto-integration-branch report.
246248
const INTEGRATE_SCHEMA = {
247249
type: 'object',
248250
additionalProperties: false,
249251
properties: {
250252
ok: { type: 'boolean' },
251253
integration_branch: { type: 'string' },
252254
base_sha: { type: 'string' },
253-
merged: { type: 'array', items: { type: 'string' }, description: 'labels that merged CLEANLY into the integration branch' },
254-
resolved: { type: 'array', items: { type: 'string' }, description: 'labels that hit conflicts which the orchestrator RESOLVED, then completed the merge' },
255+
merged: { type: 'array', items: { type: 'string' }, description: 'labels that cherry-picked CLEANLY onto the integration branch (one raw commit each)' },
256+
resolved: { type: 'array', items: { type: 'string' }, description: 'labels that hit conflicts which the orchestrator RESOLVED, folded into the same raw commit (cherry-pick --continue)' },
255257
unresolved: { type: 'array', items: { type: 'string' }, description: 'labels whose conflict could not be safely resolved and were left for the user (rare)' },
256-
empty: { type: 'array', items: { type: 'string' }, description: 'labels whose worktree had no changes to merge' },
258+
empty: { type: 'array', items: { type: 'string' }, description: 'labels whose worktree had no changes to integrate' },
257259
summary: { type: 'string', description: 'human-readable summary of the integration outcome + how to inspect/merge the branch' },
258260
reason: { type: 'string' },
259261
},
@@ -646,37 +648,43 @@ const usage = {
646648
}
647649
if (cliRecords.length) log(`usage: ${cliRecords.length} subtask(s) executed on CLI (off-budget); ${nativeRecords.length} on native Claude`)
648650

649-
// ---- 2.5 · Writable integration: merge worktrees -> integration branch ------
651+
// ---- 2.5 · Writable integration: replay worktree commits -> integration branch ------
650652
// A single integration sub-agent (native Claude, Bash + edit tools) commits each subtask's worktree
651-
// on its branch, then merges every branch into the integration branch (off HEAD). On a CONFLICT it
652-
// RESOLVES it itself — reading both sides and editing the files to a correct combined result, then
653-
// completing the merge — so the user ends up with ONE finished, conflict-free integration branch
654-
// (not a pile of worktrees to merge by hand). Only a conflict it genuinely cannot resolve is left
655-
// for the user (reported under `unresolved`). The user's current branch is NEVER touched and no gh
656-
// PR is created — the result sits on `${INT_BRANCH}` for them to merge/PR when they're ready.
653+
// on its branch with a CLEAN message, then CHERRY-PICKS each subtask commit onto the integration
654+
// branch (off HEAD) — NO merge commits. Each subtask lands as one raw commit; a conflict is resolved
655+
// IN-PLACE and folded into that same raw commit (cherry-pick --continue), so the only commits on the
656+
// branch are the per-subtask ones plus, at most, conflict-resolution work squashed into them. The
657+
// user ends up with ONE finished, conflict-free integration branch with a clean linear history (not
658+
// a pile of worktrees, and not noisy `merge <label>` commits). Only a conflict it genuinely cannot
659+
// resolve is left for the user (reported under `unresolved`). The user's current branch is NEVER
660+
// touched and no gh PR is created — the result sits on `${INT_BRANCH}` for them to merge/PR.
657661
let integration = null
658662
if (WRITABLE) {
659663
phase('Integrate')
660-
const labelLines = kept.map((s) => ` - "${s.label}": worktree "${worktreeFor(s.label)}", branch "${branchFor(s.label)}"`).join('\n')
664+
const labelLines = kept.map((s) => ` - "${s.label}": worktree "${worktreeFor(s.label)}", branch "${branchFor(s.label)}", goal: ${JSON.stringify(String(s.task || '').slice(0, 140))}`).join('\n')
661665
integration = await agent(
662-
`You are the WRITABLE-MODE INTEGRATION agent for the multi-model-team /team pipeline. Use the Bash tool (plus the Read/Edit/Write tools when resolving a conflict) to merge each subtask's worktree into the integration branch AND to resolve any merge conflicts yourself. The goal is ONE finished, conflict-free integration branch the user can merge as-is — not a pile of worktrees for them to reconcile.
666+
`You are the WRITABLE-MODE INTEGRATION agent for the multi-model-team /team pipeline. Use the Bash tool (plus the Read/Edit/Write tools when resolving a conflict) to bring each subtask's worktree changes onto the integration branch AND to resolve any conflicts yourself. The goal is ONE finished, conflict-free integration branch with a CLEAN, LINEAR history the user can merge as-is — NO merge commits, just one well-described raw commit per subtask.
663667
664-
CRITICAL SAFETY RULE: all merges + conflict edits happen INSIDE the dedicated integration worktree at "${INT_WORKTREE}" (checked out to "${INT_BRANCH}"). NEVER run \`git switch\`/\`git checkout\` in the main working tree, and NEVER merge the integration branch into the user's current branch. The user's checkout must end exactly as it started.
668+
CRITICAL SAFETY RULE: all cherry-picks + conflict edits happen INSIDE the dedicated integration worktree at "${INT_WORKTREE}" (checked out to "${INT_BRANCH}"). NEVER run \`git switch\`/\`git checkout\` in the main working tree, and NEVER merge the integration branch into the user's current branch. The user's checkout must end exactly as it started.
665669
666670
Integration branch: "${INT_BRANCH}" (already created off the original HEAD). Integration worktree: "${INT_WORKTREE}".
667671
Subtasks (each wrote changes into its own worktree):
668672
${labelLines}
669673
670674
Do EXACTLY this, in order:
671-
1. For EACH subtask worktree: \`git -C "<worktree>" add -A\`; if there are staged changes, commit them: \`git -C "<worktree>" commit -m "mmt(${SLUG}): <label>"\`. If the worktree has NO changes (\`git -C "<worktree>" status --porcelain\` empty), record the label under "empty" and skip it.
672-
2. From the INTEGRATION WORKTREE only, merge each non-empty subtask branch into "${INT_BRANCH}", one at a time, no-fast-forward (visible merge commit):
673-
\`git -C ${JSON.stringify(INT_WORKTREE)} merge --no-ff -m "mmt(${SLUG}): merge <label>" "<branch>"\`
674-
- If it merges cleanly, record the label under "merged".
675-
- If it CONFLICTS: do NOT abort. RESOLVE it. Inspect the conflicted files in "${INT_WORKTREE}" (\`git -C "${INT_WORKTREE}" diff\`, the \`<<<<<<<\`/\`=======\`/\`>>>>>>>\` markers), read BOTH sides, and edit each conflicted file to a correct COMBINED result that preserves the intent of every subtask (don't just pick one side unless that is genuinely correct; remove ALL conflict markers). Then \`git -C "${INT_WORKTREE}" add -A\` and \`git -C "${INT_WORKTREE}" commit --no-edit\` to complete the merge. Record the label under "resolved" (it IS merged — just needed manual conflict work). If a conflict is genuinely beyond safe resolution (e.g. fundamentally contradictory changes you cannot reconcile without guessing at intent), THEN \`git -C "${INT_WORKTREE}" merge --abort\`, record it under "unresolved", keep that subtask's worktree, and continue.
675+
1. COMMIT each subtask's UNcommitted changes in its OWN worktree with a CLEAN, conventional message — do NOT decide "empty" here (an agent may have already committed its own work, leaving a clean tree but a non-empty branch; emptiness is decided in step 2 by the branch's commit count, never by worktree dirtiness). For EACH subtask worktree: \`git -C "<worktree>" add -A\`; then, ONLY if \`git -C "<worktree>" status --porcelain\` is NON-empty (there are staged changes the agent left uncommitted), commit them as ONE raw commit:
676+
\`git -C "<worktree>" commit -m "<message>"\`
677+
(If the tree is already clean, the agent committed its own work — that's fine; do NOT mark it empty and do NOT skip it, just move on; step 2 will integrate whatever commits the branch carries.)
678+
The <message> MUST be a clean conventional-commit line scoped to the SUBTASK LABEL (NOT the long task slug): \`<type>(<label>): <imperative one-line summary>\` — e.g. \`feat(detail-ui-polish): tighten listing card spacing and hover states\`. Choose <type> from feat/fix/refactor/docs/test/chore by what the subtask actually did; derive the summary from the subtask's stated goal above; keep the subject ≤72 chars, no trailing period. Do NOT use the task slug "${SLUG}" as the scope and do NOT write "merge ..." messages.
679+
2. From the INTEGRATION WORKTREE only, replay each subtask branch's commit(s) onto "${INT_BRANCH}" as RAW commits with NO merge commit, one branch at a time. FIRST decide emptiness by the BRANCH's commit count (NOT worktree dirtiness): compute the base \`B="$(git -C ${JSON.stringify(INT_WORKTREE)} merge-base ${JSON.stringify(INT_BRANCH)} "<branch>")"\` (the original HEAD), then \`N="$(git -C ${JSON.stringify(INT_WORKTREE)} rev-list --count "$B".."<branch>")"\`. If N is 0 the subtask produced no commits — record the label under "empty" and skip to the next branch. Otherwise the branch carries N≥1 commit(s) (usually 1, but a native solver or a fix re-dispatch may leave several — integrate ALL of them, drop none); cherry-pick the whole RANGE so nothing is lost:
680+
\`GIT_EDITOR=true git -C ${JSON.stringify(INT_WORKTREE)} cherry-pick "$B".."<branch>"\` (the range picks ALL of that branch's commits in order, each as its own raw commit — never a merge commit. \`GIT_EDITOR=true\` so nothing opens an editor; you have no TTY.)
681+
- If the whole range applies cleanly, record the label under "merged".
682+
- If git reports "The previous cherry-pick is now empty" / "nothing to commit" for an individual commit (its changes were already applied by an earlier subtask), that commit is a redundant no-op: \`git -C "${INT_WORKTREE}" cherry-pick --skip\` and continue the range. (Do NOT count this as a conflict or as unresolved.)
683+
- If it CONFLICTS: do NOT abort. RESOLVE it. Inspect the conflicted files in "${INT_WORKTREE}" (\`git -C "${INT_WORKTREE}" status\`, \`git -C "${INT_WORKTREE}" diff\`, the \`<<<<<<<\`/\`=======\`/\`>>>>>>>\` markers), read BOTH sides, and edit each conflicted file to a correct COMBINED result that preserves the intent of every subtask (don't just pick one side unless that is genuinely correct; remove ALL conflict markers). Then \`git -C "${INT_WORKTREE}" add -A\` and \`GIT_EDITOR=true git -C "${INT_WORKTREE}" cherry-pick --continue\` (keeps the existing message, never opens an editor) to land it as ONE raw commit with the resolution folded in — NO separate merge/resolution commit — then let the rest of the range continue. Record the label under "resolved". If a conflict is genuinely beyond safe resolution (fundamentally contradictory changes you cannot reconcile without guessing at intent), THEN \`git -C "${INT_WORKTREE}" cherry-pick --abort\` (this rewinds the WHOLE range for that branch back to its pre-pick state), record it under "unresolved", keep that subtask's worktree, and continue with the next subtask.
676684
3. After integrating, sanity-check the integration worktree builds/parses if it's quick and obvious (e.g. \`node --check\` a changed .mjs); note any breakage in the summary. Then remove the cleanly-merged + resolved + empty subtask worktrees (\`git worktree remove --force "<path>"\`). Keep ONLY any "unresolved" worktrees AND the integration worktree "${INT_WORKTREE}". Do NOT delete the integration branch and do NOT touch the user's branch.
677-
4. Report honestly.
685+
4. Report honestly. The final \`git -C "${INT_WORKTREE}" log --oneline\` should show clean per-subtask commits and ZERO "merge" commits.
678686
679-
Return JSON matching the schema: { ok, integration_branch, base_sha, merged:[label], resolved:[label], unresolved:[label], empty:[label], summary, reason? }. "resolved" = merged after you fixed conflicts; "unresolved" = the rare conflict you left for the user. The summary MUST tell the user the integration branch is ready to merge (e.g. "git log ${INT_BRANCH}"), note any "resolved" conflicts you reconciled (so they can review your resolution), and flag any "unresolved" ones.`,
687+
Return JSON matching the schema: { ok, integration_branch, base_sha, merged:[label], resolved:[label], unresolved:[label], empty:[label], summary, reason? }. "merged" = cherry-picked cleanly; "resolved" = cherry-picked after you fixed conflicts (folded into the same raw commit); "unresolved" = the rare conflict you left for the user. The summary MUST tell the user the integration branch is ready to merge (e.g. "git log ${INT_BRANCH}"), note any "resolved" conflicts you reconciled (so they can review your resolution), and flag any "unresolved" ones.`,
680688
{ label: 'integrate-worktrees', phase: 'Integrate', model: tierModel('opus'), schema: INTEGRATE_SCHEMA }
681689
)
682690
const resolved = (integration && Array.isArray(integration.resolved)) ? integration.resolved : []

0 commit comments

Comments
 (0)