feat(epic-execute): wire contract validation gate + self-heal fix loop

Final piece of the contract execution increment.

- run_contract_validation: env up → backend cases + UI flows → env down
- contract_validation_gate: bounded self-heal loop (execute_contract_fix_phase
  feeds CONTRACT_EXEC_FAILURES back to a focused fix prompt, commits, re-runs)
- Wired as a per-epic gate after the story loop (v1 granularity: the app
  reflects the whole epic before contracts run), opt-in by harness presence and
  --skip-contract-validation
- Exit-code-honest: CONTRACT_VALIDATION_FAILED makes the epic exit non-zero if
  contracts never pass, mirroring the preflight gate

Tested: orchestrator brings the env up/down and runs backend cases against a
live mock server with correct pass/fail + failure detail. Live UI flows and the
fix loop's Claude calls need the real app/CLI.

This completes the UI-contract + execution work held on this branch; ready to
bundle into one PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Caleb 2026-06-04 05:44:40 -05:00
parent c45d4fa875
commit 34b331c242
2 changed files with 138 additions and 0 deletions

View File

@ -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>
$failures
</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: <what remains>"
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
}

View File

@ -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"