feat: phases + tasks — per-task execution with phase directories
- A phase is now a directory: 00_phase.md (overview + task index) plus small, quick NN_task.md task files; complete/ mirrors todo/ - Task is the unit of execution: run-task.sh (one unit), run-phase.sh (phase to completion incl. 00_phase.md final pass), auto-phase.sh (all units in order); validate.sh gate runs after every task - PHASE_COMMIT commits at phase boundaries (00_phase.md / legacy file moves) - Legacy flat todo/NN_name.md files still execute as a single unit; new migrate-phases-to-tasks.sh converts them to the directory layout - phase-status.sh reports per-task state and the next unit - New task-template.md; phase templates now carry a Tasks index
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# auto-phase.sh — run the full phased pipeline, no LLM in the loop.
|
||||
#
|
||||
# Processes every file in .agent/phases/todo/ in alphanumerical order.
|
||||
# Each phase runs in its own pi process (fresh context); on failure the
|
||||
# executor's session is resumed for up to MAX_FIX_ATTEMPTS fixer rounds.
|
||||
# A phase moves to .agent/phases/complete/ only after the child exits 0
|
||||
# AND .agent/validate.sh passes. Stops at the first phase that cannot be
|
||||
# completed — re-run this script to continue where it stopped.
|
||||
# Processes every pending task in .agent/phases/todo/ in pipeline order
|
||||
# (phase order, then task order within each phase). Each task runs in its
|
||||
# own pi process (fresh context); .agent/validate.sh runs after EVERY task.
|
||||
# On failure the executor's session is resumed for up to MAX_FIX_ATTEMPTS
|
||||
# fixer rounds. A task moves to .agent/phases/complete/ only after the
|
||||
# child exits 0 AND validation passes; a phase completes when its
|
||||
# 00_phase.md final pass succeeds (legacy phases: when their file moves).
|
||||
# Stops at the first task that cannot be completed — re-run this script to
|
||||
# continue where it stopped.
|
||||
#
|
||||
# Env: see SKILL.md (MAX_FIX_ATTEMPTS, PHASE_MODEL, PHASE_THINKING,
|
||||
# PHASE_COMMIT, PI_TRUST, FRESH_FIX).
|
||||
@@ -16,24 +19,24 @@ source "$SCRIPT_DIR/lib.sh"
|
||||
|
||||
# Make interruptions visible: state stays in .agent/phases/todo, and the
|
||||
# failed executor's session is still resumable on the next run.
|
||||
trap 'echo; echo "✗ ERROR: interrupted (SIGINT) — ${phase:-the pipeline} is left in $PHASE_TODO/; re-run to continue where it stopped" >&2; exit 130' INT
|
||||
trap 'echo; echo "✗ ERROR: interrupted (SIGTERM) — ${phase:-the pipeline} is left in $PHASE_TODO/; re-run to continue where it stopped" >&2; exit 143' TERM
|
||||
trap 'echo; echo "✗ ERROR: interrupted (SIGINT) — ${unit:-the pipeline} is left in $PHASE_TODO/; re-run to continue where it stopped" >&2; exit 130' INT
|
||||
trap 'echo; echo "✗ ERROR: interrupted (SIGTERM) — ${unit:-the pipeline} is left in $PHASE_TODO/; re-run to continue where it stopped" >&2; exit 143' TERM
|
||||
|
||||
cd "$(find_root)" || die "no .agent/phases/todo found in this or parent directories (run /to-phase or /audit-create first)"
|
||||
|
||||
build_pi_args
|
||||
delivered=()
|
||||
while phase="$(next_phase)"; do
|
||||
[[ -n "$phase" ]] || break
|
||||
if ! execute_phase "$phase"; then
|
||||
echo "✗ ERROR: pipeline stopped — $phase FAILED after $MAX_FIX_ATTEMPTS attempts" >&2
|
||||
while unit="$(next_unit)"; do
|
||||
[[ -n "$unit" ]] || break
|
||||
if ! execute_unit "$unit"; then
|
||||
echo "✗ ERROR: pipeline stopped — $unit FAILED after $MAX_FIX_ATTEMPTS attempts" >&2
|
||||
echo " fix the issues above, then re-run this script to continue where it stopped" >&2
|
||||
exit 1
|
||||
fi
|
||||
delivered+=("$phase")
|
||||
delivered+=("$unit")
|
||||
done
|
||||
|
||||
echo
|
||||
echo "✓ pipeline done — ${#delivered[@]} phase(s) delivered this run: ${delivered[*]:-none}"
|
||||
remaining="$(ls -1 "$PHASE_TODO" 2>/dev/null | grep -cE '^[0-9]' || true)"
|
||||
echo " remaining in $PHASE_TODO/: $remaining"
|
||||
echo "✓ pipeline done — ${#delivered[@]} task(s) delivered this run: ${delivered[*]:-none}"
|
||||
remaining="$(count_units)"
|
||||
echo " remaining in $PHASE_TODO/: $remaining task(s)"
|
||||
|
||||
+183
-64
@@ -1,19 +1,28 @@
|
||||
#!/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.
|
||||
# Sourced by run-task.sh, 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)
|
||||
# .agent/PLAN.md master plan, LOCKED DECISIONS (binding)
|
||||
# .agent/phases/todo/NN_name/ pending phase: 00_phase.md (overview) + NN_task.md task files
|
||||
# .agent/phases/todo/NN_name.md legacy single-file phase (still executable)
|
||||
# .agent/phases/complete/ finished phases — mirrors the todo/ layout
|
||||
# .agent/reports/ per-task 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.
|
||||
# The unit of execution is the TASK: each task file runs in its own pi
|
||||
# subprocess (fresh context) with bounded fixer retries, and
|
||||
# .agent/validate.sh runs after EVERY task. A unit only moves to complete/
|
||||
# after the child exits 0, the child's stream ends with a clean final
|
||||
# report, and validation passes. When all of a phase's tasks are done,
|
||||
# 00_phase.md runs as the phase's final pass (any remaining inline work +
|
||||
# completion criteria + phase-level verification); moving it completes the
|
||||
# phase and is the PHASE_COMMIT commit point.
|
||||
|
||||
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
EXECUTOR_PROMPT_FILE="$SKILL_DIR/assets/executor-prompt.md"
|
||||
TASK_EXECUTOR_PROMPT_FILE="$SKILL_DIR/assets/task-executor-prompt.md"
|
||||
PHASE_FINAL_PROMPT_FILE="$SKILL_DIR/assets/phase-final-prompt.md"
|
||||
|
||||
PHASE_TODO=".agent/phases/todo"
|
||||
PHASE_DONE=".agent/phases/complete"
|
||||
@@ -36,10 +45,100 @@ find_root() {
|
||||
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
|
||||
# --- unit selection -------------------------------------------------------------
|
||||
# A unit is the smallest schedulable piece of work, referenced relative to
|
||||
# .agent/phases/todo/:
|
||||
# directory phase → each task file "NN_name/NN_task.md" (sort order), then
|
||||
# the phase overview "NN_name/00_phase.md" as the final pass
|
||||
# legacy flat → the phase file itself, "NN_name.md"
|
||||
|
||||
# Pending phase entries in todo/ (phase directories or legacy .md files), in execution order.
|
||||
phase_entries() {
|
||||
( cd "$PHASE_TODO" 2>/dev/null && ls -1 | grep -E '^[0-9]' | sort ) || true
|
||||
}
|
||||
|
||||
# First pending unit of one phase entry, or empty when the phase is done.
|
||||
phase_next_unit() {
|
||||
local p="$1" t
|
||||
if [[ -d "$PHASE_TODO/$p" ]]; then
|
||||
t="$(ls -1 "$PHASE_TODO/$p" 2>/dev/null | grep -E '^[0-9].*\.md$' | grep -vE '^00_phase\.md$' | sort | head -n1)"
|
||||
if [[ -n "$t" ]]; then printf '%s/%s\n' "$p" "$t"; return 0; fi
|
||||
if [[ -f "$PHASE_TODO/$p/00_phase.md" ]]; then printf '%s/00_phase.md\n' "$p"; return 0; fi
|
||||
elif [[ -f "$PHASE_TODO/$p" ]]; then
|
||||
printf '%s\n' "$p"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Next pending unit in the whole pipeline, or empty when everything is done.
|
||||
next_unit() {
|
||||
local p u
|
||||
for p in $(phase_entries); do
|
||||
u="$(phase_next_unit "$p")"
|
||||
if [[ -n "$u" ]]; then printf '%s\n' "$u"; return 0; fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# Count of pending units across all phases (task files + 00_phase.md + legacy files).
|
||||
count_units() {
|
||||
local p n=0
|
||||
for p in $(phase_entries); do
|
||||
if [[ -d "$PHASE_TODO/$p" ]]; then
|
||||
n=$(( n + $(ls -1 "$PHASE_TODO/$p" 2>/dev/null | grep -cE '\.md$') ))
|
||||
elif [[ -f "$PHASE_TODO/$p" ]]; then
|
||||
n=$(( n + 1 ))
|
||||
fi
|
||||
done
|
||||
printf '%s\n' "$n"
|
||||
}
|
||||
|
||||
# Name used in reports/sessions: "NN_name__NN_task" (dir phases) or "NN_name" (flat).
|
||||
unit_base() {
|
||||
local u="${1%.md}"
|
||||
printf '%s\n' "${u//\//__}"
|
||||
}
|
||||
|
||||
# Phase name for a unit (both forms).
|
||||
unit_phase() {
|
||||
local u="${1%.md}"
|
||||
printf '%s\n' "${u%%/*}"
|
||||
}
|
||||
|
||||
# Reports directory for a unit: per-phase subdir for dir phases, flat otherwise.
|
||||
unit_report_dir() {
|
||||
local u="$1"
|
||||
if [[ "$u" == */* ]]; then
|
||||
printf '%s/%s\n' "$PHASE_REPORTS" "$(unit_phase "$u")"
|
||||
else
|
||||
printf '%s\n' "$PHASE_REPORTS"
|
||||
fi
|
||||
}
|
||||
|
||||
# Report/log path for a unit attempt: <dir>/<base>.a<attempt>.<ext>.
|
||||
unit_report() {
|
||||
local u="$1" attempt="$2" ext="$3"
|
||||
printf '%s/%s.a%s.%s\n' "$(unit_report_dir "$u")" "$(unit_base "$u")" "$attempt" "$ext"
|
||||
}
|
||||
|
||||
# Phase-end unit (the commit point): 00_phase.md, or the whole file for legacy.
|
||||
is_phase_end() {
|
||||
local u="$1"
|
||||
if [[ "$u" == */* ]]; then [[ "${u##*/}" == "00_phase.md" ]]; else return 0; fi
|
||||
}
|
||||
|
||||
# Move a completed unit from todo/ to complete/ (same relative path).
|
||||
move_unit() {
|
||||
local u="$1" p t
|
||||
if [[ "$u" == */* ]]; then
|
||||
p="${u%%/*}"; t="${u##*/}"
|
||||
mkdir -p "$PHASE_DONE/$p"
|
||||
mv -f "$PHASE_TODO/$u" "$PHASE_DONE/$p/$t"
|
||||
if [[ -z "$(ls -A "$PHASE_TODO/$p" 2>/dev/null)" ]]; then rmdir "$PHASE_TODO/$p"; fi
|
||||
else
|
||||
mkdir -p "$PHASE_DONE"
|
||||
mv -f "$PHASE_TODO/$u" "$PHASE_DONE/$u"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- child pi arguments -------------------------------------------------------
|
||||
@@ -89,38 +188,52 @@ recover_report() {
|
||||
}
|
||||
|
||||
# --- prompts ------------------------------------------------------------------
|
||||
# First-attempt prompt for a unit. Directory tasks render the task executor
|
||||
# prompt; the phase overview renders the phase-final prompt; legacy flat
|
||||
# phase files render the original phase executor prompt.
|
||||
first_prompt() {
|
||||
local phase="$1"
|
||||
[[ -f "$EXECUTOR_PROMPT_FILE" ]] || die "missing $EXECUTOR_PROMPT_FILE"
|
||||
sed "s|{{PHASE}}|$phase|g" "$EXECUTOR_PROMPT_FILE"
|
||||
local unit="$1" p t
|
||||
if [[ "$unit" == */* ]]; then
|
||||
p="${unit%%/*}"; t="${unit##*/}"
|
||||
if [[ "$t" == "00_phase.md" ]]; then
|
||||
[[ -f "$PHASE_FINAL_PROMPT_FILE" ]] || die "missing $PHASE_FINAL_PROMPT_FILE"
|
||||
sed "s|{{PHASE}}|$p|g" "$PHASE_FINAL_PROMPT_FILE"
|
||||
else
|
||||
[[ -f "$TASK_EXECUTOR_PROMPT_FILE" ]] || die "missing $TASK_EXECUTOR_PROMPT_FILE"
|
||||
sed "s|{{PHASE}}|$p|g; s|{{TASK}}|$t|g" "$TASK_EXECUTOR_PROMPT_FILE"
|
||||
fi
|
||||
else
|
||||
[[ -f "$EXECUTOR_PROMPT_FILE" ]] || die "missing $EXECUTOR_PROMPT_FILE"
|
||||
sed "s|{{PHASE}}|$unit|g" "$EXECUTOR_PROMPT_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
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 "Your previous attempt at this task 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 tasks' 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)."
|
||||
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 task)."
|
||||
}
|
||||
}
|
||||
|
||||
# --- child executor -----------------------------------------------------------
|
||||
# run_child <phase> <attempt> <prompt> [resume-session]
|
||||
# run_child <unit> <attempt> <prompt> [resume-session]
|
||||
# Attempt 1: fresh session in .agent/phase-sessions/.
|
||||
# Attempt N>1: resumes the given session file — the failed executor's own
|
||||
# session, tracked by execute_phase (so retries keep its work). When no
|
||||
# session, tracked by execute_unit (so retries keep its work). When no
|
||||
# session was captured (or FRESH_FIX=1) a fresh ephemeral session runs with
|
||||
# the failure context instead.
|
||||
# Progress (tool calls, assistant text, thinking, compaction) 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
|
||||
# assistant message to: unit_report <unit> <attempt> md
|
||||
# Child stderr → unit_report <unit> <attempt> err
|
||||
# QUIET=1 suppresses the progress display (report file is still written).
|
||||
# Sets CHILD_RC (pi's exit code) and PROGRESS_RC (progress.mjs's exit code;
|
||||
# non-zero means the stream ended without a clean final report).
|
||||
@@ -141,21 +254,23 @@ _run_pi_pipeline() {
|
||||
# trap had fired, so the failure is always visible.
|
||||
if (( CHILD_RC == 130 || PROGRESS_RC == 130 )); then
|
||||
echo
|
||||
echo "✗ ERROR: interrupted (SIGINT) — phase is left in $PHASE_TODO/; re-run to continue" >&2
|
||||
echo "✗ ERROR: interrupted (SIGINT) — unit is left in $PHASE_TODO/; re-run to continue" >&2
|
||||
exit 130
|
||||
fi
|
||||
if (( CHILD_RC == 143 || PROGRESS_RC == 143 )); then
|
||||
echo
|
||||
echo "✗ ERROR: interrupted (SIGTERM) — phase is left in $PHASE_TODO/; re-run to continue" >&2
|
||||
echo "✗ ERROR: interrupted (SIGTERM) — unit is left in $PHASE_TODO/; re-run to continue" >&2
|
||||
exit 143
|
||||
fi
|
||||
}
|
||||
|
||||
run_child() {
|
||||
local phase="$1" attempt="$2" prompt="$3" resume_session="${4:-}"
|
||||
local base="${phase%.md}"
|
||||
local out="$PHASE_REPORTS/$base.a$attempt.md"
|
||||
local errf="$PHASE_REPORTS/$base.a$attempt.err"
|
||||
local unit="$1" attempt="$2" prompt="$3" resume_session="${4:-}"
|
||||
local base out errf
|
||||
base="$(unit_base "$unit")"
|
||||
out="$(unit_report "$unit" "$attempt" md)"
|
||||
errf="$(unit_report "$unit" "$attempt" err)"
|
||||
mkdir -p "$(dirname "$out")"
|
||||
local PROGRESS=(node "$SKILL_DIR/scripts/progress.mjs" "$out")
|
||||
[[ "${QUIET:-0}" == "1" ]] && PROGRESS+=(--quiet)
|
||||
|
||||
@@ -176,7 +291,7 @@ ensure_validate() {
|
||||
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."
|
||||
warn "adapt it to this project's real test/lint/coverage commands; it is the pass/fail gate after every task."
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -185,39 +300,39 @@ 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=""
|
||||
# --- one unit (task, phase final pass, or legacy phase), with retries ---------
|
||||
# execute_unit <unit>
|
||||
# Returns 0 and moves the unit to complete/ on success; returns 1 after
|
||||
# MAX_FIX_ATTEMPTS failed attempts (unit file is left in todo/).
|
||||
execute_unit() {
|
||||
local unit="$1"
|
||||
local base attempt=1 errors=""
|
||||
local last_session="" pre post
|
||||
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"
|
||||
base="$(unit_base "$unit")"
|
||||
command -v node >/dev/null 2>&1 || die "node not found on PATH (needed to render task progress)"
|
||||
mkdir -p "$PHASE_DONE" "$PHASE_REPORTS" "$PHASE_SESSIONS" "$(unit_report_dir "$unit")"
|
||||
ensure_validate
|
||||
|
||||
while (( attempt <= MAX_FIX_ATTEMPTS )); do
|
||||
echo "━━ $phase — attempt $attempt/$MAX_FIX_ATTEMPTS ━━"
|
||||
echo "━━ $unit — attempt $attempt/$MAX_FIX_ATTEMPTS ━━"
|
||||
pre="$(latest_child_session)"
|
||||
if (( attempt == 1 )); then
|
||||
run_child "$phase" 1 "$(first_prompt "$phase")"
|
||||
run_child "$unit" 1 "$(first_prompt "$unit")"
|
||||
else
|
||||
run_child "$phase" "$attempt" "$(fix_prompt "$errors")" "$last_session"
|
||||
run_child "$unit" "$attempt" "$(fix_prompt "$errors")" "$last_session"
|
||||
fi
|
||||
# Track which session file this attempt used, so the next attempt resumes
|
||||
# exactly this phase's failed session (not just "latest in the directory").
|
||||
# exactly this unit's failed session (not just "latest in the directory").
|
||||
post="$(latest_child_session)"
|
||||
if [[ -n "$post" && "$post" != "$pre" ]]; then
|
||||
last_session="$post"
|
||||
fi
|
||||
|
||||
# The child can be signaled mid-flush: the session file then holds a final
|
||||
# report the stream lost. Recover it so a completed phase is not retried.
|
||||
if (( CHILD_RC == 0 )) && [[ -f "$PHASE_REPORTS/$base.a$attempt.md" ]] \
|
||||
&& grep -q "no final assistant message" "$PHASE_REPORTS/$base.a$attempt.md"; then
|
||||
if [[ -n "$last_session" ]] && recover_report "$PHASE_REPORTS/$base.a$attempt.md" "$last_session"; then
|
||||
# report the stream lost. Recover it so a completed unit is not retried.
|
||||
if (( CHILD_RC == 0 )) && [[ -f "$(unit_report "$unit" "$attempt" md)" ]] \
|
||||
&& grep -q "no final assistant message" "$(unit_report "$unit" "$attempt" md)"; then
|
||||
if [[ -n "$last_session" ]] && recover_report "$(unit_report "$unit" "$attempt" md)" "$last_session"; then
|
||||
warn "stream lost the final message — report recovered from ${last_session##*/}"
|
||||
PROGRESS_RC=0
|
||||
fi
|
||||
@@ -225,40 +340,44 @@ execute_phase() {
|
||||
|
||||
errors=""
|
||||
if (( CHILD_RC != 0 )); then
|
||||
warn "child pi exited with code $CHILD_RC — see $PHASE_REPORTS/$base.a$attempt.err"
|
||||
errors+="[child pi exited with code $CHILD_RC]"$'\n'"$(tail -c 4000 "$PHASE_REPORTS/$base.a$attempt.err" 2>/dev/null)"
|
||||
warn "child pi exited with code $CHILD_RC — see $(unit_report "$unit" "$attempt" err)"
|
||||
errors+="[child pi exited with code $CHILD_RC]"$'\n'"$(tail -c 4000 "$(unit_report "$unit" "$attempt" err)" 2>/dev/null)"
|
||||
fi
|
||||
if (( PROGRESS_RC != 0 )); then
|
||||
warn "child run ended without a clean final report — see $PHASE_REPORTS/$base.a$attempt.md"
|
||||
errors+="[child run ended without a clean final report]"$'\n'"$(tail -c 4000 "$PHASE_REPORTS/$base.a$attempt.err" 2>/dev/null)"
|
||||
warn "child run ended without a clean final report — see $(unit_report "$unit" "$attempt" md)"
|
||||
errors+="[child run ended without a clean final report]"$'\n'"$(tail -c 4000 "$(unit_report "$unit" "$attempt" err)" 2>/dev/null)"
|
||||
fi
|
||||
if ! run_validation "$PHASE_REPORTS/$base.a$attempt.validate"; then
|
||||
warn ".agent/validate.sh FAILED — see $PHASE_REPORTS/$base.a$attempt.validate"
|
||||
errors+="[.agent/validate.sh FAILED]"$'\n'"$(tail -n 120 "$PHASE_REPORTS/$base.a$attempt.validate" 2>/dev/null)"
|
||||
if ! run_validation "$(unit_report "$unit" "$attempt" validate)"; then
|
||||
warn ".agent/validate.sh FAILED — see $(unit_report "$unit" "$attempt" validate)"
|
||||
errors+="[.agent/validate.sh FAILED]"$'\n'"$(tail -n 120 "$(unit_report "$unit" "$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)"
|
||||
move_unit "$unit"
|
||||
if is_phase_end "$unit"; then
|
||||
echo "✓ $unit → complete (phase $(unit_phase "$unit") done)"
|
||||
if [[ "${PHASE_COMMIT:-0}" == "1" ]]; then
|
||||
if git add -A 2>/dev/null && git commit --no-gpg-sign -m "phase: $(unit_phase "$unit")" >/dev/null 2>&1; then
|
||||
echo " (committed)"
|
||||
else
|
||||
warn "git commit failed (continuing)"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "✓ $unit → complete"
|
||||
fi
|
||||
echo "── executor report ──"
|
||||
cat "$PHASE_REPORTS/$base.a$attempt.md"
|
||||
cat "$(unit_report "$unit" "$attempt" md)"
|
||||
return 0
|
||||
fi
|
||||
attempt=$(( attempt + 1 ))
|
||||
done
|
||||
|
||||
{
|
||||
echo "✗ $phase FAILED after $MAX_FIX_ATTEMPTS attempts — left in $PHASE_TODO/."
|
||||
echo "✗ $unit FAILED after $MAX_FIX_ATTEMPTS attempts — left in $PHASE_TODO/."
|
||||
echo " last errors:"
|
||||
printf '%s\n' "$errors" | tail -n 40 | sed 's/^/ /'
|
||||
echo " logs: $PHASE_REPORTS/$base.a*.{md,err,validate}"
|
||||
echo " logs: $(unit_report_dir "$unit")/$(unit_base "$unit").a*.{md,err,validate}"
|
||||
if [[ -n "${last_session:-}" && -f "${last_session:-}" ]]; then
|
||||
echo " resume: re-run this script (it auto-resumes the failed session), or manually:"
|
||||
echo " pi --session $last_session -c \"review the failures above, fix them, re-validate\""
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# run-phase.sh — execute exactly one phase in a fresh pi context.
|
||||
# run-phase.sh — run every remaining task of one phase to completion.
|
||||
#
|
||||
# Each task runs in its own pi process (fresh context) with the
|
||||
# .agent/validate.sh gate after every task; the phase ends with its
|
||||
# 00_phase.md final pass.
|
||||
#
|
||||
# Usage:
|
||||
# run-phase.sh # first pending phase (alphanumerical order)
|
||||
# run-phase.sh 03_api.md # a specific pending phase (warns if out of order)
|
||||
# run-phase.sh # next phase with pending work, to completion
|
||||
# run-phase.sh 03_api # a specific phase (warns if out of order)
|
||||
# run-phase.sh 03_api.md # a legacy single-file phase
|
||||
#
|
||||
# Env: see SKILL.md (MAX_FIX_ATTEMPTS, PHASE_MODEL, PHASE_THINKING,
|
||||
# PHASE_COMMIT, PI_TRUST, FRESH_FIX).
|
||||
@@ -20,22 +25,29 @@ cd "$(find_root)" || die "no .agent/phases/todo found in this or parent director
|
||||
|
||||
phase="${1:-}"
|
||||
if [[ -n "$phase" ]]; then
|
||||
[[ "$phase" == *.md ]] || phase="$phase.md"
|
||||
[[ -f "$PHASE_TODO/$phase" ]] || die "$phase not found in $PHASE_TODO/"
|
||||
first="$(next_phase)"
|
||||
if [[ -n "$first" && "$first" != "$phase" ]]; then
|
||||
phase="${phase%/}"; phase="${phase%.md}"; phase="${phase%%/*}"
|
||||
[[ -d "$PHASE_TODO/$phase" || -f "$PHASE_TODO/$phase.md" ]] || die "$phase not found in $PHASE_TODO/"
|
||||
first="$(next_unit)"
|
||||
if [[ -n "$first" && "${first%%/*}" != "$phase" ]]; then
|
||||
warn "dependency order: $first is pending before $phase — running out of order"
|
||||
fi
|
||||
else
|
||||
phase="$(next_phase)"
|
||||
[[ -n "$phase" ]] || { echo "✓ no pending phases in $PHASE_TODO/ — project complete."; exit 0; }
|
||||
unit="$(next_unit)"
|
||||
[[ -n "$unit" ]] || { echo "✓ no pending tasks in $PHASE_TODO/ — project complete."; exit 0; }
|
||||
phase="$(unit_phase "$unit")"
|
||||
fi
|
||||
|
||||
build_pi_args
|
||||
if execute_phase "$phase"; then
|
||||
exit 0
|
||||
failed=0
|
||||
while unit="$(phase_next_unit "$phase")"; do
|
||||
[[ -n "$unit" ]] || break
|
||||
execute_unit "$unit" || { failed=1; break; }
|
||||
done
|
||||
if (( failed )); then
|
||||
echo "✗ ERROR: phase $phase FAILED — see error above" >&2
|
||||
echo " reports: $(unit_report_dir "$unit")/$(unit_base "$unit").a*.{md,err,validate}" >&2
|
||||
echo " resume: re-run this script — the failed executor session is resumed automatically" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "✗ ERROR: phase $phase FAILED after $MAX_FIX_ATTEMPTS attempts" >&2
|
||||
echo " reports: $PHASE_REPORTS/${phase%.md}.a*.{md,err,validate}" >&2
|
||||
echo " resume: re-run this script — the failed executor session is resumed automatically" >&2
|
||||
exit 1
|
||||
echo "✓ phase $phase complete"
|
||||
exit 0
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# run-task.sh — execute exactly one task in a fresh pi context.
|
||||
#
|
||||
# A task is the smallest execution unit: a task file inside a phase
|
||||
# directory (03_api/02_routes.md), a phase's 00_phase.md final pass, or a
|
||||
# legacy single-file phase.
|
||||
#
|
||||
# Usage:
|
||||
# run-task.sh # next pending task (pipeline order)
|
||||
# run-task.sh 03_api/02_routes.md # a specific task (warns if out of order)
|
||||
# run-task.sh 03_api/02_routes # ".md" is added when missing
|
||||
#
|
||||
# Env: see SKILL.md (MAX_FIX_ATTEMPTS, PHASE_MODEL, PHASE_THINKING,
|
||||
# PHASE_COMMIT, PI_TRUST, FRESH_FIX).
|
||||
set -uo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/lib.sh"
|
||||
|
||||
# Make interruptions visible: state stays in .agent/phases/todo, and the
|
||||
# failed executor's session is still resumable on the next run.
|
||||
trap 'echo; echo "✗ ERROR: interrupted (SIGINT) — ${unit:-this task} is left in $PHASE_TODO/; re-run to continue" >&2; exit 130' INT
|
||||
trap 'echo; echo "✗ ERROR: interrupted (SIGTERM) — ${unit:-this task} is left in $PHASE_TODO/; re-run to continue" >&2; exit 143' TERM
|
||||
|
||||
cd "$(find_root)" || die "no .agent/phases/todo found in this or parent directories (run /to-phase or /audit-create first)"
|
||||
|
||||
unit="${1:-}"
|
||||
if [[ -n "$unit" ]]; then
|
||||
[[ "$unit" == *.md ]] || unit="$unit.md"
|
||||
[[ -f "$PHASE_TODO/$unit" ]] || die "$unit not found in $PHASE_TODO/"
|
||||
first="$(next_unit)"
|
||||
if [[ -n "$first" && "$first" != "$unit" ]]; then
|
||||
warn "execution order: $first is pending before $unit — running out of order"
|
||||
fi
|
||||
else
|
||||
unit="$(next_unit)"
|
||||
[[ -n "$unit" ]] || { echo "✓ no pending tasks in $PHASE_TODO/ — project complete."; exit 0; }
|
||||
fi
|
||||
|
||||
build_pi_args
|
||||
if execute_unit "$unit"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "✗ ERROR: task $unit FAILED after $MAX_FIX_ATTEMPTS attempts" >&2
|
||||
echo " reports: $(unit_report_dir "$unit")/$(unit_base "$unit").a*.{md,err,validate}" >&2
|
||||
echo " resume: re-run this script — the failed executor session is resumed automatically" >&2
|
||||
exit 1
|
||||
Reference in New Issue
Block a user