diff --git a/scripts/epic-execute-lib/contract-exec.sh b/scripts/epic-execute-lib/contract-exec.sh
index 27c34782b..9aeb32bd7 100644
--- a/scripts/epic-execute-lib/contract-exec.sh
+++ b/scripts/epic-execute-lib/contract-exec.sh
@@ -21,6 +21,10 @@
CONTRACT_EXEC_FAILURES=""
# PID of the app started by contract_env_up (so contract_env_down can stop it).
CONTRACT_APP_PID=""
+# Set true when contract validation ultimately fails (drives the epic exit code).
+CONTRACT_VALIDATION_FAILED=false
+# Max self-heal attempts for contract failures.
+MAX_CONTRACT_FIX_ATTEMPTS="${MAX_CONTRACT_FIX_ATTEMPTS:-2}"
# =============================================================================
# Environment Lifecycle
@@ -349,3 +353,114 @@ run_ui_flows() {
log_error " UI flows failed"
return 1
}
+
+# =============================================================================
+# Orchestration + Fix Loop
+# =============================================================================
+
+# Run the full contract validation: bring the env up, run backend cases and UI
+# flows, tear the env down. Detail of any failures lands in CONTRACT_EXEC_FAILURES.
+# Returns 0 if everything passes, 1 otherwise.
+run_contract_validation() {
+ local h="$1"
+ CONTRACT_EXEC_FAILURES=""
+
+ if [ "${DRY_RUN:-false}" = true ]; then
+ echo "[DRY RUN] Would run contract validation (backend cases + UI flows)"
+ return 0
+ fi
+
+ log ">>> CONTRACT VALIDATION: bringing up sample environment"
+ if ! contract_env_up "$h"; then
+ contract_env_down "$h"
+ CONTRACT_EXEC_FAILURES="- sample environment failed to start"$'\n'
+ return 1
+ fi
+
+ local rc=0
+ run_backend_cases "$h" || rc=1
+ run_ui_flows "$h" || rc=1
+
+ contract_env_down "$h"
+ return $rc
+}
+
+# A focused fix pass for contract validation failures (epic-level self-heal,
+# mirroring the traceability fix loop). Returns 0 if the model reports success.
+execute_contract_fix_phase() {
+ local failures="$1"
+ local attempt="$2"
+
+ log ">>> CONTRACT FIX: attempt $attempt (addressing contract validation failures)"
+
+ local fix_prompt="You are fixing failures found by automated contract validation - real API calls and browser flows run against a running app.
+
+## Failures to Fix
+
+
+$failures
+
+
+## Rules
+- Fix the IMPLEMENTATION so these contract checks pass
+- Do NOT weaken, skip, or delete the contract checks themselves
+- Do NOT use 'git add -A' or 'git add .' - stage explicit paths
+- This is attempt $attempt of $MAX_CONTRACT_FIX_ATTEMPTS
+
+## Completion Signal
+When done, output exactly: CONTRACT FIX COMPLETE
+If you cannot fix everything, output: CONTRACT FIX INCOMPLETE: "
+
+ if [ "${DRY_RUN:-false}" = true ]; then
+ echo "[DRY RUN] Would run contract fix (attempt $attempt)"
+ return 0
+ fi
+
+ run_claude_to_file "$fix_prompt"
+ local result
+ result=$(read_phase_tail)
+
+ if echo "$result" | grep -q "CONTRACT FIX COMPLETE"; then
+ log_success "Contract fix reported complete (attempt $attempt)"
+ return 0
+ fi
+ log_warn "Contract fix incomplete (attempt $attempt)"
+ return 1
+}
+
+# Top-level gate: run contract validation with a bounded self-heal loop.
+# Non-blocking for the run itself, but sets CONTRACT_VALIDATION_FAILED so the
+# epic exits non-zero if contracts never pass.
+contract_validation_gate() {
+ local h="$1"
+
+ local attempt=0
+ while true; do
+ if run_contract_validation "$h"; then
+ log_success "Contract validation passed"
+ return 0
+ fi
+
+ if [ -z "$CONTRACT_EXEC_FAILURES" ]; then
+ log_warn "Contract validation failed without captured detail - continuing"
+ CONTRACT_VALIDATION_FAILED=true
+ return 1
+ fi
+
+ attempt=$((attempt + 1))
+ if [ "$attempt" -gt "$MAX_CONTRACT_FIX_ATTEMPTS" ]; then
+ log_error "Contract validation still failing after $MAX_CONTRACT_FIX_ATTEMPTS fix attempt(s)"
+ type add_metrics_issue >/dev/null 2>&1 && add_metrics_issue "epic-${EPIC_ID:-?}" "contract_validation_failed" "Contract checks failing after $MAX_CONTRACT_FIX_ATTEMPTS attempts"
+ CONTRACT_VALIDATION_FAILED=true
+ return 1
+ fi
+
+ log_warn "Contract failures found, attempting fix $attempt of $MAX_CONTRACT_FIX_ATTEMPTS"
+ execute_contract_fix_phase "$CONTRACT_EXEC_FAILURES" "$attempt" || true
+
+ if [ "${NO_COMMIT:-false}" = false ] && [ "${DRY_RUN:-false}" = false ]; then
+ git -C "$PROJECT_ROOT" add -u 2>/dev/null || true
+ git -C "$PROJECT_ROOT" commit -m "fix(epic-${EPIC_ID:-0}): contract validation fixes (attempt $attempt)" 2>/dev/null || true
+ fi
+ done
+}
diff --git a/scripts/epic-execute.sh b/scripts/epic-execute.sh
index 6cba74beb..a45818a17 100755
--- a/scripts/epic-execute.sh
+++ b/scripts/epic-execute.sh
@@ -133,6 +133,7 @@ LIB_DIR="$SCRIPT_DIR/epic-execute-lib"
[ -f "$LIB_DIR/json-output.sh" ] && source "$LIB_DIR/json-output.sh"
[ -f "$LIB_DIR/tdd-flow.sh" ] && source "$LIB_DIR/tdd-flow.sh"
[ -f "$LIB_DIR/contract-harness.sh" ] && source "$LIB_DIR/contract-harness.sh"
+[ -f "$LIB_DIR/contract-exec.sh" ] && source "$LIB_DIR/contract-exec.sh"
STORIES_DIR="$PROJECT_ROOT/docs/stories"
SPRINT_ARTIFACTS_DIR="$PROJECT_ROOT/docs/sprint-artifacts"
@@ -3192,6 +3193,21 @@ for story_file in "${STORIES[@]}"; do
fi
done
+# =============================================================================
+# Contract Validation (Per-Epic: execute the harness against the live app)
+# =============================================================================
+# v1 granularity: runs once after all stories are implemented (the app reflects
+# the full epic). Brings the sample env up, runs backend cases + UI flows, and
+# self-heals via a bounded fix loop. Non-blocking mid-run, but sets the epic
+# exit code if contracts never pass.
+if [ "$SKIP_CONTRACT_VALIDATION" != true ] && [ -n "${CONTRACT_HARNESS_FILE:-}" ] && type contract_validation_gate >/dev/null 2>&1; then
+ echo ""
+ log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ log "Contract Validation (API + UI)"
+ log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ contract_validation_gate "$CONTRACT_HARNESS_FILE" || true
+fi
+
# =============================================================================
# Traceability Check (Per-Epic, with Self-Healing)
# =============================================================================
@@ -3288,6 +3304,13 @@ if [ "${PREFLIGHT_FAILED:-false}" = true ]; then
exit 1
fi
+# Contract validation is an exit-code-honest gate: if API/UI contracts never
+# passed (after self-heal attempts), fail the epic.
+if [ "${CONTRACT_VALIDATION_FAILED:-false}" = true ]; then
+ log_warn "Contract validation did not pass - see failures above"
+ exit 1
+fi
+
if [ $FAILED -gt 0 ]; then
log_warn "$FAILED stories failed - check log for details"
log "Checkpoint preserved for resume capability"