init
This commit is contained in:
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/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.
|
||||
#
|
||||
# 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"
|
||||
|
||||
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
|
||||
execute_phase "$phase" || exit 1
|
||||
delivered+=("$phase")
|
||||
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"
|
||||
Executable
+185
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env node
|
||||
// progress.mjs — render a `pi --mode json` event stream as human-readable
|
||||
// progress on stdout, and write the final assistant message to a report file.
|
||||
//
|
||||
// Usage: pi --mode json "prompt" | node progress.mjs <report-file> [--quiet]
|
||||
//
|
||||
// Quiet mode still writes the report file (for harness logs); it only
|
||||
// suppresses the progress display.
|
||||
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const reportPath = args.find((a) => !a.startsWith("--"));
|
||||
const quiet = args.includes("--quiet");
|
||||
if (!reportPath) {
|
||||
console.error("usage: progress.mjs <report-file> [--quiet]");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
||||
const c = {
|
||||
dim: (s) => (useColor ? `\x1b[2m${s}\x1b[0m` : s),
|
||||
cyan: (s) => (useColor ? `\x1b[36m${s}\x1b[0m` : s),
|
||||
red: (s) => (useColor ? `\x1b[31m${s}\x1b[0m` : s),
|
||||
yellow: (s) => (useColor ? `\x1b[33m${s}\x1b[0m` : s),
|
||||
green: (s) => (useColor ? `\x1b[32m${s}\x1b[0m` : s),
|
||||
};
|
||||
|
||||
const out = (s = "") => {
|
||||
if (!quiet) process.stdout.write(s + "\n");
|
||||
};
|
||||
|
||||
function truncate(s, n) {
|
||||
s = String(s ?? "").replace(/\s+/g, " ").trim();
|
||||
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
||||
}
|
||||
|
||||
function summarizeTool(name, a = {}) {
|
||||
switch (name) {
|
||||
case "bash":
|
||||
return "$ " + truncate(a.command ?? a.cmd, 110);
|
||||
case "read":
|
||||
return truncate(a.path, 90) + (a.offset ? `:${a.offset}` : "");
|
||||
case "write":
|
||||
return truncate(a.path, 90);
|
||||
case "edit":
|
||||
return truncate(a.path, 90);
|
||||
case "grep":
|
||||
return `${truncate(a.pattern, 40)} in ${truncate(a.path ?? ".", 60)}`;
|
||||
case "find":
|
||||
return truncate(a.pattern, 40) + (a.path ? " in " + truncate(a.path, 60) : "");
|
||||
case "ls":
|
||||
return truncate(a.path ?? ".", 90);
|
||||
default: {
|
||||
let s = "";
|
||||
try {
|
||||
s = JSON.stringify(a);
|
||||
} catch {
|
||||
s = String(a);
|
||||
}
|
||||
return truncate(s, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function textOf(message) {
|
||||
if (!message) return "";
|
||||
if (typeof message.content === "string") return message.content;
|
||||
if (Array.isArray(message.content))
|
||||
return message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
||||
return "";
|
||||
}
|
||||
|
||||
function wrap(text, indent, width = 100) {
|
||||
const pad = " ".repeat(indent);
|
||||
const lines = [];
|
||||
for (const raw of text.split("\n")) {
|
||||
if (!raw) {
|
||||
lines.push("");
|
||||
continue;
|
||||
}
|
||||
let line = raw;
|
||||
while (line.length > width - indent) {
|
||||
let cut = line.lastIndexOf(" ", width - indent);
|
||||
if (cut < 20) cut = width - indent;
|
||||
lines.push(pad + line.slice(0, cut).trimEnd());
|
||||
line = line.slice(cut).trimStart();
|
||||
}
|
||||
lines.push(pad + line);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
let sessionId = "";
|
||||
let lastUsage = null;
|
||||
let lastAssistantText = "";
|
||||
let sawError = false;
|
||||
|
||||
function handle(ev) {
|
||||
switch (ev.type) {
|
||||
case "session":
|
||||
sessionId = ev.id ?? "";
|
||||
break;
|
||||
case "tool_execution_start":
|
||||
out(" " + c.cyan("⏺ " + ev.toolName) + " " + c.dim(summarizeTool(ev.toolName, ev.args)));
|
||||
break;
|
||||
case "tool_execution_end":
|
||||
if (ev.isError) out(" " + c.red("✗ " + ev.toolName + " failed"));
|
||||
break;
|
||||
case "message_update":
|
||||
if (ev.usage) lastUsage = ev.usage;
|
||||
break;
|
||||
case "message_end": {
|
||||
const m = ev.message ?? {};
|
||||
if (m.role === "assistant") {
|
||||
const text = textOf(m);
|
||||
if (text) {
|
||||
lastAssistantText = text;
|
||||
out(wrap(text, 2));
|
||||
out("");
|
||||
}
|
||||
if (m.stopReason === "error") {
|
||||
sawError = true;
|
||||
out(c.red(" ✗ " + (m.errorMessage ?? "assistant error")));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let buf = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
buf += chunk;
|
||||
let idx;
|
||||
while ((idx = buf.indexOf("\n")) >= 0) {
|
||||
const line = buf.slice(0, idx).trim();
|
||||
buf = buf.slice(idx + 1);
|
||||
if (!line) continue;
|
||||
let ev;
|
||||
try {
|
||||
ev = JSON.parse(line);
|
||||
} catch {
|
||||
out(c.yellow(" ? " + truncate(line, 120)));
|
||||
continue;
|
||||
}
|
||||
handle(ev);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on("end", () => {
|
||||
writeFileSync(
|
||||
reportPath,
|
||||
lastAssistantText ? lastAssistantText + "\n" : "(no final assistant message — see the .err log)\n",
|
||||
);
|
||||
const u = lastUsage ?? {};
|
||||
const bits = [];
|
||||
if (u.input) bits.push(`↑${u.input}`);
|
||||
if (u.output) bits.push(`↓${u.output}`);
|
||||
if (u.cacheRead) bits.push(`R${u.cacheRead}`);
|
||||
if (sessionId) bits.push("session " + String(sessionId).slice(0, 8));
|
||||
if (bits.length) out(c.dim(" · " + bits.join(" ")));
|
||||
out(sawError ? c.red(" · child finished with errors") : c.green(" · child done"));
|
||||
});
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# run-phase.sh — execute exactly one phase in a fresh pi context.
|
||||
#
|
||||
# Usage:
|
||||
# run-phase.sh # first pending phase (alphanumerical order)
|
||||
# run-phase.sh 03_api.md # a specific pending phase (warns if out of order)
|
||||
#
|
||||
# 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"
|
||||
|
||||
cd "$(find_root)" || die "no .agent/phases/todo found in this or parent directories (run /to-phase or /audit-create first)"
|
||||
|
||||
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
|
||||
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; }
|
||||
fi
|
||||
|
||||
build_pi_args
|
||||
execute_phase "$phase"
|
||||
Reference in New Issue
Block a user