Standard production layout: the OKR app (was nested under AINative_OKR_CASAN5/) is now
the repository root. No more wrapper directory.
- Promote AINative_OKR_CASAN5/* -> repo root (backend/ frontend/ packages/ apps/
.specify/ docs/ infra/ nginx/ scripts/ + configs). Merge tool dirs: .gitea (kept the
active deploy ci.yml, added harness-ci.yml + runbooks), .claude (agents/commands +
launch.json), .github moved up.
- Remove redundant: 00_SUBMISSION_PACKAGE, scattered root notes (FPT_CASAN_Full.md,
tu-tuong-casan.md, casan-tu-sinh..., casan_harness_assessment.md, source-review...,
README_CASAN5_REFINED.md), casan-next-plans/ and optimize-docs/ (competition/planning
artifacts — roadmap + design history preserved in git log / commit messages).
- Update all references to the old layout:
- .gitea/workflows/{ci,harness-ci}.yml, .github/workflows/{ci,deploy}.yml:
working-directory .; drop AINative_OKR_CASAN5/ prefix; .specify/{tests,scripts}
-> packages/casan-harness/... (.specify/logs state kept)
- .claude/launch.json, .gitea/*-runbook.md: path prefixes
- CLAUDE.md, README.md: docs/input -> apps/okr/domain/input
- policy-bundle.yaml: 8 policy paths -> packages/casan-harness/...; manifest re-signed
- secrets-scan.sh: fixture excludes -> new package/domain paths.
Full gate from the new root: PASS=64 FAIL=0 SKIP=3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
156 lines
6.5 KiB
Bash
156 lines
6.5 KiB
Bash
#!/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"
|