Files
small-scripts/specs/mm-worktree-task.spec.md
Paul O'Reilly cbab812917 mm-worktree-task: --base flag + set -e safe returns (uncommitted WIP)
Committing work found uncommitted in the checkout: --base lets a unit be based
on a branch other than main (used throughout the M32 cut-over session for
u6-tests and hatchmate-migration bases), and the bare 'return' statements
become 'return 0' so they cannot abort under set -e when the preceding test is
false.

Per the project agent-safety rule: untracked/uncommitted work that matters gets
committed the day it is found, so local loss cannot take it.

Claude-Session: https://claude.ai/code/session_014KNyfHmHpMbdMrHZXDRkAp
2026-09-23 19:26:52 +12:00

11 KiB

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 <item-uuid> [OPTIONS]

Arguments

Arg Description
<item-uuid> Full 36-char planner work-item UUID. Required unless --brief is given together with --slug.

Options

Flag Default Description
--slug <slug> first 8 chars of the uuid Worktree/branch name suffix. Worktree = <repo>/.claude/worktrees/mm-<slug>, branch = mm/<slug>.
--focus "<line>" — One-line focus appended to the task frame.
--brief <file> — 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 "<list>" — Extra --allowedTools entries merged into the default allowlist (comma-separated).
--timeout <secs> 1800 Max seconds the sub-session may run (timeout <secs> wrapper).
--context <name> 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 <ref> 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/<slug> branch).
--collect off Don't launch. Print git log --oneline <base>..HEAD, git diff --stat <base>...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=<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=<name> Same as --context (the flag wins).
MM_WORKTREE_TASK_CPCONFIG=<file> cpconfig file used for context auto-resolution (default ~/.cpconfig).

Environment overrides (for testing)

Var Effect
MM_WORKTREE_TASK_FAKE_ITEM=<file> Read item JSON from this file instead of calling cp_cli planner items get.
MM_WORKTREE_TASK_FAKE_DOCS_DIR=<dir> Read document list/bodies from this directory instead of calling cp_cli planner items documents *. Expected layout: <dir>/list.json ({"documents": [{"name": ..., "updated_at": ...}, ...]}) and <dir>/<name> per document body.

Behaviour

  1. 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 <repo>/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 <cli-dir> && python3 -m lib.cp_cli [--context <ctx>] …. --dryrun prints both resolved values (cp-cli from:, cp-cli ctx:).
  2. Determine repo root: git rev-parse --show-toplevel from cwd. Fail (exit 1) if not inside a git repository.
  3. Resolve <slug>: --slug if given, else first 8 chars of <item-uuid> (or required alongside --brief when no uuid is given).
  4. Resolve report dir: ${XDG_RUNTIME_DIR:-/tmp}/mm-worktree-task/<repo-name>/. Create it (unless --dryrun, which only prints the path).
  5. Fetch the item (through the §0 wrapper), unless MM_WORKTREE_TASK_FAKE_ITEM is set:
    • python3 -m lib.cp_cli planner items get <uuid> --json → fields used: id, title, label, state, flow_state, body, tags, parent_id, related_items.
    • python3 -m lib.cp_cli planner items documents list <uuid> --json → document names (+ updated_at if present).
    • For each document selected by --docs: python3 -m lib.cp_cli planner items documents get <uuid> <name> (fake dir equivalent: read <dir>/<name>).
    • If parent_id is set: fetch the parent item (title + body) via planner items get <parent_id> --json.
    • If related_items includes blocked_by targets: fetch each target's title + state.
    • Fetch sibling tasks under the same parent: planner items list <project> --parent <parent_id> --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.
  6. Write <report-dir>/<slug>-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: <name> 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.
  7. Create the worktree (skipped entirely under --dryrun and --collect): git worktree add <repo>/.claude/worktrees/mm-<slug> -b mm/<slug> <base> (--base, default main). Fail clearly (exit 1) if the branch or worktree directory already exists.
  8. Assemble the prompt file <report-dir>/<slug>-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 "<type>: <summary> (<uuid>)" after verifying git rev-parse --abbrev-ref HEAD prints mm/<slug>; write the report to <report-dir>/<slug>-result.md with sections Summary / Files changed / Tests run / Decisions / Open questions; one-line stdout ack only.
    • Task frame: Implement work item <uuid>: <title>. 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).
  9. 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*).
  10. Print: report dir, prompt file path, stdout file path, launch.log path, and a one-line poll with: tail -f <stdout file> hint.
  11. --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

# 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