Flash is the default model; --m3 falls back to M3, --effort overrides. Defaults: max (review-minimax, review-loop, mm-worktree-task), medium (explore), low (ci-triage). ask-minimax-run applies MINIMAX_EFFORT to Flash models when --effort is not passed. Bump stale fixture date.
162 lines
6.3 KiB
Bash
Executable File
162 lines
6.3 KiB
Bash
Executable File
#!/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 <path> [--root <path> ...] \
|
|
# --prompt-file <question-file> --out <result-file> \
|
|
# [--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-Preview"; 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"
|