feat: update plan 17
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user