#!/usr/bin/env bash set -uo pipefail # CASAN Loop Orchestrator (Plan-17 Track 6, harness-owned). # # Drives one governed agent loop. Every turn runs the loop primitives in order # (17.20): loop-gate (verify) -> loop-governor (budget) -> loop-convergence # (progress) -> loop-trace (record). The loop stops on the FIRST stop-condition: # DONE gate PASS on the artifact (success-criteria proven), # HALT governor budget exceeded (loop-breaker) or gate DENY (security), # ESCALATE convergence STALLED/OSCILLATING (or gate ESCALATE). # # Secure-by-default (17.20): governance is ON. In profile=prod, disabling it # (CASAN_LOOP_GOVERNANCE=off) is refused unless an explicit, audited opt-out # reason is given (CASAN_LOOP_OPTOUT_REASON) — reversing the ARCH-03 lesson. # # Between-turn context compaction (17.21) uses context-compress.py + must-keep # when --context is supplied, so the loop's growing context is kept bounded. # # Usage: # loop-run.sh --run-id R --artifact FILE [--success-criteria FILE] \ # [--profile prod|dev] [--delegation-level L2] [--project okr] \ # [--max-steps N] [--tokens-per-step T] [--cost-per-step C] \ # [--context FILE] # # Exit codes: 0 DONE · 3 HALT/ESCALATE/DENY (governance stopped the loop) · # 4 opt-out refused (prod) · 2 usage error. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" GATE="$SCRIPT_DIR/loop-gate.py" GOV="$SCRIPT_DIR/loop-governor.py" CONV="$SCRIPT_DIR/loop-convergence.py" TRACE="$SCRIPT_DIR/loop-trace.py" COMPRESS="$SCRIPT_DIR/context-compress.py" RUN_ID=""; ARTIFACT=""; CRIT=""; PROFILE=""; DLEVEL=""; PROJECT="" MAX_STEPS=25; TOKENS_PER_STEP=1000; COST_PER_STEP="0.01"; CONTEXT="" while [[ "$#" -gt 0 ]]; do case "$1" in --run-id) RUN_ID="${2:-}"; shift 2 ;; --artifact) ARTIFACT="${2:-}"; shift 2 ;; --success-criteria) CRIT="${2:-}"; shift 2 ;; --profile) PROFILE="${2:-}"; shift 2 ;; --delegation-level) DLEVEL="${2:-}"; shift 2 ;; --project) PROJECT="${2:-}"; shift 2 ;; --max-steps) MAX_STEPS="${2:-}"; shift 2 ;; --tokens-per-step) TOKENS_PER_STEP="${2:-}"; shift 2 ;; --cost-per-step) COST_PER_STEP="${2:-}"; shift 2 ;; --context) CONTEXT="${2:-}"; shift 2 ;; *) echo "loop-run: unknown arg $1" >&2; exit 2 ;; esac done if [[ -z "$RUN_ID" || -z "$ARTIFACT" ]]; then echo "Usage: loop-run.sh --run-id R --artifact FILE [--success-criteria FILE] ..." >&2 exit 2 fi PROFILE="${PROFILE:-${CASAN_PROFILE:-dev}}" prof_args=(--profile "$PROFILE") [[ -n "$DLEVEL" ]] && prof_args+=(--delegation-level "$DLEVEL") [[ -n "$PROJECT" ]] && prof_args+=(--project "$PROJECT") sha_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" 2>/dev/null | awk '{print $1}' else shasum -a 256 "$1" 2>/dev/null | awk '{print $1}'; fi } json_field() { sed -n "s/.*\"$1\": \"\\([A-Za-z_]*\\)\".*/\\1/p" | head -1; } # --- secure-by-default governance gate (17.20) ------------------------------ GOVERNANCE="${CASAN_LOOP_GOVERNANCE:-on}" if [[ "$GOVERNANCE" == "off" ]]; then if [[ "$PROFILE" == "prod" && -z "${CASAN_LOOP_OPTOUT_REASON:-}" ]]; then echo "LOOP_REFUSE opt-out of loop governance requires CASAN_LOOP_OPTOUT_REASON in prod" >&2 exit 4 fi # An allowed opt-out is always audited (never silent). PYTHONPATH="$SCRIPT_DIR" python3 - "$RUN_ID" "${CASAN_LOOP_OPTOUT_REASON:-dev-optout}" <<'PY' import sys, loop_common as lc lc.append_audit({"kind": "loop_governance_optout", "run_id": sys.argv[1], "reason": sys.argv[2]}) PY fi echo "===== loop-run run_id=$RUN_ID profile=$PROFILE governance=$GOVERNANCE =====" FINAL="DONE"; RC=0 CUM_TOKENS=0 COST_ACC="0" for (( step=1; step<=MAX_STEPS; step++ )); do CUM_TOKENS=$(( CUM_TOKENS + TOKENS_PER_STEP )) COST_ACC="$(python3 -c "print(round($COST_ACC + $COST_PER_STEP, 6))")" DECISION="CONTINUE"; VERDICT=""; PROGRESS="0" if [[ "$GOVERNANCE" == "on" ]]; then # 1) Governor: cumulative budget check (loop-breaker) BEFORE more work. set +e python3 "$GOV" check --run-id "$RUN_ID" --step "$step" \ --tokens "$CUM_TOKENS" --cost "$COST_ACC" "${prof_args[@]}" >/dev/null 2>&1 grc=$? set -e 2>/dev/null || true if [[ "$grc" -eq 3 ]]; then FINAL="HALT"; DECISION="HALT"; RC=3; fi if [[ "$grc" -eq 4 ]]; then FINAL="ESCALATE"; DECISION="ESCALATE"; RC=3; fi if [[ "$DECISION" == "CONTINUE" ]]; then # 2) Gate: per-iteration verify contract. set +e GATE_OUT="$(python3 "$GATE" verify --run-id "$RUN_ID" --step "$step" \ --artifact "$ARTIFACT" ${CRIT:+--success-criteria "$CRIT"} "${prof_args[@]}" 2>/dev/null)" set -e 2>/dev/null || true VERDICT="$(printf '%s' "$GATE_OUT" | json_field verdict)" case "$VERDICT" in PASS) FINAL="DONE"; DECISION="DONE"; PROGRESS="1"; RC=0 ;; DENY) FINAL="HALT"; DECISION="HALT"; RC=3 ;; ESCALATE) FINAL="ESCALATE"; DECISION="ESCALATE"; RC=3 ;; *) DECISION="CONTINUE"; PROGRESS="0" ;; # FAIL -> keep correcting esac fi # 3) Convergence: observe + verdict (no-progress / oscillation breaker). if [[ "$DECISION" == "CONTINUE" ]]; then AH="$(sha_of "$ARTIFACT")"; AH="${AH:0:16}" python3 "$CONV" observe --run-id "$RUN_ID" --step "$step" \ --action-hash "$AH" --progress "$PROGRESS" >/dev/null 2>&1 set +e python3 "$CONV" verdict --run-id "$RUN_ID" "${prof_args[@]}" >/dev/null 2>&1 cvrc=$? set -e 2>/dev/null || true if [[ "$cvrc" -eq 3 ]]; then FINAL="HALT"; DECISION="HALT"; RC=3; fi if [[ "$cvrc" -eq 4 ]]; then FINAL="ESCALATE"; DECISION="ESCALATE"; RC=3; fi fi else # Governance opted out: record turns only, terminate at max-steps as DONE. DECISION="CONTINUE" fi # 4) Trace: append the immutable iteration record. python3 "$TRACE" record --run-id "$RUN_ID" --step "$step" \ --intent "turn-$step" --action verify --tool loop-run \ ${VERDICT:+--gate-verdict "$VERDICT"} --decision "$DECISION" --progress "$PROGRESS" \ --artifact "$ARTIFACT" ${CRIT:+--success-criteria "$CRIT"} \ --budget-snapshot "{\"steps\":$step,\"tokens\":$CUM_TOKENS,\"cost_usd\":$COST_ACC}" >/dev/null 2>&1 # 5) Between-turn context compaction (17.21). if [[ -n "$CONTEXT" && -f "$CONTEXT" ]]; then python3 "$COMPRESS" --mode structural --input "$CONTEXT" \ > "$(dirname "$ARTIFACT")/.loop-context-compacted.txt" 2>/dev/null || true fi [[ "$DECISION" == "CONTINUE" ]] || break done echo "LOOP_RESULT run_id=$RUN_ID final=$FINAL last_step=$step tokens=$CUM_TOKENS cost=$COST_ACC" exit "$RC"