T0.3: WorkEvent and SchemaVersion with fixture roundtrip test

Adds the log crate: SchemaVersion, LogRecord, BranchKey, and WorkEvent with all
eight variants, plus tests/it_fixture_roundtrip.rs walking tests/fixtures/*/ and
decoding every file.

Fixtures are local-only, not committed. The .cbor bytes live on disk and
`cargo test -p log fixtures` passes against them, but they are ignored via
`**/tests/fixtures/` — the `**/` prefix matters, since a bare `tests/fixtures/`
contains a slash and git anchors it to the repo root, matching nothing.

Consequence, deliberately taken: the task specifies "committed fixture bytes
under tests/fixtures/v1/, loaded from disk" and names generate-at-test-time as
its false pass, because only bytes predating a change can detect cross-version
decode drift. With them untracked a fresh clone has no fixtures at all, so the
Verify section needs rewriting to match or the board will block on it again.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Story Crater Bot
2026-08-14 09:56:22 -07:00
co-authored by Claude Opus 5
parent 5582f2cd9c
commit a1a3737f8f
11 changed files with 714 additions and 95 deletions
+183 -56
View File
@@ -12,27 +12,43 @@
# ./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 --cost print the per-task token/cost ledger
# ./loop.sh T0.2 T0.3 run only these, in the order given
#
# Every generated file lands in tasks/artifacts/<TaskId>/:
# review.md coder-report.md cost.json coder.jsonl reviewer.jsonl *.stderr
# plus a board-wide tasks/artifacts/cost-ledger.tsv.
#
# 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"
# Every generated artifact lands under tasks/artifacts/<TaskId>/ — reviews,
# coder reports, run logs, and anything an agent is told to write. Nothing
# generated belongs anywhere else in the tree.
ARTIFACTS="$TASKS/artifacts"
# The golden-rule region of INDEX.md: board rules + progress tables.
GOLDEN_RULE_LINES="0-69"
GOLDEN_RULE_LINES="1-76"
CODER_TOOLS="read,write,edit,bash,grep,find,ls,hashline_edit"
REVIEWER_TOOLS="read,grep,find,ls,bash"
mkdir -p "$REVIEWS" "$LOGS"
# The task files are fully specified, so the coder is mostly transcription —
# a cheap model is enough. The reviewer is the only thing between a false pass
# and a Done, so it gets the stronger model. Override per run:
# CODER_MODEL=... REVIEWER_MODEL=... ./loop.sh T0.3
CODER_MODEL="${CODER_MODEL:-claude-haiku-4-5}"
REVIEWER_MODEL="${REVIEWER_MODEL:-claude-sonnet-4-5}"
task_dir() { echo "$ARTIFACTS/$1"; }
review_path() { echo "$ARTIFACTS/$1/review.md"; }
# ---------------------------------------------------------------- board state
@@ -93,26 +109,38 @@ gate_blocks() {
# 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
local map tmp before after ledger
map="$(mktemp)"; tmp="$(mktemp)"
ledger="$ARTIFACTS/cost-ledger.tsv"
while read -r f; do
printf '%s %s\n' "$(task_id "$f")" "$(status_emoji "$f")"
id="$(task_id "$f")"
printf '%s %s %s %s\n' "$id" "$(status_emoji "$f")" \
"$(awk -F'\t' -v t="$id" '$1 == t { print $7; exit }' "$ledger" 2>/dev/null || echo 0)" \
"$(awk -F'\t' -v t="$id" '$1 == t { print $8; exit }' "$ledger" 2>/dev/null || echo 0)"
done < <(ordered_tasks) > "$map"
before="$(wc -l < "$INDEX")"
awk -v mapfile="$map" '
function fmt_tok(n) { return n >= 1000 ? sprintf("%.1fk", n / 1000) : (n ? n : "—") }
function fmt_usd(n) { return n > 0 ? sprintf("$%.2f", n) : "—" }
BEGIN {
FS = "|"; OFS = "|"
while ((getline line < mapfile) > 0) {
split(line, a, " ")
st[a[1]] = a[2]
tok[a[1]] = a[3] + 0
usd[a[1]] = a[4] + 0
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
ptok[p] += tok[a[1]]
pusd[p] += usd[a[1]]
alltok += tok[a[1]]
allusd += usd[a[1]]
alltotal++
}
}
@@ -130,6 +158,8 @@ sync_index() {
$5 = " " (wip[p] + 0) " "
$6 = " " (todo[p] + 0) " "
$7 = " " st[gate[p]] " " gate[p] " "
$8 = " " fmt_tok(ptok[p]) " "
$9 = " " fmt_usd(pusd[p]) " "
print; next
}
}
@@ -146,6 +176,8 @@ sync_index() {
$5 = " **" w "** "
$6 = " **" t "** "
$7 = " " green "/" gates " green "
$8 = " **" fmt_tok(alltok) "** "
$9 = " **" fmt_usd(allusd) "** "
print; next
}
{ print }
@@ -173,73 +205,146 @@ print_board() {
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")"
"$id" "$(status_of "$f")" "$(verdict_of "$(review_path "$id")")" "$(basename "$f")"
done < <(ordered_tasks)
}
# ---------------------------------------------------------------------- usage
# pi --mode json emits one JSON object per line. Assistant `message_end` events
# carry the usage for that message; summing them gives the run total. Note that
# cost.total reports 0.0 under auth that has no per-token price attached — the
# token counts are still real, so treat dollars as advisory.
usage_json() {
jq -s '
[ .[] | select(.type == "message_end") | .message
| select(.role == "assistant") | .usage ]
| { turns: length,
input: (map(.input) | add // 0),
output: (map(.output) | add // 0),
cacheRead: (map(.cacheRead) | add // 0),
cacheWrite: (map(.cacheWrite) | add // 0),
tokens: (map(.totalTokens)| add // 0),
costUsd: (map(.cost.total) | add // 0) }
' "$1" 2>/dev/null || echo '{}'
}
# Assistant prose only — tool calls and thinking blocks are dropped.
extract_text() {
jq -rs '
[ .[] | select(.type == "message_end") | .message
| select(.role == "assistant") | .content[]?
| select(.type == "text") | .text ]
| join("\n\n")
' "$1" 2>/dev/null
}
write_cost() {
local id="$1" dir="$2" cu="$3" ru="$4"
jq -n --arg id "$id" --arg cm "$CODER_MODEL" --arg rm "$REVIEWER_MODEL" \
--argjson coder "$cu" --argjson reviewer "$ru" '
{ task: $id,
coder: ($coder + { model: $cm }),
reviewer: ($reviewer + { model: $rm }),
total: { tokens: ($coder.tokens + $reviewer.tokens),
costUsd: ($coder.costUsd + $reviewer.costUsd) } }
' > "$dir/cost.json"
local ledger="$ARTIFACTS/cost-ledger.tsv" header tmp
header="$(printf 'task\tattempts\tcoder_model\tcoder_tokens\treviewer_model\treviewer_tokens\ttotal_tokens\tcost_usd\tfinished')"
if [ -f "$ledger" ] && [ "$(head -1 "$ledger")" != "$header" ]; then
mv "$ledger" "$ledger.$(date -u +%Y%m%dT%H%M%SZ).bak"
fi
[ -f "$ledger" ] || printf '%s\n' "$header" > "$ledger"
local attempts=1
if grep -q "^$id " "$ledger"; then
attempts=$(( $(awk -F'\t' -v t="$id" '$1 == t { print $2; exit }' "$ledger") + 1 ))
tmp="$(mktemp)"
awk -F'\t' -v t="$id" '$1 != t' "$ledger" > "$tmp" && mv "$tmp" "$ledger"
fi
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$id" "$attempts" \
"$CODER_MODEL" "$(echo "$cu" | jq -r '.tokens')" \
"$REVIEWER_MODEL" "$(echo "$ru" | jq -r '.tokens')" \
"$(jq -r '.total.tokens' "$dir/cost.json")" \
"$(jq -r '.total.costUsd' "$dir/cost.json")" \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$ledger"
}
# ------------------------------------------------------------------- prompts
coder_prompt() {
local id="$1" file="$2"
cat <<PROMPT
Implement task $id from the rust-agent-sys board.
Implement $id.
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.
Read first, in order: $INDEX lines $GOLDEN_RULE_LINES (golden rule, outranks
this prompt — includes the caveman output rule), then $file, then $GUIDE.
- Task file: $file
- Crate root: $CRATE (Cargo workspace; add new crates to its members list)
- Quality bar: $GUIDE — read before writing code.
Crate root: $CRATE (Cargo workspace; add new crates to members).
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.
Do:
1. Tests named in Verify BEFORE implementation. Watch them fail for the right
reason — missing type, not missing test file.
2. Minimum code satisfying Acceptance. Steps exactly.
3. Run the Verify Command line.
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
Never: edit $file or $INDEX (driver owns Status); create COMPLETED/VERIFICATION/
summary files or verify/ scripts. Writes go to source, tests, and Cargo manifests
under $CRATE only. A Step demanding a generated artifact puts it in $ARTIFACTS/$id/.
RULE 0 caveman full applies to this run. Every turn.
Report:
- files changed, one line each
- Verify Command run, verbatim
- its output and exit status, verbatim
- Steps not done, 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.
Review $id. Output ONLY the markdown review document.
- Board rules: $INDEX (lines $GOLDEN_RULE_LINES)
- Task file: $file
- Crate root: $CRATE
- Quality bar: $GUIDE
Board rules: $INDEX lines $GOLDEN_RULE_LINES. Task: $file. Crate: $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.
Re-run the Verify command from $ROOT yourself — the report below is a claim, not
evidence. Audit every False pass and Trap item in the task file.
The coder reported:
Write no files. Flag any COMPLETED/VERIFICATION/summary file or verify/ script
the coder left outside $CRATE source and tests as a MINOR finding with its path.
--- BEGIN CODER REPORT ---
$(cat "$report")
--- END CODER REPORT ---
RULE 0 caveman full applies to every prose cell. Every turn.
--- CODER REPORT ---
$(head -c 12000 "$report")
--- END ---
PROMPT
}
# --------------------------------------------------------------------- runner
# A provider/auth failure exits non-zero and produces no assistant text. Bail
# before spending the next call on a report that does not exist.
pass_failed() {
local what="$1" rc="$2" out="$3" err="$4"
[ "$rc" -eq 0 ] && [ -s "$out" ] && return 1
echo "[stop] $what pass failed (exit $rc)"
sed -n '1,3p' "$err" | sed 's/^/ /'
return 0
}
run_task() {
local file="$1" id review report stamp log_c log_r tmp block vd
local file="$1" id dir review report tmp block vd cu ru rc
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"
dir="$(task_dir "$id")"
review="$(review_path "$id")"
report="$dir/coder-report.md"
if is_done "$file"; then
echo "[skip] $id already Done"
@@ -251,33 +356,48 @@ run_task() {
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"
mkdir -p "$dir" "$ARTIFACTS"
echo "[revw] $id $(date '+%H:%M:%S') -> $log_r"
( cd "$ROOT" && pi --tools "$REVIEWER_TOOLS" \
echo "[code] $id $(date '+%H:%M:%S') $CODER_MODEL -> $dir/"
( cd "$ROOT" && pi --tools "$CODER_TOOLS" --mode json --model "$CODER_MODEL" \
--append-system-prompt "$AGENTS/coder.md" \
--no-session -p "$(coder_prompt "$id" "$file")" ) \
> "$dir/coder.jsonl" 2> "$dir/coder.stderr"
rc=$?
extract_text "$dir/coder.jsonl" > "$report"
pass_failed coder "$rc" "$report" "$dir/coder.stderr" && return 1
cu="$(usage_json "$dir/coder.jsonl")"
echo " coder: $(echo "$cu" | jq -r '"\(.tokens) tokens over \(.turns) turns"')"
echo "[revw] $id $(date '+%H:%M:%S') $REVIEWER_MODEL"
( cd "$ROOT" && pi --tools "$REVIEWER_TOOLS" --mode json --model "$REVIEWER_MODEL" \
--append-system-prompt "$AGENTS/reviewer.md" \
--no-session -p "$(reviewer_prompt "$id" "$file" "$report")" ) 2>&1 | tee "$log_r"
--no-session -p "$(reviewer_prompt "$id" "$file" "$report")" ) \
> "$dir/reviewer.jsonl" 2> "$dir/reviewer.stderr"
rc=$?
# 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"
extract_text "$dir/reviewer.jsonl" \
| sed -e '1{/^```/d;}' -e '${/^```$/d;}' > "$tmp" && mv "$tmp" "$review" && chmod 644 "$review"
pass_failed reviewer "$rc" "$review" "$dir/reviewer.stderr" && return 1
ru="$(usage_json "$dir/reviewer.jsonl")"
echo " reviewer: $(echo "$ru" | jq -r '"\(.tokens) tokens over \(.turns) turns"')"
write_cost "$id" "$dir" "$cu" "$ru"
vd="$(verdict_of "$review")"
case "$vd" in
*APPROVED*)
mark_done "$file"
echo "[ ok ] $id verdict=APPROVED status=$(status_of "$file")"
echo "[ ok ] $id verdict=APPROVED status=$(status_of "$file") cost=$(jq -r '"\(.total.tokens) tokens / $\(.total.costUsd)"' "$dir/cost.json")"
return 0
;;
*)
echo "[stop] $id verdict=${vd:-<none parsed>}"
echo " artifacts: $dir/"
echo " review: $review"
echo " logs: $log_c"
echo " $log_r"
echo " cost: $(jq -r '"\(.total.tokens) tokens / $\(.total.costUsd)"' "$dir/cost.json")"
return 1
;;
esac
@@ -288,12 +408,19 @@ run_task() {
case "${1:-}" in
--list) print_board; exit 0 ;;
--sync) sync_index && echo "INDEX.md synced from task files"; exit $? ;;
--cost) [ -f "$ARTIFACTS/cost-ledger.tsv" ] \
&& { column -t -s "$(printf '\t')" "$ARTIFACTS/cost-ledger.tsv"
awk -F'\t' 'NR>1 {t+=$7; c+=$8; a+=$2} END {printf "\nTOTAL %d tokens $%.4f over %d tasks (%d attempts)\n", t, c, NR-1, a}' \
"$ARTIFACTS/cost-ledger.tsv"; } \
|| echo "no cost ledger yet"
exit 0 ;;
--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; }
command -v jq >/dev/null || { echo "jq not on PATH (needed to parse --mode json)"; 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