cp_cli lives in agent-runtimes; previously every planner call ran 'python3 -m lib.cp_cli' from the current repo with no --context, so the script only worked inside agent-runtimes. The context now resolves from ~/.cpconfig by matching project_id to the repo directory name. Verified live from hatchmate (item + parent + documents fetched via the hatchmate context). Claude-Session: https://claude.ai/code/session_01JgJbWzVJioVznVTFyQFEfx
133 lines
11 KiB
Markdown
133 lines
11 KiB
Markdown
# 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. |
|
|
| `--collect` | off | Don't launch. Print `git log --oneline main..HEAD`, `git diff --stat main...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
|
|
|
|
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 `<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:`).
|
|
1. Determine repo root: `git rev-parse --show-toplevel` from cwd. Fail (exit 1) if not inside a git repository.
|
|
2. Resolve `<slug>`: `--slug` if given, else first 8 chars of `<item-uuid>` (or required alongside `--brief` when no uuid is given).
|
|
3. Resolve report dir: `${XDG_RUNTIME_DIR:-/tmp}/mm-worktree-task/<repo-name>/`. 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 <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`.
|
|
5. 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.
|
|
6. Create the worktree (skipped entirely under `--dryrun` and `--collect`): `git worktree add <repo>/.claude/worktrees/mm-<slug> -b mm/<slug> main`. Fail clearly (exit 1) if the branch or worktree directory already exists.
|
|
7. 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).
|
|
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 main..HEAD`, `git -C <worktree> diff --stat main...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
|
|
```
|