(chore) identify new types
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env bash
|
||||
# Agentic driver for the rust-agent-sys board.
|
||||
#
|
||||
# Per task, two `pi` passes:
|
||||
# 1. coder — implements, runs the task's Verify command, reports
|
||||
# 2. reviewer — re-verifies, audits false passes/traps, emits a markdown review
|
||||
# The shell is the chain: the coder's report is fed into the reviewer's prompt.
|
||||
# The shell also owns the gate rule and the Status writes, because those are
|
||||
# mechanical and must not depend on a model choosing to comply.
|
||||
#
|
||||
# ./loop.sh run from the first unfinished task until done or blocked
|
||||
# ./loop.sh --list show phase order + current status, run nothing
|
||||
# ./loop.sh --dry-run list what would run, run nothing
|
||||
# ./loop.sh --sync rewrite INDEX.md status cells from the task files
|
||||
# ./loop.sh T0.2 T0.3 run only these, in the order given
|
||||
#
|
||||
# Resumable: state lives in the task files, not here. Rerun after a crash.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TASKS="$ROOT/tasks"
|
||||
INDEX="$TASKS/INDEX.md"
|
||||
REVIEWS="$ROOT/reviews"
|
||||
GUIDE="$TASKS/rust-guide-line.md"
|
||||
CRATE="$ROOT/poimen"
|
||||
AGENTS="$ROOT/.pi/agents"
|
||||
LOGS="$ROOT/.pi/logs"
|
||||
|
||||
# The golden-rule region of INDEX.md: board rules + progress tables.
|
||||
GOLDEN_RULE_LINES="0-69"
|
||||
|
||||
CODER_TOOLS="read,write,edit,bash,grep,find,ls,hashline_edit"
|
||||
REVIEWER_TOOLS="read,grep,find,ls,bash"
|
||||
|
||||
mkdir -p "$REVIEWS" "$LOGS"
|
||||
|
||||
# ---------------------------------------------------------------- board state
|
||||
|
||||
# Status cells carry emoji ("✅ Done", "⬜ Not started"), so match on substring.
|
||||
status_of() { sed -n 's/^| *Status *| *\(.*[^ ]\) *|$/\1/p' "$1" | head -1; }
|
||||
is_done() { case "$(status_of "$1")" in *Done*) return 0 ;; *) return 1 ;; esac; }
|
||||
|
||||
status_emoji() {
|
||||
case "$(status_of "$1")" in
|
||||
*Done*) printf '✅' ;;
|
||||
*rogress*) printf '🟡' ;;
|
||||
*lock*|*BLOCK*) printf '⛔' ;;
|
||||
*) printf '⬜' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
verdict_of() {
|
||||
[ -f "$1" ] || { echo "no-review"; return; }
|
||||
sed -n 's/^| *Verdict *| *\(.*[^ ]\) *|$/\1/p' "$1" | head -1
|
||||
}
|
||||
|
||||
task_id() { basename "$1" | cut -d- -f1; }
|
||||
phase_of() { echo "${1#T}" | cut -d. -f1; }
|
||||
|
||||
# Phase order. Numeric sort on phase then minor puts each gate task (highest
|
||||
# minor in its phase) last, so sequential execution satisfies the board's
|
||||
# "no phase starts until its predecessor's gate is green" rule for free.
|
||||
ordered_tasks() {
|
||||
find "$TASKS" -maxdepth 1 -name 'T*.md' \
|
||||
| sed "s|.*/T||" \
|
||||
| sort -t. -k1,1n -k2,2n \
|
||||
| sed "s|^|$TASKS/T|"
|
||||
}
|
||||
|
||||
file_for_id() { find "$TASKS" -maxdepth 1 -name "$1-*.md" | head -1; }
|
||||
|
||||
# The gate task of a phase is its highest-numbered task.
|
||||
gate_of_phase() {
|
||||
ordered_tasks | while read -r f; do
|
||||
[ "$(phase_of "$(task_id "$f")")" = "$1" ] && echo "$f"
|
||||
done | tail -1
|
||||
}
|
||||
|
||||
# No phase starts until its predecessor's gate is green.
|
||||
gate_blocks() {
|
||||
local phase="$1" prev gate
|
||||
[ "$phase" -eq 0 ] && return 1
|
||||
prev=$((phase - 1))
|
||||
gate="$(gate_of_phase "$prev")"
|
||||
[ -n "$gate" ] || return 1
|
||||
is_done "$gate" && return 1
|
||||
echo "P$phase blocked: P$prev gate $(task_id "$gate") is '$(status_of "$gate")'"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------- index mirroring
|
||||
|
||||
# Rewrite INDEX.md's status cells from the task files. The task file is the
|
||||
# source of truth; a status changed in one and not the other is a lie.
|
||||
sync_index() {
|
||||
local map tmp before after
|
||||
map="$(mktemp)"; tmp="$(mktemp)"
|
||||
while read -r f; do
|
||||
printf '%s %s\n' "$(task_id "$f")" "$(status_emoji "$f")"
|
||||
done < <(ordered_tasks) > "$map"
|
||||
|
||||
before="$(wc -l < "$INDEX")"
|
||||
|
||||
awk -v mapfile="$map" '
|
||||
BEGIN {
|
||||
FS = "|"; OFS = "|"
|
||||
while ((getline line < mapfile) > 0) {
|
||||
split(line, a, " ")
|
||||
st[a[1]] = a[2]
|
||||
p = a[1]; sub(/^T/, "", p); sub(/\..*$/, "", p)
|
||||
total[p]++
|
||||
if (a[2] == "✅") done[p]++
|
||||
else if (a[2] == "🟡") wip[p]++
|
||||
else todo[p]++
|
||||
gate[p] = a[1] # ordered input => last id in phase is the gate
|
||||
alltotal++
|
||||
}
|
||||
}
|
||||
# per-task row: | [T0.1](T0.1-foo.md) | Title | S | — | ⬜ |
|
||||
$0 ~ /^\| \[T[0-9]+\.[0-9]+\]/ {
|
||||
id = $2; sub(/^ *\[/, "", id); sub(/\].*$/, "", id)
|
||||
if (id in st) { $6 = " " st[id] " "; print; next }
|
||||
}
|
||||
# phase summary row: | P0 — Foundations | 9 | 0 | 0 | 9 | ⬜ T0.9 |
|
||||
$0 ~ /^\| P[0-8] / {
|
||||
p = $2; sub(/^ *P/, "", p); sub(/ .*$/, "", p)
|
||||
if (p in total) {
|
||||
$3 = " " total[p] " "
|
||||
$4 = " " (done[p] + 0) " "
|
||||
$5 = " " (wip[p] + 0) " "
|
||||
$6 = " " (todo[p] + 0) " "
|
||||
$7 = " " st[gate[p]] " " gate[p] " "
|
||||
print; next
|
||||
}
|
||||
}
|
||||
# total row: | **Total** | **71** | **0** | **0** | **71** | 0/9 green |
|
||||
$0 ~ /^\| \*\*Total\*\*/ {
|
||||
gates = 0; green = 0; d = 0; w = 0; t = 0
|
||||
for (p in total) {
|
||||
gates++
|
||||
if (st[gate[p]] == "✅") green++
|
||||
d += done[p]; w += wip[p]; t += todo[p]
|
||||
}
|
||||
$3 = " **" alltotal "** "
|
||||
$4 = " **" d "** "
|
||||
$5 = " **" w "** "
|
||||
$6 = " **" t "** "
|
||||
$7 = " " green "/" gates " green "
|
||||
print; next
|
||||
}
|
||||
{ print }
|
||||
' "$INDEX" > "$tmp"
|
||||
|
||||
after="$(wc -l < "$tmp")"
|
||||
if [ "$before" != "$after" ]; then
|
||||
echo "[warn] index sync changed line count ($before -> $after); INDEX.md left alone"
|
||||
rm -f "$map" "$tmp"
|
||||
return 1
|
||||
fi
|
||||
mv "$tmp" "$INDEX"
|
||||
rm -f "$map"
|
||||
}
|
||||
|
||||
mark_done() {
|
||||
local file="$1" tmp
|
||||
tmp="$(mktemp)"
|
||||
sed 's/^| *Status *|.*|$/| Status | ✅ Done |/' "$file" > "$tmp" && mv "$tmp" "$file"
|
||||
sync_index
|
||||
}
|
||||
|
||||
print_board() {
|
||||
printf '%-7s %-16s %-18s %s\n' TASK STATUS VERDICT FILE
|
||||
while read -r f; do
|
||||
id="$(task_id "$f")"
|
||||
printf '%-7s %-16s %-18s %s\n' \
|
||||
"$id" "$(status_of "$f")" "$(verdict_of "$REVIEWS/$id-review.md")" "$(basename "$f")"
|
||||
done < <(ordered_tasks)
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------- prompts
|
||||
|
||||
coder_prompt() {
|
||||
local id="$1" file="$2"
|
||||
cat <<PROMPT
|
||||
Implement task $id from the rust-agent-sys board.
|
||||
|
||||
Read $INDEX lines $GOLDEN_RULE_LINES FIRST, in full, before anything else. That
|
||||
is the golden rule for this board and it outranks everything else in this
|
||||
prompt. Obey its ordering rule, its Engineering Quality Rule, and its statement
|
||||
that the Status field in each task file is the source of truth.
|
||||
|
||||
- Task file: $file
|
||||
- Crate root: $CRATE (Cargo workspace; add new crates to its members list)
|
||||
- Quality bar: $GUIDE — read before writing code.
|
||||
|
||||
Then:
|
||||
1. Read the task file in full: Steps, Acceptance, Verify, False pass, Traps.
|
||||
2. Write the tests named in the Verify section BEFORE the implementation. Watch
|
||||
them fail for the right reason (missing type, not missing test file).
|
||||
3. Implement the minimum code that satisfies Acceptance. Follow Steps exactly.
|
||||
4. Run the exact Command line from the Verify section.
|
||||
5. Do NOT edit $file or $INDEX. Status is written by the driver, not by you.
|
||||
|
||||
Report, in this order:
|
||||
- files created/changed, one line each, and what each does
|
||||
- the Verify Command line you ran, verbatim
|
||||
- its full output and exit status, verbatim, whether it passed or failed
|
||||
- anything in Steps you could not do, and why
|
||||
PROMPT
|
||||
}
|
||||
|
||||
reviewer_prompt() {
|
||||
local id="$1" file="$2" report="$3"
|
||||
cat <<PROMPT
|
||||
Review task $id. Emit ONLY the markdown review document, nothing before or after.
|
||||
|
||||
- Board rules: $INDEX (lines $GOLDEN_RULE_LINES)
|
||||
- Task file: $file
|
||||
- Crate root: $CRATE
|
||||
- Quality bar: $GUIDE
|
||||
|
||||
Re-run the Verify command yourself from $ROOT. Do not trust the report below.
|
||||
Audit every False pass item and every Trap item named in the task file.
|
||||
|
||||
The coder reported:
|
||||
|
||||
--- BEGIN CODER REPORT ---
|
||||
$(cat "$report")
|
||||
--- END CODER REPORT ---
|
||||
PROMPT
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------- runner
|
||||
|
||||
run_task() {
|
||||
local file="$1" id review report stamp log_c log_r tmp block vd
|
||||
id="$(task_id "$file")"
|
||||
review="$REVIEWS/$id-review.md"
|
||||
stamp="$(date +%Y%m%dT%H%M%S)"
|
||||
report="$LOGS/$id-coder-report.md"
|
||||
log_c="$LOGS/$id-$stamp-coder.log"
|
||||
log_r="$LOGS/$id-$stamp-reviewer.log"
|
||||
|
||||
if is_done "$file"; then
|
||||
echo "[skip] $id already Done"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if block="$(gate_blocks "$(phase_of "$id")")"; then
|
||||
echo "[stop] $block"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "[code] $id $(date '+%H:%M:%S') -> $log_c"
|
||||
( cd "$ROOT" && pi --tools "$CODER_TOOLS" \
|
||||
--append-system-prompt "$AGENTS/coder.md" \
|
||||
--no-session -p "$(coder_prompt "$id" "$file")" ) 2>&1 | tee "$log_c"
|
||||
cp "$log_c" "$report"
|
||||
|
||||
echo "[revw] $id $(date '+%H:%M:%S') -> $log_r"
|
||||
( cd "$ROOT" && pi --tools "$REVIEWER_TOOLS" \
|
||||
--append-system-prompt "$AGENTS/reviewer.md" \
|
||||
--no-session -p "$(reviewer_prompt "$id" "$file" "$report")" ) 2>&1 | tee "$log_r"
|
||||
|
||||
# Strip an outer ``` fence if the reviewer wrapped the whole document.
|
||||
tmp="$(mktemp)"
|
||||
sed -e '1{/^```/d;}' -e '${/^```$/d;}' "$log_r" > "$tmp" && mv "$tmp" "$review"
|
||||
|
||||
vd="$(verdict_of "$review")"
|
||||
case "$vd" in
|
||||
*APPROVED*)
|
||||
mark_done "$file"
|
||||
echo "[ ok ] $id verdict=APPROVED status=$(status_of "$file")"
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo "[stop] $id verdict=${vd:-<none parsed>}"
|
||||
echo " review: $review"
|
||||
echo " logs: $log_c"
|
||||
echo " $log_r"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------- main
|
||||
|
||||
case "${1:-}" in
|
||||
--list) print_board; exit 0 ;;
|
||||
--sync) sync_index && echo "INDEX.md synced from task files"; exit $? ;;
|
||||
--dry-run) ordered_tasks | while read -r f; do
|
||||
is_done "$f" || echo "would run $(task_id "$f")"
|
||||
done; exit 0 ;;
|
||||
esac
|
||||
|
||||
command -v pi >/dev/null || { echo "pi not on PATH"; exit 127; }
|
||||
|
||||
# The TypeScript pi silently ignores unknown tools and lacks the Rust flags, so a
|
||||
# run against it degrades instead of failing. --list-providers exists only on the
|
||||
# Rust port; use it as the discriminator.
|
||||
if ! pi --list-providers >/dev/null 2>&1; then
|
||||
echo "pi at $(command -v pi) is not pi_agent_rust (reports: $(pi --version 2>&1 | head -1))"
|
||||
echo "install the Rust port with install.sh --adopt, or point PATH at it"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ -f "$AGENTS/coder.md" ] && [ -f "$AGENTS/reviewer.md" ] || {
|
||||
echo "missing coder.md / reviewer.md in $AGENTS"; exit 1; }
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
for id in "$@"; do
|
||||
f="$(file_for_id "$id")"
|
||||
[ -n "$f" ] || { echo "no task file for $id"; exit 1; }
|
||||
run_task "$f" || exit 1
|
||||
done
|
||||
echo "[done] requested tasks complete"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while read -r f; do
|
||||
run_task "$f" || exit 1
|
||||
done < <(ordered_tasks)
|
||||
|
||||
echo "[done] board complete"
|
||||
Reference in New Issue
Block a user