This commit is contained in:
2026-08-21 02:30:51 -04:00
commit 38ecedcaa3
7 changed files with 532 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
# lib.sh — shared logic for the phased-execution skill.
# Sourced by run-phase.sh and auto-phase.sh. Not meant to be run directly.
#
# Phase state lives in files, not chat context:
# .agent/PLAN.md master plan, LOCKED DECISIONS (binding)
# .agent/phases/todo/ pending phases, NN_name.md, sorted = execution order
# .agent/phases/complete/ finished phases
# .agent/reports/ per-phase executor reports, stderr, validation logs
# .agent/phase-sessions/ child pi session files (resumable fixers)
#
# The pass/fail gate is .agent/validate.sh. A phase only moves to complete/
# after the child executor exits 0 AND validation passes.
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
EXECUTOR_PROMPT_FILE="$SKILL_DIR/assets/executor-prompt.md"
PHASE_TODO=".agent/phases/todo"
PHASE_DONE=".agent/phases/complete"
PHASE_REPORTS=".agent/reports"
PHASE_SESSIONS=".agent/phase-sessions"
MAX_FIX_ATTEMPTS="${MAX_FIX_ATTEMPTS:-3}"
warn() { echo "⚠ $*" >&2; }
die() { echo "✗ ERROR: $*" >&2; exit 1; }
# --- project root -------------------------------------------------------------
# Walk up from $PWD to the nearest directory containing .agent/phases/todo.
find_root() {
local d
d="$(pwd)"
while :; do
if [[ -d "$d/.agent/phases/todo" ]]; then printf '%s\n' "$d"; return 0; fi
[[ "$d" == "/" ]] && return 1
d="$(dirname "$d")"
done
}
# --- phase selection ----------------------------------------------------------
# First pending phase (alphanumerical sort), or empty when none remain.
next_phase() {
( cd "$PHASE_TODO" 2>/dev/null && ls -1 | grep -E '^[0-9]' | sort | head -n1 ) || true
}
# --- child pi arguments -------------------------------------------------------
build_pi_args() {
PI_ARGS=()
[[ -n "${PHASE_MODEL:-}" ]] && PI_ARGS+=(--model "$PHASE_MODEL")
[[ -n "${PHASE_THINKING:-}" ]] && PI_ARGS+=(--thinking "$PHASE_THINKING")
if [[ "${PI_TRUST:-0}" == "1" ]]; then
PI_ARGS+=(--approve) # load project .pi/ settings, skills, extensions
else
PI_ARGS+=(--no-approve) # deterministic: global config + AGENTS.md only
fi
}
# Most recently modified child session file (or empty).
latest_child_session() {
ls -t "$PHASE_SESSIONS"/*.jsonl 2>/dev/null | head -n1 || true
}
# --- prompts ------------------------------------------------------------------
first_prompt() {
local phase="$1"
[[ -f "$EXECUTOR_PROMPT_FILE" ]] || die "missing $EXECUTOR_PROMPT_FILE"
sed "s|{{PHASE}}|$phase|g" "$EXECUTOR_PROMPT_FILE"
}
fix_prompt() {
local errors="$1"
{
echo "Your previous attempt at this phase was rejected by the harness."
echo "The failures from the last attempt are below. Review them, fix the code, and re-run the full test suite and linter until everything is green. Do not start other phases' work."
echo
echo "Failure output (may be truncated):"
echo '```'
printf '%s\n' "$errors" | tail -c 6000
echo '```'
echo
echo "When everything is green, reply with the same report as before (at most 15 lines: what was fixed, test/lint/coverage results, notable decisions, next pending phase)."
}
}
# --- child executor -----------------------------------------------------------
# run_child <phase> <attempt> <prompt>
# Attempt 1: fresh session in .agent/phase-sessions/.
# Attempt N>1: resume attempt N-1's session (unless FRESH_FIX=1 or no session
# was created — in that case a fresh ephemeral session with the failure context).
# Progress (tool calls + assistant text) streams to the terminal live via
# scripts/progress.mjs, which also writes the final assistant message to:
# .agent/reports/<base>.a<attempt>.md
# Child stderr → .agent/reports/<base>.a<attempt>.err
# QUIET=1 suppresses the progress display (report file is still written).
# Sets CHILD_RC.
run_child() {
local phase="$1" attempt="$2" prompt="$3"
local base="${phase%.md}"
local out="$PHASE_REPORTS/$base.a$attempt.md"
local errf="$PHASE_REPORTS/$base.a$attempt.err"
local ref="$PHASE_REPORTS/.ref"
local session
local PROGRESS=(node "$SKILL_DIR/scripts/progress.mjs" "$out")
[[ "${QUIET:-0}" == "1" ]] && PROGRESS+=(--quiet)
if (( attempt == 1 )); then
touch "$ref"
pi "${PI_ARGS[@]}" --session-dir "$PHASE_SESSIONS" --name "$base" --mode json "$prompt" 2>"$errf" | "${PROGRESS[@]}"
else
touch "$ref"
session="$(latest_child_session)"
if [[ "${FRESH_FIX:-0}" != "1" && -n "$session" && "$session" -nt "$ref" ]]; then
echo " (resuming failed executor session: ${session##*/})"
pi "${PI_ARGS[@]}" --session "$session" --mode json "$prompt" 2>"$errf" | "${PROGRESS[@]}"
else
echo " (no resumable session — starting fresh)"
pi "${PI_ARGS[@]}" --no-session --mode json "$prompt" 2>"$errf" | "${PROGRESS[@]}"
fi
fi
CHILD_RC=${PIPESTATUS[0]}
}
# --- validation gate ----------------------------------------------------------
ensure_validate() {
if [[ ! -f .agent/validate.sh ]]; then
cp "$SKILL_DIR/assets/validate.sh" .agent/validate.sh
chmod +x .agent/validate.sh
warn "no .agent/validate.sh found — created it from the skill template."
warn "adapt it to this project's real test/lint/coverage commands; it is the pass/fail gate for every phase."
fi
}
# run_validation <logfile>; returns 0 iff .agent/validate.sh exits 0.
run_validation() {
bash .agent/validate.sh >"$1" 2>&1
}
# --- one phase, with bounded fixer retries ------------------------------------
# execute_phase <phase-file>
# Returns 0 and moves the phase to complete/ on success; returns 1 after
# MAX_FIX_ATTEMPTS failed attempts (phase file is left in todo/).
execute_phase() {
local phase="$1"
local base="${phase%.md}"
local attempt=1 errors=""
command -v node >/dev/null 2>&1 || die "node not found on PATH (needed to render phase progress)"
mkdir -p "$PHASE_DONE" "$PHASE_REPORTS" "$PHASE_SESSIONS"
ensure_validate
while (( attempt <= MAX_FIX_ATTEMPTS )); do
echo "━━ $phase — attempt $attempt/$MAX_FIX_ATTEMPTS ━━"
if (( attempt == 1 )); then
run_child "$phase" 1 "$(first_prompt "$phase")"
else
run_child "$phase" "$attempt" "$(fix_prompt "$errors")"
fi
errors=""
if (( CHILD_RC != 0 )); then
errors+="[child pi exited with code $CHILD_RC]"$'\n'"$(tail -c 4000 "$PHASE_REPORTS/$base.a$attempt.err" 2>/dev/null)"
fi
if ! run_validation "$PHASE_REPORTS/$base.a$attempt.validate"; then
errors+="[.agent/validate.sh FAILED]"$'\n'"$(tail -n 120 "$PHASE_REPORTS/$base.a$attempt.validate" 2>/dev/null)"
fi
if [[ -z "$errors" ]]; then
mv -f "$PHASE_TODO/$phase" "$PHASE_DONE/$phase"
echo "✓ $phase → complete"
if [[ "${PHASE_COMMIT:-0}" == "1" ]]; then
if git add -A 2>/dev/null && git commit --no-gpg-sign -m "phase: $phase" >/dev/null 2>&1; then
echo " (committed)"
else
warn "git commit failed (continuing)"
fi
fi
echo "── executor report ──"
cat "$PHASE_REPORTS/$base.a$attempt.md"
return 0
fi
attempt=$(( attempt + 1 ))
done
echo "✗ $phase FAILED after $MAX_FIX_ATTEMPTS attempts — left in $PHASE_TODO/." >&2
echo " logs: $PHASE_REPORTS/$base.a*.{md,err,validate}" >&2
echo " resume: pi --session-dir $PHASE_SESSIONS -c" >&2
return 1
}