#!/usr/bin/env bash
# Ralph-style audit loop: for each model, for each scope task, run a FRESH
# auditor container (clean context) and capture its findings. Sequential by
# design — concurrent opencode runs in one project deadlock, and it respects
# provider rate limits.
#
# Usage:
# OUT=
./run-audit.sh # default models, all tasks
# OUT= MODELS="deepseek/deepseek-v4-pro" ./run-audit.sh
# OUT= ./run-audit.sh 02-authorization 05-csp-headers # only these tasks
# OUT= TIMEOUT=1800 ./run-audit.sh
# OUT= VARIANT=high ./run-audit.sh # reasoning effort; default max, "" to omit
#
# OUT names the directory that receives findings. Keep it outside this one:
# this directory is tracked, and a run's output is not.
set -euo pipefail
cd "$(dirname "$0")"
PROJECT=member-console-audit
: "${OUT:?set OUT to the directory that receives findings}"
mkdir -p "$OUT"; OUT="$(cd "$OUT" && pwd)"; export OUT
# Reverify current IDs with `opencode models`; these drift. Gemini is in the
# default set
# (google/gemini-3.8-flash); its key is in the mounted auth.json and its host
# is allowlisted for egress. Override the set with MODELS="..." ./run-audit.sh.
DEFAULT_MODELS="deepseek/deepseek-v4-pro google/gemini-3.8-flash zai-coding-plan/glm-5.3 kimi-for-coding/k3-256k"
MODELS="${MODELS:-$DEFAULT_MODELS}"
# 1200s was too tight for Kimi at --variant max: it timed out mid-investigation
# on the larger slices (2026-09-08). Thorough models need real headroom.
TIMEOUT="${TIMEOUT:-2700}"
# Provider-specific reasoning effort. "max" asks each model for its deepest
# reasoning; set VARIANT="" to omit the flag if a provider rejects the value.
VARIANT="${VARIANT:-max}"
VARIANT_FLAG=""
[[ -n "$VARIANT" ]] && VARIANT_FLAG="--variant '$VARIANT'"
if [[ ! -d .code ]]; then echo "Run ./prepare.sh first." >&2; exit 1; fi
docker compose up -d egress-proxy >/dev/null
# The scanner pass is deterministic and cheap; run it once if it has not been.
if [[ ! -d "$OUT/tools" ]]; then
echo "==> No scanner output yet; running ./run-tools.sh first"
./run-tools.sh
fi
# Which tasks: args override; else every scope file.
if [[ $# -gt 0 ]]; then
TASKS=("$@")
else
mapfile -t TASKS < <(cd scope && ls *.md | sed 's/\.md$//')
fi
RUN_ID="$(date +%Y-%m-%d_%H%M%S)"
RUN_DIR="$OUT/$RUN_ID"
mkdir -p "$RUN_DIR"
{
echo "run: $RUN_ID"
echo "commit: $(cat .code/AUDIT_COMMIT.txt 2>/dev/null || echo '?')"
echo "models: $MODELS"
echo "variant: ${VARIANT:-}"
echo "tasks: ${TASKS[*]}"
echo "timeout-per-iteration: ${TIMEOUT}s"
echo "execution: ${SERIAL:+serial}${SERIAL:-parallel by model ($(echo $MODELS | wc -w) lanes)}"
} | tee "$RUN_DIR/manifest.txt"
# One lane per model: the models run concurrently, but within a lane the
# slices run serially, so each provider/key sees one request at a time (no
# rate-limit stacking). Set SERIAL=1 to fall back to a single sequential lane.
# Progress interleaves across lanes; each lane also logs to $outdir/_lane.log.
run_lane() {
local model="$1"
local safe_model="${model//\//__}"
local outdir="$RUN_DIR/$safe_model"; mkdir -p "$outdir"
local lane_log="$outdir/_lane.log"
local task out err
for task in "${TASKS[@]}"; do
task="${task%.md}"
if [[ ! -f "scope/$task.md" ]]; then echo " ! [$model] no scope/$task.md, skipping"; continue; fi
out="$outdir/$task.md"; err="$outdir/$task.err"
echo "==> [$model] $task (started $(date +%H:%M:%S))"
# Fresh --rm container. --no-deps: don't restart the proxy. -T: no TTY.
# Prompt = auditor instructions + this task. stdin closed (headless quirk).
if timeout "$TIMEOUT" docker compose run --rm --no-deps -T auditor \
bash -c "opencode run --model '$model' $VARIANT_FLAG \"\$(cat AUDITOR.md; echo; echo '---'; cat CONTEXT.md; echo; echo '---'; cat scope/$task.md)\" "$out" 2>"$err"; then :; else
echo " [$model] $task: exit non-zero or timeout — opencode exit codes are unreliable; judging by output" >>"$lane_log"
fi
# opencode headless exits 0 even when it did nothing; trust the artifact.
if [[ -s "$out" ]] && grep -qiE 'finding|COMPLETE|summary' "$out"; then
echo " ok [$model] $task -> $out ($(wc -l <"$out") lines)"
else
echo " SUSPECT [$model] $task -> EMPTY/SUSPECT; see $err"
fi
done
echo "== lane done: [$model]"
}
if [[ -n "${SERIAL:-}" ]]; then
for model in $MODELS; do run_lane "$model"; done
else
declare -a lane_pids=()
for model in $MODELS; do
run_lane "$model" &
lane_pids+=("$!")
done
echo "==> ${#lane_pids[@]} model lanes running in parallel (pids: ${lane_pids[*]}); one provider/key per lane"
wait "${lane_pids[@]}"
fi
echo
echo "Findings under: $RUN_DIR"
echo "Review, then verify each real finding against a fresh stack before it"
echo "enters the issue ledger in status/issues.md. These models over-report."