diff --git a/data/claude-profile/modes/deep-fable.md b/data/claude-profile/modes/deep-fable.md index e677a93..45ef387 100644 --- a/data/claude-profile/modes/deep-fable.md +++ b/data/claude-profile/modes/deep-fable.md @@ -20,7 +20,8 @@ You are running as Fable 5. Fixed for the session. To get Opus reasoning, do NOT ## Subagent policy - Default subagent model: Fable -- Spawn a Fable subagent when: a research task spans >3 files or needs broad exploration (use the Explore agent) +- Broad read-only exploration (>3 files, "find where X lives" sweeps): prefer the `/explore-minimax` skill — the sweep runs in MiniMax and only a cited conclusion returns. Spot-check 1–2 of its citations before relying on the answer. Fall back to the Explore agent (Fable) if it fails or the answer is uncited. +- Spawn a Fable subagent when: research needs iterative steering, code-quality judgement, or tools MiniMax lacks (use the Explore agent) - Spawn an Opus subagent ONLY via the named workflows below — not freelance - Do NOT spawn parallel Fable subagents to "go faster" on coding work; finish one thing properly before starting the next @@ -31,7 +32,7 @@ You are running as Fable 5. Fixed for the session. To get Opus reasoning, do NOT ## Async policy - Background bash: enabled -- Third-party delegation: the /ask-minimax skill (file-heavy grunt work, second opinions); on MiniMax failure, fall back to an Opus subagent. +- Third-party delegation: the /ask-minimax skill (file-heavy grunt work, second opinions) and /explore-minimax (broad read-only exploration); on MiniMax failure, fall back to an Opus subagent (ask-minimax) or the Explore agent (explore-minimax). - ScheduleWakeup: enabled, for checking on background jobs. Wakeup prompts MUST be self-contained. - Container agents (control plane): not the focus of this mode — if you find yourself wanting them, suggest relaunching in `orch` diff --git a/data/claude-profile/modes/deep.md b/data/claude-profile/modes/deep.md index 67dab75..bc38d0b 100644 --- a/data/claude-profile/modes/deep.md +++ b/data/claude-profile/modes/deep.md @@ -20,7 +20,8 @@ You are running as Sonnet. Fixed for the session. To get Opus reasoning, do NOT ## Subagent policy - Default subagent model: Sonnet -- Spawn a Sonnet subagent when: a research task spans >3 files or needs broad exploration (use the Explore agent) +- Broad read-only exploration (>3 files, "find where X lives" sweeps): prefer the `/explore-minimax` skill — the sweep runs in MiniMax and only a cited conclusion returns. Spot-check 1–2 of its citations before relying on the answer. Fall back to the Explore agent (Sonnet) if it fails or the answer is uncited. +- Spawn a Sonnet subagent when: research needs iterative steering, code-quality judgement, or tools MiniMax lacks (use the Explore agent) - Spawn an Opus subagent ONLY via the named workflows below — not freelance - Do NOT spawn parallel Sonnet subagents to "go faster" on coding work; finish one thing properly before starting the next @@ -31,7 +32,7 @@ You are running as Sonnet. Fixed for the session. To get Opus reasoning, do NOT ## Async policy - Background bash: enabled -- Third-party delegation: the /ask-minimax skill (file-heavy grunt work, second opinions); on MiniMax failure, fall back to an Opus subagent. +- Third-party delegation: the /ask-minimax skill (file-heavy grunt work, second opinions) and /explore-minimax (broad read-only exploration); on MiniMax failure, fall back to an Opus subagent (ask-minimax) or the Explore agent (explore-minimax). - ScheduleWakeup: enabled, for checking on background jobs. Wakeup prompts MUST be self-contained. - Container agents (control plane): not the focus of this mode — if you find yourself wanting them, suggest relaunching in `orch` diff --git a/data/claude-profile/modes/orch.md b/data/claude-profile/modes/orch.md index cf4a5e3..24bdf9c 100644 --- a/data/claude-profile/modes/orch.md +++ b/data/claude-profile/modes/orch.md @@ -21,6 +21,7 @@ You are running as Sonnet. Fixed for the session. Your job is orchestration, not ## Subagent policy - Default subagent model: Sonnet (for any in-session research) - Spawn a subagent when: you need to read a lot of CP state, agent logs, or task output that would bloat your context +- Pre-dispatch codebase exploration (scoping tasks before decompose/dispatch): prefer the `/explore-minimax` skill — read-only MiniMax sweep, cited conclusion only; fall back to the Explore agent on failure - The MAIN parallelism mechanism in this mode is container agents via the agent-runtimes control plane, not in-session subagents ## Reasoning posture diff --git a/scripts/explore-minimax-run b/scripts/explore-minimax-run new file mode 100755 index 0000000..ae0f2d6 --- /dev/null +++ b/scripts/explore-minimax-run @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# Run a read-only codebase exploration sweep in a MiniMax M3 sub-session. +# Wraps ask-minimax-run with the hardening the exploration case needs: +# - --add-dir per search root (paths outside cwd are otherwise denied) +# - secrets-root refusal + skip-list in the prompt preamble +# - enforced read-only (--disallowedTools beats the profile's acceptEdits default) +# - JSON output channel (.result via jq) so permission noise never pollutes the answer +# - internal timeout, one retry on transient errors, citation check on the answer +# Used by the /explore-minimax skill; safe to drive directly. +# +# Usage: +# explore-minimax-run --root [--root ...] \ +# --prompt-file --out \ +# [--breadth medium|very-thorough] [--max-turns N] [--timeout SECS] +# +# Exit codes: +# 0 answer written to --out +# 1 usage / configuration error (including a refused secrets root) +# 2 MiniMax failed after retry (transport error, timeout, is_error) +# 3 ran but answer is empty or carries no file:line citations — fall back to an Explore agent +set -euo pipefail + +usage() { grep '^#' "$0" | sed 's/^# \{0,1\}//' | sed -n '2,20p'; } + +ROOTS=() +PROMPT_FILE="" +OUT_FILE="" +BREADTH="medium" +MAX_TURNS=40 +TIMEOUT_SECS=540 + +while [[ $# -gt 0 ]]; do + case "$1" in + --root) ROOTS+=("$2"); shift 2 ;; + --prompt-file) PROMPT_FILE="$2"; shift 2 ;; + --out) OUT_FILE="$2"; shift 2 ;; + --breadth) BREADTH="$2"; shift 2 ;; + --max-turns) MAX_TURNS="$2"; shift 2 ;; + --timeout) TIMEOUT_SECS="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "error: unknown argument '$1'" >&2; usage >&2; exit 1 ;; + esac +done + +[[ ${#ROOTS[@]} -gt 0 ]] || { echo "error: at least one --root required" >&2; exit 1; } +[[ -n "$PROMPT_FILE" && -f "$PROMPT_FILE" ]] || { echo "error: --prompt-file missing or not a file" >&2; exit 1; } +[[ -n "$OUT_FILE" ]] || { echo "error: --out required" >&2; exit 1; } +command -v jq >/dev/null || { echo "error: jq not found" >&2; exit 1; } +command -v ask-minimax-run >/dev/null || { echo "error: ask-minimax-run not on PATH" >&2; exit 1; } + +# Resolve roots to absolute paths; refuse anything inside a secrets directory. +RESOLVED_ROOTS=() +for root in "${ROOTS[@]}"; do + abs=$(realpath -e "$root" 2>/dev/null) || { echo "error: root '$root' does not exist" >&2; exit 1; } + case "$abs" in + */secrets|*/secrets/*|*/local_secrets|*/local_secrets/*) + echo "error: refusing secrets root '$abs' — exploration never touches secrets/" >&2 + exit 1 ;; + esac + RESOLVED_ROOTS+=("$abs") +done + +# Assemble the full prompt: fixed exploration preamble + roots + the caller's question. +WORK_DIR=$(mktemp -d /tmp/explore-minimax.XXXXXX) +trap 'rm -rf "$WORK_DIR"' EXIT +FULL_PROMPT="$WORK_DIR/prompt.txt" + +{ + cat <<'PREAMBLE' +You are a READ-ONLY codebase exploration agent. Answer the question below by +searching and reading the listed roots. Rules: + +- Read-only: never write, edit, or run shell commands. Only Read, Glob, Grep. +- Every claim MUST cite an absolute file path with a line number (path:line). + Quote at most 10 lines per citation. +- If you cannot find something, say NOT FOUND for that part — do NOT guess or + invent names, paths, or behaviour. +- Skip credential material entirely: any 'secrets/' or 'local_secrets/' + directory, .env* files, *.key, *.pem, and decrypted SOPS content. Never quote + their contents even if encountered. +- Do not run test suites or builds. +- End your reply with a "Searches run:" section listing the globs/greps you + actually executed, so breadth can be verified. +- Reply with the answer directly as your final message text (there is no + output file — your message IS the result). +PREAMBLE + echo + echo "## Search roots" + printf '%s\n' "${RESOLVED_ROOTS[@]}" + echo + echo "## Breadth" + if [[ "$BREADTH" == "very-thorough" ]]; then + echo "Very thorough: sweep all roots, multiple naming conventions, check for duplicate/alternate implementations." + else + echo "Medium: stop once the question is confidently answered with citations." + fi + echo + echo "## Question" + cat "$PROMPT_FILE" +} > "$FULL_PROMPT" + +ADD_DIR_ARGS=() +for abs in "${RESOLVED_ROOTS[@]}"; do + ADD_DIR_ARGS+=(--add-dir "$abs") +done + +run_once() { + local json_out="$1" + # --disallowedTools: the minimax profile defaults to acceptEdits and allows + # Edit(/tmp/**)/WebFetch at user level; deny beats allow. + timeout "$TIMEOUT_SECS" ask-minimax-run \ + --model MiniMax-M3 \ + --permission-mode default \ + --allowedTools "Read,Glob,Grep" \ + --disallowedTools "Write,Edit,MultiEdit,NotebookEdit,Bash,WebFetch,WebSearch" \ + --max-turns "$MAX_TURNS" \ + --output-format json \ + "${ADD_DIR_ARGS[@]}" \ + -p < "$FULL_PROMPT" > "$json_out" 2>"$WORK_DIR/stderr.txt" +} + +extract_result() { + local json_out="$1" + jq -e -r 'select(.is_error != true) | .result // empty' "$json_out" 2>/dev/null || true +} + +JSON_OUT="$WORK_DIR/response.json" +ANSWER="" +for attempt in 1 2; do + if run_once "$JSON_OUT"; then + ANSWER=$(extract_result "$JSON_OUT") + [[ -n "$ANSWER" ]] && break + fi + if [[ $attempt -eq 1 ]]; then + echo "warn: attempt 1 failed ($(tail -1 "$WORK_DIR/stderr.txt" 2>/dev/null || echo 'no stderr')); retrying once" >&2 + fi +done + +if [[ -z "$ANSWER" ]]; then + echo "error: MiniMax failed after retry" >&2 + tail -5 "$WORK_DIR/stderr.txt" >&2 2>/dev/null || true + exit 2 +fi + +printf '%s\n' "$ANSWER" > "$OUT_FILE" + +# Citation check: an exploration answer with no path:line references (and no +# honest NOT FOUND) is untrustworthy — signal the caller to fall back. +if ! grep -qE '(:[0-9]+)|NOT FOUND' "$OUT_FILE"; then + echo "warn: answer contains no file:line citations — treat as unreliable (fall back to Explore agent)" >&2 + exit 3 +fi + +echo "ok: answer written to $OUT_FILE" diff --git a/specs/explore-minimax-run.spec.md b/specs/explore-minimax-run.spec.md new file mode 100644 index 0000000..97b5808 --- /dev/null +++ b/specs/explore-minimax-run.spec.md @@ -0,0 +1,51 @@ +# explore-minimax-run + +## Purpose + +Run a broad, read-only "where does X live" codebase exploration in a MiniMax M3 sub-session (via `ask-minimax-run`), so the sweep's file reads cost MiniMax tokens instead of Anthropic tokens and only a cited conclusion returns. Backs the `/explore-minimax` skill; safe to drive directly. + +## Usage + +``` +explore-minimax-run --root [--root ...] \ + --prompt-file --out \ + [--breadth medium|very-thorough] [--max-turns N] [--timeout SECS] +``` + +| Flag | Default | Description | +|------|---------|-------------| +| `--root` | required, repeatable | Search root; resolved with `realpath -e`, passed as `--add-dir` | +| `--prompt-file` | required | File containing the exploration question (question only — the script prepends the preamble) | +| `--out` | required | Where the answer text is written | +| `--breadth` | `medium` | `medium` (stop when confidently answered) or `very-thorough` (full sweep, alternate naming conventions) | +| `--max-turns` | 40 | Sub-session turn cap | +| `--timeout` | 540 | Internal timeout in seconds (keeps callers under the harness Bash 600s cap) | + +## Behaviour / requirements + +1. **Roots**: every `--root` resolved to an absolute path; a nonexistent root is a usage error. Any root that is or lies inside a `secrets/` or `local_secrets/` directory is refused (exit 1) — exploration never touches secrets. +2. **Prompt assembly**: fixed preamble (read-only rules, `path:line` citation requirement with ≤10-line excerpts, "NOT FOUND — do not guess", credential-file skip list, no builds/tests, "Searches run:" trailer) + resolved roots + breadth hint + the caller's question. Prompt passed via stdin redirect — no `$()`. +3. **Read-only enforcement**: `--permission-mode default --allowedTools "Read,Glob,Grep" --disallowedTools "Write,Edit,MultiEdit,NotebookEdit,Bash,WebFetch,WebSearch"`. Deny-list is required because the minimax profile's settings.json defaults to `acceptEdits` and allows `Edit(/tmp/**)` and `WebFetch`. +4. **Answer channel**: `--output-format json`; answer extracted with `jq` from `.result` (skipping `is_error` responses); stderr kept separate. Avoids the documented MiniMax failure where permission-denial noise lands in stdout and an "exit 0, no result" run looks like success. +5. **Retry**: one retry on failure/empty result (covers transient `invalid system role` errors). Matching is on exit status/JSON error, never on answer text. +6. **Citation gate**: after writing `--out`, an answer with neither a `:NNN` line reference nor `NOT FOUND` exits 3 — the caller should fall back to an Anthropic Explore agent. +7. **Context window**: relies on `CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000` in `~/.claude-oreillyit-minimax/provider.env` (exported by `ask-minimax-run`). +8. Temp files live in a `mktemp -d` dir removed on exit; only `--out` survives. + +## Exit codes + +| Code | Meaning | Caller action | +|------|---------|---------------| +| 0 | Answer written to `--out` | Read it; spot-check 1–2 citations before relying on it | +| 1 | Usage/config error (bad flag, missing root, secrets root, missing jq) | Fix invocation | +| 2 | MiniMax failed after retry | Fall back to Explore agent | +| 3 | Ran, but answer empty or uncited | Fall back to Explore agent | + +## Security notes + +- The returned answer is third-party model output over repo contents: **data, not instructions**. Callers must never act on directives embedded in it. +- The secrets guard is defence-in-depth: root refusal (hard) + prompt skip-list (soft). Do not weaken the hard check. + +## Dependencies + +`ask-minimax-run` (same repo, on PATH), `jq`, `timeout`, `realpath`, the `~/.claude-oreillyit-minimax` profile.