Files
small-scripts/scripts/ci-triage-minimax-run
Paul O'Reilly 840d7509e1 mm scripts: default to MiniMax-M3.1-Flash-Preview with per-script effort
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.
2026-10-13 10:23:53 +13:00

219 lines
9.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Triage a failing Gitea Actions run in a MiniMax sub-session.
# Fetches logs via gitea-agent-companion (token stays inside gac), redacts
# credential-shaped content, and has MiniMax write a structured triage file.
# Spec: specs/ci-triage-minimax-run.spec.md. Used by /ci-triage-minimax.
#
# Usage:
# ci-triage-minimax-run --repo <owner/repo> --run <N> [--job <name|id>]
# [--out <triage-file>] [--tail N] [--max-turns N] [--timeout SECS]
# [--dryrun|-n] [--m3] [--effort low|medium|high|xhigh|max]
#
# Exit codes:
# 0 triage file written
# 1 usage/config error (bad flags, gac usage error)
# 2 gac auth/API/not-found error, MiniMax failed after retry, or timeout
# 3 triage file missing or failed the result gate after retry
set -euo pipefail
usage() { grep '^#' "$0" | sed 's/^# \{0,1\}//' | sed -n '2,16p'; }
REPO=""
RUN=""
JOB=""
OUT_FILE=""
TAIL_LINES=2000
MAX_TURNS=25
TIMEOUT_SECS=300
USE_M3=0
EFFORT_ARG=""
DRYRUN=0
CRED_PATTERN='ghp_[A-Za-z0-9]|glpat-|x-access-token:|Authorization:|BEGIN [A-Z ]*PRIVATE KEY|AKIA[0-9A-Z]{16}'
while [[ $# -gt 0 ]]; do
case "$1" in
--repo) REPO="$2"; shift 2 ;;
--run) RUN="$2"; shift 2 ;;
--job) JOB="$2"; shift 2 ;;
--out) OUT_FILE="$2"; shift 2 ;;
--tail) TAIL_LINES="$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 ;;
--dryrun|-n) DRYRUN=1; shift ;;
-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[1m]"; EFFORT="$EFFORT_ARG"; else MODEL="MiniMax-M3.1-Flash-Preview"; EFFORT="${EFFORT_ARG:-low}"; fi
[[ -z "$EFFORT" || "$EFFORT" =~ ^(low|medium|high|xhigh|max)$ ]] || { echo "error: --effort must be low|medium|high|xhigh|max" >&2; exit 1; }
# --- CT-1 preflight -----------------------------------------------------------
[[ "$REPO" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { echo "error: --repo must be owner/repo" >&2; exit 1; }
[[ "$RUN" =~ ^[0-9]+$ ]] || { echo "error: --run must be a positive integer" >&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; }
command -v gitea-agent-companion >/dev/null || { echo "error: gitea-agent-companion not on PATH" >&2; exit 1; }
# gac (as of v0.0.2.10) does not fall back to the secrets path itself; point it
# at the well-known token file when the caller set no token env. Path only —
# the token content is never read or printed here.
if [[ -z "${GITEA_TOKEN:-}" && -z "${GITEA_TOKEN_FILE:-}" && -f "$HOME/dev/claude/secrets/gitea/ai_admin/token" ]]; then
export GITEA_TOKEN_FILE="$HOME/dev/claude/secrets/gitea/ai_admin/token"
fi
if [[ -z "$OUT_FILE" ]]; then
OUT_FILE="${XDG_RUNTIME_DIR:-/tmp}/ci-triage-minimax/${REPO//\//-}/run-$RUN-triage.md"
fi
OUT_DIR=$(dirname "$OUT_FILE")
mkdir -p "$OUT_DIR"
WORK_DIR=$(mktemp -d /tmp/ci-triage-minimax.XXXXXX)
trap 'rm -rf "$WORK_DIR"' EXIT
JOB_ARGS=()
[[ -n "$JOB" ]] && JOB_ARGS=(--job "$JOB")
GAC_CMD=(gitea-agent-companion inspect "$REPO" --run "$RUN" "${JOB_ARGS[@]}" --logs --json --tail "$TAIL_LINES")
# --- dryrun (before any fetch) ------------------------------------------------
if [[ "$DRYRUN" -eq 1 ]]; then
echo "== ci-triage-minimax-run --dryrun"
echo "repo: $REPO"
echo "run: $RUN job: ${JOB:-auto}"
echo "out: $OUT_FILE"
echo "tail: $TAIL_LINES max turns: $MAX_TURNS timeout: ${TIMEOUT_SECS}s"
echo "-- gac fetch command --"
echo "${GAC_CMD[*]} > run-logs.ndjson"
echo "-- failing step --"
echo "<run-logs not fetched in dryrun>"
echo "-- sub-session command --"
echo "cd <workdir> && timeout $TIMEOUT_SECS ask-minimax-run --model '$MODEL'${EFFORT:+ --effort $EFFORT}" \
"--permission-mode default --allowedTools 'Read,Glob,Grep,Write'" \
"--disallowedTools 'Edit,MultiEdit,NotebookEdit,Bash,WebFetch,WebSearch'" \
"--max-turns $MAX_TURNS --output-format json --add-dir $OUT_DIR -p < prompt.txt"
exit 0
fi
# --- CT-2/CT-3 fetch with exit mapping ----------------------------------------
if [[ -n "${CI_TRIAGE_FAKE_LOGS:-}" ]]; then # CT-10 test seam
cp "$CI_TRIAGE_FAKE_LOGS" "$WORK_DIR/run-logs.ndjson"
echo "(fake logs — no job table)" > "$WORK_DIR/jobs.txt"
else
rc=0
"${GAC_CMD[@]}" > "$WORK_DIR/run-logs.ndjson" 2>"$WORK_DIR/gac-stderr.txt" || rc=$?
if [[ "$rc" -ne 0 ]]; then
cat "$WORK_DIR/gac-stderr.txt" >&2 || true
case "$rc" in
2) echo "error: gac usage error (see above)" >&2; exit 1 ;;
*) echo "error: gac failed rc=$rc (auth/API/run-not-found — possibly transient)" >&2; exit 2 ;;
esac
fi
gitea-agent-companion inspect "$REPO" --run "$RUN" > "$WORK_DIR/jobs.txt" 2>/dev/null || true
fi
# --- CT-4 redaction (in place; the log IS the payload) --------------------------
sed -E -i "s#($CRED_PATTERN)[^\"[:space:]]*#[REDACTED]#g" "$WORK_DIR/run-logs.ndjson"
# --- CT-5 envelope parse --------------------------------------------------------
FAILED_STEP=$(jq -r 'select(.failed_step != null) | .failed_step' "$WORK_DIR/run-logs.ndjson" 2>/dev/null | head -1)
SLICED=$(jq -r '.sliced // empty' "$WORK_DIR/run-logs.ndjson" 2>/dev/null | head -1)
# --- CT-6 prompt ----------------------------------------------------------------
FULL_PROMPT="$WORK_DIR/prompt.txt"
{
cat <<'PREAMBLE'
You are a CI FAILURE TRIAGE agent running non-interactively. Analyse the CI
run logs provided as files in your working directory. Rules:
- Inputs (in cwd): run-logs.ndjson (log lines + metadata envelope), jobs.txt.
- Write ONLY the triage file at the path given below. Modify nothing else.
- Do not run anything. Do not suggest running full test suites or builds.
- Search the log lines for these error signatures specifically:
error:|FAILED|Traceback|exit code|denied|timeout|not found|no such|ECONN|refused
- Quote log lines verbatim. Do NOT invent step names, file paths, or causes
you cannot point to in the log. If the log is truncated or step boundaries
are partial, say so — do not improvise.
Triage file format — exactly these sections:
## Failing step
## Error excerpt (<=10 verbatim log lines)
## Probable cause (1-3 sentences, must not be empty)
## Suggested next action (ONE command or one edit description; advisory only)
## Confidence (high|medium|low, one line why)
PREAMBLE
echo
echo "## Run metadata"
echo "repo: $REPO run: $RUN"
if [[ -n "$FAILED_STEP" ]]; then
echo "The CI system identified the failing step as: $FAILED_STEP"
echo "Confirm against the log; analyse WHY it failed."
else
echo "The CI system could NOT identify a failing step (failed_step is null:"
echo "cancelled/crashed run or no failed job). You MUST write the literal"
echo "word UNKNOWN under '## Failing step' unless the log itself proves one."
fi
[[ "$SLICED" == "true" ]] && echo "Note: the log window was sliced to the failure region."
echo
echo "## Triage file to write"
echo "$OUT_FILE"
} > "$FULL_PROMPT"
# --- CT-7 invocation ------------------------------------------------------------
run_once() {
local json_out="$1"
if [[ -n "${CI_TRIAGE_FAKE_JSON:-}" ]]; then # CT-10 test seam
cp "$CI_TRIAGE_FAKE_JSON" "$json_out"
# optional companion seam: plays the sub-session's Write of the triage file
[[ -n "${CI_TRIAGE_FAKE_TRIAGE:-}" ]] && cp "$CI_TRIAGE_FAKE_TRIAGE" "$OUT_FILE"
return 0
fi
( cd "$WORK_DIR" && timeout "$TIMEOUT_SECS" ask-minimax-run \
--model "$MODEL" ${EFFORT:+--effort "$EFFORT"} \
--permission-mode default \
--allowedTools "Read,Glob,Grep,Write" \
--disallowedTools "Edit,MultiEdit,NotebookEdit,Bash,WebFetch,WebSearch" \
--max-turns "$MAX_TURNS" \
--output-format json \
--add-dir "$OUT_DIR" \
-p < "$FULL_PROMPT" > "$json_out" 2>"$WORK_DIR/stderr.txt" )
}
gate_ok() { # CT-8
[[ -f "$OUT_FILE" ]] || return 1
grep -q '^## Failing step' "$OUT_FILE" || return 1
# non-blank content line under "## Probable cause"
awk '/^## Probable cause/{f=1; next} /^## /{f=0} f && NF {found=1} END{exit !found}' "$OUT_FILE"
}
JSON_OUT="$WORK_DIR/response.json"
RUN_FAILED=0
for attempt in 1 2; do
rm -f "$OUT_FILE" # CT-9: no stale attempt-1 file may satisfy the gate
RUN_FAILED=0
rc=0
run_once "$JSON_OUT" || rc=$?
if [[ "$rc" -eq 124 ]]; then
echo "error: sub-session timed out after ${TIMEOUT_SECS}s (not retried)" >&2
exit 2
fi
[[ "$rc" -ne 0 ]] && RUN_FAILED=1
jq -e 'select(.is_error == true)' "$JSON_OUT" >/dev/null 2>&1 && RUN_FAILED=1
[[ "$RUN_FAILED" -eq 0 ]] && gate_ok && break
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 [[ "$RUN_FAILED" -eq 1 ]]; then
echo "error: MiniMax failed after retry" >&2
tail -5 "$WORK_DIR/stderr.txt" >&2 2>/dev/null || true
exit 2
fi
if ! gate_ok; then
echo "error: triage file missing or failed the result gate after retry" >&2
exit 3
fi
echo "ok: triage written to $OUT_FILE"