- docs/commit-map.tsv: 4,032 rows (ordinal of the ORIGINAL main -> rewritten hash, author/committer dates, subject); 1 pruned row of zeros (ordinal 1712, "session archive update"); 0 old hashes asserted; ordinal 1 unchanged by the rewrite (byte-identical) - resolve_tokens: 1,238 commit:NNNN tokens -> shortest-unique new hashes in 98 files (docs, phase-ends, logs, tool docstrings, 2 C comments, the A5 evidence logs); residue left as tokens: commit:1712 x4 (the pruned commit), commit:orphan-24 x2, commit:orphan-26, commit:orphan-35 (cited commits that exist in no lineage) - the rewrite (C4): filter-repo 2.47.0 on a bare clone of the C2 tip, 311 s, exactly 1 pruned, main 4,032 -> 4,031; the pre-rewrite history is mirrored in the private archive repo and in the local bundle - the proof (C5): verify_rewrite 4,031 pairs / 0 failures; absent_scan 0 offenders; gate_scan 0 offenders on the clone - adoption (C6): 100 text files differ at the tip, 0 purge paths, 0 added/deleted; leftover refs dropped; no gc yet - resolver skips tools/public_rewrite/ (its self-test fixtures are the token grammar, not citations); repo-local identity is the GitHub noreply address from here on; CURRENT_PHASE: C4–C7 logged, checkpoint -> NEXT = C8
16 KiB
Automation runbook — HISTORICAL: the OpenRouter / ox-alpha autonomous campaign
⛔ THIS IS NOT THE CURRENT PIPELINE. See
docs/wave-playbook.md.Superseded 2026-08-31 (P31 S67). This file documents the OpenRouter (ox-alpha) era automation, built to sustain a free-window API quota. Its drafting toolchain (§2) is OpenRouter, and its lanes —
drafter/gater/maintenance/stallguard/distill/main— are all DEAD, deliberately.tools/campaign_status.pywill confirm that.The project returned to the Claude agent wave system on purpose. Running a wave today means
draw_waves→t5_cards→claude_wave_packs→wave_args→claude_wave_draft.js→gate_wave.py→twin_sweep→ harvest → R22, which is written up start-to-finish, with the measured failure behind each guard, indocs/wave-playbook.md.Kept because its mechanism notes are still accurate for the parts that survived — the banking toolchain by binary class (§4), the main lane's batch cadence (§5), the distill flywheel (§6), and the recovery procedures (§8) — and because the rate-limit measurements (§7) are the record of why that era ended.
Rewritten 2026-08-24 (P31 S59). The previous version described the June reach-1 grinder pivot and
was two months stale: it named no lane that exists today. If this file and the lane scripts ever
disagree again, the scripts are the truth — tools/lanes/*.sh are the tracked copies of what runs.
Companions: docs/SETUP.md (per-tool reference table, versions, install), docs/accelerators.md
(ops traps that cost real time), docs/matching-cookbook.md (the matching knowledge the drafters grep).
1. What runs right now
Six detached lanes, each setsid-launched from .run/<name>.sh (a copy of tools/lanes/<name>.sh):
| lane | script | what it does | safe to restart? |
|---|---|---|---|
| drafter | .run/drafter.sh |
draw → shard → draft → queue a ready marker, forever | Never stop it to ship a change. Doing that cost 139 of 162 idle minutes on 2026-08-23. Restart only at a wave boundary (see §3) |
| gater | .run/gater.sh |
reloc pre-filter → gate → commit → harvest → ledger | yes, freely — nothing is lost but the pause |
| maintenance | .run/maintenance.sh |
free zero-token work when the gater is idle: the A-prop sibling sweep and recover_rejects.py (rebase pre-gate rejects whose body already matches and only the symbols are wrong — §171; 45% of drafts never reach the gate and 13% of those are recoverable) |
yes |
| stallguard | .run/stallguard.sh |
60 s: revive a dead lane shell, kill agents silent >20 min, kill a gate >90 min, bounce an idle drafter | yes |
| distill | .run/distill.sh |
watch harvested candidates, raise a READY marker when a batch is worth review | yes |
| main | .run/main.sh |
the EXE's own draft→gate→commit cadence (§5) | yes — gate_main reverts its own aborts |
# is everything alive?
for l in drafter gater maintenance stallguard distill main; do
printf "%-12s %s\n" "$l" "$(pgrep -f "bash .run/$l.sh" >/dev/null && echo alive || echo DEAD)"; done
pgrep -cf '^\.venv/bin/python -u tools/api_agent' # drafting agents right now
touch .run/ox_campaign.stop # THE KILL SWITCH — every lane exits at its next loop top
rm .run/ox_campaign.stop # then relaunch the lanes you want
2. The drafting toolchain (OpenRouter)
tools/ox_campaign.py --drafter draws a wave, shards it, and spawns one tools/api_agent.py per
card. Shard count tracks CARDS, not the --workers cap: a 71-card wave runs 71 agents however
many workers are configured. Card supply, not throughput, is the binding constraint.
-
Models.
--models 'stealth/ox-alpha:2000'— ox is free for this window. The paid deepseek lane was dropped 2026-08-24 at $2.22 remaining (§7). Restoring it is two edits, named in the drafter script's header. -
The card is the fuel.
tools/build_wave_atlas.pywrites.run/wave_<tag>_cards.json:fn · binary · nins · lever · model · tu · seed_ref · tu_ref · decl_prior.api_agent._fuelrenders it, andLEVER_CRIBexplains what each lever MEANS plus the cookbook § to grep — a bare lever label is a dead end (108 transcripts greppedextend-tellagainst a cookbook containing that string zero times). -
Budgets are per lane (
ox_campaign.LANE_BUDGET): tells gets 40 turns / $0.40, everything else 24 / $0.15. One global cap starved the large cards — 98 of 270 tells attempts ended AT the cap. -
Model routing inside a wave is by function size (cookbook §157): Haiku ≤30 ins → Sonnet 50-120 → Opus ≥120. Fable is for new wall classes only — never for idiom distillation or review.
-
Output budget and socket timeout are ONE setting (P31 S59, probed directly against ox-alpha):
knob value why --maxtok16000 the model's thinking is IN the content stream ( reasoning_tokens=0), so the output cap WAS the reasoning cap. Measured over ALL turns (the denominator matters — an early count compared truncated turns only against turns that printed a finish reason, i.e. against themselves, and read as ~100%): wavebkat 8k truncated 240 of 3,222 turns = 7.4%; wavebtat 16k truncated 16 of 1,210 = 1.3%, a ~6x reduction. A truncated turn is a TAX, not a death — the log shows the agent emitting a tool call on the very next turn — so it costs one turn of 24. An uncapped hard prompt wanted 8,067 tokens, finishing exactly where the old cap cut itHTTP_TIMEOUT700 ox generates at ~30 tok/s, so a full 16k generation needs ~530 s. At the old 420 s the socket would kill the very turns the bigger budget exists to allow — and a timeout wastes the whole turn where truncation leaves a partial model ceiling 1M context / 131,072 max completion so 16k is OUR choice, not a limit. The binding constraint is the timeout, not the model The ordering that must hold: generation <
HTTP_TIMEOUT(700) < stallguard's wedged-agent kill (1200 s). 420 was itself deliberate — 1800 once parked a hung agent for thirty minutes. A reasoning cap (REASON_CAP→reasoning:{max_tokens}) does work on ox, but it shortens the ANSWER too (618-672 tokens against 8,067 uncapped): a quality dial, not a truncation fix. -
Turn caps are NOT binding on the default lane. Across 1,166 completions, non-MATCH runs used a median of 4 oracle calls and a p90 of 12 against 24 available; exactly 1 of 194 reached 20. Agents give up early after truncated turns — which is why the fix above is the budget, not the cap. (The tells lane WAS cap-bound, 98 of 270, hence its 40.)
What the draw admits, and what it refuses (all counted in the skip census)
| filter | why |
|---|---|
o0-in-an-O2-object |
an -O0 function in an -O2 object cannot bank however good the draft (§261). 11 such were drawn 79 times across 19 waves before this existed |
tells-oversize-serial-lane |
tell-lever members > --tells-max-ins (80). Bank rate 27-40% at 5-80, 10% at 81-120, 1% at 121-200, 0% above. Those 383 members are idiom_serial's work |
jtbl-* (main-manual, island-blocked, island-pads) |
the gate's carve cannot reach that table yet (jtbl_carve.island_probe) |
jtbl-one-per-binary |
§61c: one table-bearing draft per gate invocation |
already-waved / already-banked / out-of-band |
ordinary pool bookkeeping |
One lane, one band (P31 S59): every wave is a full-range default draw. The tells slot became redundant when the quota landed, and the dedicated 120-2000 slot was the worst wave we ran — bank rate 3% at 120-200 and 6% above, against 57% under 50 ins. Large functions still appear: the full band contains them and the draw takes mass-first inside each gate group.
Quotas reserve cards that the gate-group ranking would otherwise never pick, because they are
spread thin across binaries: --tells-quota 60, --jtbl-quota 6. A quota is a floor AND a
ceiling — tells enter through the quota or not at all (left free, they took 41% of a test wave).
3. Changing something while it runs (read this before editing a lane)
bash parses a while … done body in full before executing it, so the loop's command line is
fixed for the life of that shell (docs/accelerators.md #5):
| change | how it takes effect |
|---|---|
lane code (tools/*.py) |
next python start — bounce the python, args are unchanged |
lane args (the .sh invocation) |
needs a fresh shell — tools/lanes/relaunch_drafter_shell.sh waits for a wave to queue first, so no drafts are lost |
wave-draw defaults (build_wave_atlas.py) |
next draw — it is a fresh subprocess per wave, no restart at all |
| the gater's args | tools/lanes/restart_gater_when_idle.sh — restarts once no sweep is in flight |
| the main lane's args | tools/lanes/restart_main_lane_when_idle.sh — restarts in the gap between its gate and its next draw |
Verify from the PROCESS, never the file: tr '\0' ' ' < /proc/<pid>/cmdline, or the startup banner.
4. The banking toolchain, by binary class
A bank is byte-identical instructions AND a green whole-binary SHA1. Nothing else counts (G3/P9).
| class | path | notes |
|---|---|---|
| overlays / md | sweep_parallel → gate_stage → harvest_verify |
per-binary flock .run/auto/gate.<bin>.lock; distinct binaries gate concurrently |
| main | gate_main.py ONLY |
incremental builds give a FALSE diff (main's extract rewrites the linker script). sweep_parallel REFUSES main (R43) after wave ab banked 0 of 105 |
| jtbl functions | the carve happens at the gate (harvest_verify._jtbl_prep_one), never pre-draw |
§61b's proven order; jtbl_lane.py is the serial route |
| -O0 functions | ordinary gates, but only from an -O0 object | the Makefile decides per object; corpus.o0_subseg() is the oracle, not the subseg name |
Standing verification: R22 = a CLEAN rebuild (make clean && make extract-all && make check-all,
~1 min parallel). Do not run it while gates are building; scope it to the binary you touched with
rm -rf build/<bin> build/src/<bin> && make extract BINARY=<bin> && make check BINARY=<bin>.
R42 — never revert a dirty src/ or config/. Other lanes bank real, byte-proven functions with
commit=False, and no tool can tell them from residue; a blind git checkout destroyed 61 banked
functions once. Commit named paths, or leave the tree alone.
5. The main lane (P31 S59)
main is excluded from every overlay wave draw (--exclude-bins main) because its gate is a clean
whole-EXE rebuild that bisects — three measured stalls put it off the critical path. It now has its
own lane instead of no cadence at all:
.run/main_queue/*.json (parked drafts, free) ─┐
build_wave_atlas --only-bins main ─┴→ draft → reloc filter → gate_main --apply
→ verify main byte-identical → commit (R42)
- Batches of 40: one clean rebuild verifies the whole slate (~15 s measured), which is what makes main affordable. The reloc pre-filter keeps the batch from bisecting.
- On a
COMPILE conflictthe lane halves the slate and retries —gate_maindeliberately refuses to bisect a decl clash (right for a human, wrong for an unattended lane: 40 innocent drafts died with one conflicting symbol that was in the TU and in no draft). - Failed drafts are parked with a try count (
.run/main_queue_failed.json), capped at 2. - Credit requires two oracles: the
INCLUDE_ASMline gone from the tree AND main re-checked byte-identical. The first version askedcorpus.stubs('main')— which returns{addr: Stub}, keyed by INT — so a name-vs-int comparison reported "12 banked of 12" from a gate that banked nothing. - Every cycle opens with
gate_main --assert-baseline(S59): one clean no-draft rebuild proving HEAD still builds byte-identical BEFORE any tokens are spent. On red the lane refuses (R43), writes.run/main_lane.BASELINE_RED, and re-checks every 30 min.gate_mainruns the same control on any batch failure and exits 3 without bisecting when the TREE, not the slate, is broken (R40). Why: auto-commit24bd25993adopted a mid-flight substitution at 14:57 and the lane then banked 0 from four 200-card rounds until 18:43 — every rejection a false verdict. Full story:docs/tool-designs/main-lane-fix-s59.md. - main's TUs (top-level
src/*.c) have ONE writer (gate_main) and ONE committer (main_lane) (S59). Every overlay/maintenance adopter (ox_campaign×3,maintenance.sh,gate_stage,gate_lane,idiom_serial) unstages them before committing and never reverts them — the old revert-then-add carve-out was the losing half of the TOCTOU that produced24bd25993.
6. The distill lane and the flywheel
The gater harvests every wave before the next draw (idiom_harvest.py) into
.run/idiom_candidates.<tag>.md. That is EXTRACTION. Distillation into the cookbook is what
changes the next wave, because that is what the agents grep.
.run/distill.shraises.run/distill_ready/<batch>.jsonat ≥30 novel candidates or ≥2 waves.- State is
{tag: novel-count-when-mined}— never a done-list, because a re-gated wave rewrites its candidate file under the same tag. - The reviewer is an Opus or Sonnet subagent, never Fable (Drew, 2026-08-24).
- Measured yield, twice: ~82-88% of candidates are already covered, ~1 new law + ~10 addenda per 80 candidates, and one in three credited levers is byte-inert (§266 — strip it and recompile before writing the law).
- Wave N's ore is distilled while wave N+1 drafts, so wave N+2 is the first that can grep it. That one-wave lag is the price of never pausing the fleet.
7. Rate limits, credits, and the measured ceiling
| fact | number |
|---|---|
| lifetime requests / 429s | 121,166 / 7,667 = 6.3% |
| 429 attribution | 100% provider shared capacity (openrouter_shared_capacity, upstream_provider_shared_pool) — zero platform refusals, ever |
| steady state | 50-95 req/min → 0-3.5% 429s |
| the knee | 195-227 req/min → 6.6-12.7% 429s |
| one-minute peak observed | 2,755 requests |
| per-agent request rate | 0.31-0.78 req/min (mean ~0.6) — so ~350-380 agents sits at the knee |
| retry policy | MAX_429=10 with 20 s backoff (default 6). A 429 costs a retry; an idle agent costs a card |
Telemetry: every request appends to .run/api_rate.jsonl; read it with tools/api_rate.py.
Bucket 429s over time before calling anything a ceiling — launch bursts fake one.
Credits. credits_left() reads the OpenRouter balance. --credit-floor does not pause the
paid lane, it breaks the whole drafting loop — and the shell then restarts a python that breaks
again. With a free model the balance is not a proxy for "can we draft", so the floor is 0.25.
8. Recovery
git status --porcelain -- src/ config/— if dirty, COMMIT it, never revert (R42). The one safe revert issrc/800.c/src/800c.calone when agate_mainwas interrupted mid-substitution.- Salvage drafts:
find .run/wave_* -name '*.c' | wc -l— drafts survive crashes and are worth re-gating viaox_campaign.py --gate-only <tag>before drafting anything new. - Relaunch lanes:
setsid nohup .run/<lane>.sh >> .run/<lane>.log 2>&1 </dev/null &, and verify from the STARTUP BANNER, not the file. - Drafting is the clock-limited resource (the free-model window) — start it FIRST, do bookkeeping while it runs.
Never pkill -f a lane by a bare name: it matches the harness's own bash -c "… eval '…'"
wrapper and kills the command issuing it (three times, twice fatally). Anchor the pattern
('^bash \.run/drafter\.sh') or collect PIDs first and kill them in a separate call.