# mm-worktree-task ## Purpose Launch a MiniMax M3 implementation sub-session (via `ask-minimax-run`) in an isolated git worktree of the current repo, with the prompt assembled from a planner work item, so multiple implementation briefs can run detached and in parallel without touching the main working tree. ## Usage ``` mm-worktree-task [OPTIONS] ``` ### Arguments | Arg | Description | |-----|-------------| | `` | Full 36-char planner work-item UUID. Required unless `--brief` is given together with `--slug`. | ### Options | Flag | Default | Description | |------|---------|-------------| | `--slug ` | first 8 chars of the uuid | Worktree/branch name suffix. Worktree = `/.claude/worktrees/mm-`, branch = `mm/`. | | `--focus ""` | — | One-line focus appended to the task frame. | | `--brief ` | — | Escape hatch: use this file as the task body instead of (or in addition to) the fetched item. | | `--docs all\|recent\|none` | `recent` | Which item documents to bundle. `recent` = documents updated in the last 14 days; falls back to `all` if the CLI does not expose update dates. The full list of document names is always included in the prompt regardless of which are bundled. | | `--tools ""` | — | Extra `--allowedTools` entries merged into the default allowlist (comma-separated). | | `--timeout ` | `1800` | Max seconds the sub-session may run (`timeout ` wrapper). | | `--context ` | resolved (see §0) | `cp-cli --context` used for every planner call. | | `--upload` | off | After the sub-session exits, upload its report as a document on the item and patch `flow_state` to `impl-complete`. Implemented only in combination with `--collect` (see Behaviour §6) since launch is detached. | | `--base ` | `main` | Branch or commit the worktree is created from, and the ref `--collect` logs/diffs against. Needed when work lives on a long-running branch, or when one agent's branch must build on another's (e.g. an implementation agent starting from a test agent's `mm/` branch). | | `--collect` | off | Don't launch. Print `git log --oneline ..HEAD`, `git diff --stat ...HEAD`, and the report file for an already-run slug's worktree. | | `--dryrun`, `-n` | off | Assemble everything, print the bundle manifest, the full prompt, and the exact command that would run. Create nothing: no worktree, no branch, no launch, no bundle/prompt files left outside the report dir's dryrun preview. | | `--help`, `-h` | — | Show usage and exit 0. | ### Environment | Var | Effect | |-----|--------| | `MM_WORKTREE_TASK_CP_CLI_DIR=` | Checkout holding `lib/cp_cli.py` to run the planner CLI from. Default resolution: this repo if it has `lib/cp_cli.py`, else `~/dev/claude/projects/agent-runtimes`. | | `MM_WORKTREE_TASK_CP_CONTEXT=` | Same as `--context` (the flag wins). | | `MM_WORKTREE_TASK_CPCONFIG=` | cpconfig file used for context auto-resolution (default `~/.cpconfig`). | ### Environment overrides (for testing) | Var | Effect | |-----|--------| | `MM_WORKTREE_TASK_FAKE_ITEM=` | Read item JSON from this file instead of calling `cp_cli planner items get`. | | `MM_WORKTREE_TASK_FAKE_DOCS_DIR=` | Read document list/bodies from this directory instead of calling `cp_cli planner items documents *`. Expected layout: `/list.json` (`{"documents": [{"name": ..., "updated_at": ...}, ...]}`) and `/` per document body. | ## Behaviour 0. **Planner CLI resolution** — the script runs from *any* planner-backed repo; `cp_cli` lives in agent-runtimes. - CLI dir: `MM_WORKTREE_TASK_CP_CLI_DIR`, else `/lib/cp_cli.py` (or the `lib/cp_cli/` package) if present, else `~/dev/claude/projects/agent-runtimes`. Only fails (exit 1, "cannot find lib/cp_cli.py") when a planner call is actually made — fake-item runs and `--brief`-only runs never need it. - Context: `--context`, else `MM_WORKTREE_TASK_CP_CONTEXT`, else the context in `~/.cpconfig` (or `MM_WORKTREE_TASK_CPCONFIG`) whose `project_id` equals the repo directory name, else none (cp-cli's `current_context`). - Every planner call is `cd && python3 -m lib.cp_cli [--context ] …`. `--dryrun` prints both resolved values (`cp-cli from:`, `cp-cli ctx:`). 1. Determine repo root: `git rev-parse --show-toplevel` from cwd. Fail (exit 1) if not inside a git repository. 2. Resolve ``: `--slug` if given, else first 8 chars of `` (or required alongside `--brief` when no uuid is given). 3. Resolve report dir: `${XDG_RUNTIME_DIR:-/tmp}/mm-worktree-task//`. Create it (unless `--dryrun`, which only prints the path). 4. **Fetch the item** (through the §0 wrapper), unless `MM_WORKTREE_TASK_FAKE_ITEM` is set: - `python3 -m lib.cp_cli planner items get --json` → fields used: `id`, `title`, `label`, `state`, `flow_state`, `body`, `tags`, `parent_id`, `related_items`. - `python3 -m lib.cp_cli planner items documents list --json` → document names (+ `updated_at` if present). - For each document selected by `--docs`: `python3 -m lib.cp_cli planner items documents get ` (fake dir equivalent: read `/`). - If `parent_id` is set: fetch the parent item (title + body) via `planner items get --json`. - If `related_items` includes `blocked_by` targets: fetch each target's title + state. - Fetch sibling tasks under the same parent: `planner items list --parent --json` if `parent_id` is set (best-effort; skip silently if the command errors, e.g. flag unsupported for the active project). - Fetching uses the operator's own `cp_cli` auth — the sub-session itself never calls `cp_cli`. 5. Write `/-bundle.md` with `## ` sections: `## Item`, `## Parent` (if any), `## Blocked By` (if any), `## Siblings` (if any), `## Documents` (full name list, marking which are bundled below), then one `## Document: ` section per bundled document with its body. In `--dryrun`, the bundle is still assembled and its manifest (section list + doc names) is printed, but the file is not written to disk — instead its path is printed as the path that would be written. 6. Create the worktree (skipped entirely under `--dryrun` and `--collect`): `git worktree add /.claude/worktrees/mm- -b mm/ ` (`--base`, default `main`). Fail clearly (exit 1) if the branch or worktree directory already exists. 7. Assemble the prompt file `/-prompt.txt`: - Fixed preamble: worktree path and branch; never `cd` elsewhere, never `git push`, never `git stash`, never touch `git config`; spec-driven (a behaviour change requires the matching `spec/*.md` requirement in the same commit); Python 3.12 / Pydantic v2 / `@pytest.mark.asyncio` only; do NOT run the full test suite, only the touched test files; don't fix test infrastructure, mock it; read specs and existing tests before editing, match existing names; keep the diff minimal; commit with `git add -A && git commit -m ": ()"` after verifying `git rev-parse --abbrev-ref HEAD` prints `mm/`; write the report to `/-result.md` with sections Summary / Files changed / Tests run / Decisions / Open questions; one-line stdout ack only. - Task frame: `Implement work item : . Read the bundle at <bundle-path> fully before editing. The item body is the requirement; documents are supporting context. Documents available on the item (not all bundled): <names>.` - `--focus` line, if given, appended after the task frame. - If `--brief <file>` is given, its content is appended (or used standalone as the task body when no uuid was given). 8. Launch detached (skipped under `--dryrun` and `--collect`): ``` cd <worktree> && setsid nohup timeout <secs> ask-minimax-run --model "MiniMax-M3[1m]" \ --allowedTools "<default-allowlist>[,<--tools>]" --permission-mode acceptEdits \ --add-dir <report-dir> -p "$(cat <prompt-file>)" \ > <report-dir>/<slug>-stdout.txt 2>&1 ``` wrapped in a backgrounded subshell that, on exit, appends `<slug> rc=<rc> elapsed=<s>s` to `<report-dir>/launch.log`. The parent process returns immediately. Default allowlist: `Read,Write,Edit,Glob,Grep,Bash(pytest *),Bash(python3 *),Bash(ruff *),Bash(grep *),Bash(ls *),Bash(git add *),Bash(git commit *),Bash(git status*),Bash(git diff*),Bash(git rev-parse*),Bash(git log*)`. 9. Print: report dir, prompt file path, stdout file path, launch.log path, and a one-line `poll with: tail -f <stdout file>` hint. 10. `--collect`: instead of steps 6-9, for the given `<slug>`'s existing worktree, print `git -C <worktree> log --oneline <base>..HEAD`, `git -C <worktree> diff --stat <base>...HEAD`, and the contents of `<report-dir>/<slug>-result.md` if present. If `--upload` is also given and the report file exists: - `python3 -m lib.cp_cli planner items documents put <uuid> mm-<slug>-<YYYY-MM-DD>.md --file <report>` - `python3 -m lib.cp_cli planner items patch <uuid> --flow-state impl-complete --description "mm-worktree-task: <slug> committed on mm/<slug>"` `--upload` without `--collect` is an error (exit 1): "`--upload` requires `--collect` (launch is detached; upload happens after the sub-session has produced a report)". ## Dryrun Behaviour With `--dryrun`/`-n`: - No worktree, branch, bundle file, prompt file, or report-dir directory is created; no `ask-minimax-run` process is started. - Prints, in order: resolved slug/uuid/worktree path/branch, the bundle manifest (section names + document names, bundled vs. available-only), the full assembled prompt text, and the exact `ask-minimax-run` command line that would run (including the resolved allowlist and timeout). - Exit 0. ## Edge Cases | Case | Handling | |------|----------| | Not inside a git repo | Error, exit 1 | | Neither `<item-uuid>` nor (`--brief` + `--slug`) given | Error: `item-uuid is required unless --brief and --slug are both given`, exit 1 | | Worktree dir or branch `mm/<slug>` already exists (non-`--dryrun`, non-`--collect`) | Error, exit 1, no partial state left behind | | `--collect` for a slug with no worktree | Error: `no worktree found for slug '<slug>'`, exit 1 | | `--upload` without `--collect` | Error, exit 1 | | `--docs recent` but CLI gives no `updated_at` | Fall back to `--docs all` behaviour, note this in bundle `## Documents` section | | cp_cli sibling-list command unsupported | Skip `## Siblings` section silently (best-effort) | ## Security - The MiniMax sub-session never calls `cp_cli` and never receives an operator token; only files (bundle, prompt) are passed into the worktree/report dir via `--add-dir`. - The script never reads or prints the MiniMax API key; `ask-minimax-run` handles `provider.env` itself. ## Examples ```bash # Dry run against a real item mm-worktree-task 1f17f61e-317e-4e10-bcd6-bee6a3264379 --slug ci-gate --dryrun # Launch for real, with an extra focus line and extra tool mm-worktree-task 1f17f61e-317e-4e10-bcd6-bee6a3264379 --slug ci-gate \ --focus "Only touch .gitea/workflows and pyproject.toml" \ --tools "Bash(scripts/*)" # Poll progress tail -f "${XDG_RUNTIME_DIR:-/tmp}/mm-worktree-task/agent-runtimes/ci-gate-stdout.txt" # Collect the result once done mm-worktree-task 1f17f61e-317e-4e10-bcd6-bee6a3264379 --slug ci-gate --collect # Collect and upload the report + mark impl-complete mm-worktree-task 1f17f61e-317e-4e10-bcd6-bee6a3264379 --slug ci-gate --collect --upload ```