#!/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."