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:
2026-08-23 19:28:01 -04:00
parent b4eb89a677
commit 85ac0d61b9
17 changed files with 696 additions and 223 deletions
+19 -16
View File
@@ -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
View File
@@ -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\""
+27 -15
View File
@@ -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
+46
View File
@@ -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