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.
219 lines
9.3 KiB
Bash
Executable File
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"
|