#!/usr/bin/env bash # Run a read-only codebase exploration sweep in a MiniMax 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] \ # [--m3] [--effort low|medium|high|xhigh|max] # # 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,21p'; } ROOTS=() PROMPT_FILE="" OUT_FILE="" BREADTH="medium" MAX_TURNS=40 TIMEOUT_SECS=540 USE_M3=0 EFFORT_ARG="" 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 ;; --m3) USE_M3=1; shift ;; --effort) EFFORT_ARG="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "error: unknown argument '$1'" >&2; usage >&2; exit 1 ;; esac done if [[ $USE_M3 -eq 1 ]]; then MODEL="MiniMax-M3"; EFFORT="$EFFORT_ARG"; else MODEL="MiniMax-M3.1-Flash"; EFFORT="${EFFORT_ARG:-medium}"; fi [[ -z "$EFFORT" || "$EFFORT" =~ ^(low|medium|high|xhigh|max)$ ]] || { echo "error: --effort must be low|medium|high|xhigh|max" >&2; exit 1; } [[ ${#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 "$MODEL" ${EFFORT:+--effort "$EFFORT"} \ --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"