1The workflow: three phases, one fresh window each
Fresh-window-per-phase is the context-control layer. Everything a later window needs travels through GitHub labels, the issue body, and the local ledger — never through a chat transcript.
/build
File the claim. /build "does signed_ema α=0.4 hold parity at cadence 10/10?"
→ issue #N, labeled. Close the window.
/plan N
Plan is written into the issue body, then the window rolls straight into the
human gate. Ends approved — or parked at awaiting:approval.
/execute N [--gpu auto|ask]
Prepare (laptop) → compute → monitor → analyze → close, to status:done +
box torn down + report published + local footprint cleaned.
2How to prompt it — worked examples
Each command takes plain language; the harness derives titles, slugs, and labels. These are copy-paste starting points.
Filing (window 1) — with and without an explicit type
The bracketed kind: is optional. Explicit wins; otherwise /new-issue
infers it (GPU comparison → experiment, variation of a PASSed parent →
ablation, code-only → implementation, GPU-free offline study →
analysis, open-ended → brainstorm). The kind decides the lifecycle
shape — analysis runs no GPU; implementation/brainstorm/literature close on the plan/PR itself —
so state it whenever inference could guess wrong.
/build "does signed_ema α=0.4 hold parity at cadence 10/10?"
# kind inferred → experiment
/build "sweep anchor β over {0.0,0.25,0.5} on the PASSed #63 setup" kind:ablation
/build "add a code-execution reward adapter for DeepCoder (sandboxed tests)" kind:implementation
/build "offline: is consecutive-update cosine predictive of projector gain?" kind:analysis
/build "reading list: RLVR-linearity papers relevant to one-layer training" kind:literature
Planning (window 2) — spend ALL uncertainty here
/plan 64 # fast tier: single hypothesis, one launch round
/plan 64 deep # multi-stage; add constraints as plain lines, e.g.:
Stage 1 is a 2-step probe on the cheapest rung; gate stage 2 on val@25 ≥ 0.70.
Budget cap 24 GPU-hr. If Big-Math OOMs, shrink MAX_RESPONSE_LENGTH before
escalating the ladder.
Deep tier is the sanctioned place for judge panels, clarifying questions, and
codex-verify. After approval there are no review loops anywhere.
Executing (window 3) — pick how the GPU appears
/execute 64 # gpu_mode from the plan / project default (auto)
/execute 64 --gpu auto # hands-off: provisions itself once CPU gates are green
/execute 64 --gpu ask # prepare everything, then PAUSE "READY FOR GPU"
# …come back later with the box you rented (login registered automatically):
/execute 64 --attach 43811234
# …or change your mind and let it provision:
/execute 64 --gpu auto
# account + attach at the stage level too:
/launch 64 --attach 43811234 --account team
Operator-managed side box (never auto-reaped)
bash .claude/skills/vast-attach/run.sh --exp-id 64-ema-sweep --issue 64 \
--instance-id 43811234 --manual # analysis/download box you own end-to-end
3The 7 stages underneath (never merged, never skipped)
The phase commands are a UX layer. The state machine is these stages —
each a self-contained skill, each flipping its own label. /go N resumes from
whatever labels + ledger say and runs what remains.
| stage | command | does | label set |
|---|---|---|---|
| 1 file | /new-issue "…" [kind:…] | issue + research:claim + kind:* | — |
| 2 plan | /plan N [deep] | plan → issue body; digest | status:planned |
| 3 gate | /approve N | your yes/no — the one human decision | status:approved |
| 4 launch | /launch N [--gpu auto|ask] [--attach id] [--account team|private] | PREPARE on the laptop (branch exp/N-slug pushed, patch, payload, one CPU pass) → COMPUTE (provision-or-attach, tmux, ledger row, run.json) | status:running |
| 5 watch | /monitor N | cheap health poller cycles; Opus classifier on anomaly; teardown the moment results sync | — |
| 6 judge | /analyze N | verdict.md, WandB tail backfill | status:pass|revise|stop |
| 7 finish | /close N | teardown check → SUMMARY row + PR → merge → checkboxes ticked → close comment (verdict SSOT) → report published to this site → local cleanup sweep | status:done |
Overview any time: /status (prints to stdout — there is deliberately no
status file to go stale).
4Where state lives (one local session file, nothing else)
| artifact | role | class |
|---|---|---|
| GitHub labels | the state machine (status:*, needs:human, awaiting:approval) | SSOT |
| GitHub issue body | the claim + the plan (between plan markers) + ticked checkboxes | SSOT |
| issue close comment | the per-issue verdict record | SSOT |
| report page + R2 | the rich per-run record + bulk artifacts (autonomous-harness-rlvr-compression/<id>/) | SSOT |
.claude/state/runs.jsonl | compute state + bounded-retry counters (locked, shared by all worktrees; compacted per issue at close) | SSOT |
runs/SUMMARY.md | one row per issue — the offline fallback index | durable digest |
runs/<id>/run.json | launch-time snapshot — monitor/analyze/close read THIS, never the plan (deleted at close) | volatile |
.claude/state/plan-cache/ | fetched copy of the issue-body plan (deleted at close) | volatile cache |
PROGRESS.md | THE one session file: capped tick echo + the end-of-session checklist; an agent reads it once before ending a window — the issue's ticks are swept at close | session-scoped |
invariant (tested)
A terminal issue's preconditions never require its runs/<id>/ dir. /close's cleanup
sweep (scripts/close_cleanup.sh) deletes it automatically once four guards pass:
status:done · no live ledger row across ALL rows · SUMMARY row present · the report
page exists. scripts/test_debloat_invariant.sh proves the degradation (20 checks).
5Plans live in the GitHub issue — nowhere else
The planner publishes the plan into the issue body between
<!-- plan:start --> / <!-- plan:end -->. There are no local plan
files — the only thing on disk is a gitignored fetched cache. Machine fields sit in the plan's
first ```yaml fence, parsed by _lib.sh plan_field.
- Edit the plan on GitHub freely — your edits are authoritative; every gating
stage (
/plan,/approve,/launch) re-fetches before acting. - Checkbox discipline: at close, every success-criteria / progress box the verdict
proves is ticked in place (
plan_tick— literal substring match, refuses to push a truncated body). A closed issue never shows unticked boxes; a box the verdict marks ✗ stays unticked and is named in the close comment. - After launch the plan is snapshotted to
runs/<id>/run.jsonand never read again — execution is decoupled from the plan.
6GPUs: hands-off vs ask-me (and bring-your-own-box)
Implementation always FINISHES on the laptop before any box exists
(verification.gpu_idle_rule): the runner's PREPARE phase — branch, patch, launch
payload, one bounded CPU sanity pass — completes before provisioning starts, in every mode.
A GPU is never up while code is being authored; real on-policy numerics are only ever tested
on the GPU.
| mode | what happens | when to use |
|---|---|---|
--gpu auto | PREPARE green → runner walks the gpu_filter_chain ladder (≤3 rungs, 1 retry each) and provisions itself | hands-off overnight runs; the /approve gate already authorized the spend |
--gpu ask | PREPARE green → issue pauses with a needs:human READY FOR GPU comment naming the exact resume commands | you want to supply the box (a machine you rented, a teammate's) or time the spend yourself |
--attach <id> | vast-attach ssh-probes the box, registers login + ledger row, runner skips provisioning | resuming an ask-pause, or launching straight onto your own box |
vast-attach --manual | ledger EXTERNAL: tracked by vast-cost, never auto-reaped | operator-managed analysis/download boxes |
Resolution order for the mode: CLI flag → plan gpu_mode: key → project.yaml
default_compute.gpu_mode (auto). Every ssh/rsync offers the registered key
(project.yaml vast_ssh) — bare ssh fails publickey.
7Two-tier watching: cheap eyes, expensive judgment
The default watcher is machine-monitor (Sonnet): purely mechanical
30-second health polls — ssh/tmux liveness, per-GPU util, global_step advance, disk,
error-pattern grep, WandB heartbeat. Zero diagnosis by design. The Opus
training-log-monitor is dispatched once per anomaly, in classify mode — you pay
for judgment only when judgment is needed.
| classified state | /monitor acts |
|---|---|
done | confirm metrics rsynced → teardown NOW → /analyze |
experiment-failure | the failure IS the data — let cells finish, sync, teardown, analyze |
probe-fix-needed | bounded on-box fix (fix_attempts ≤ 3), relaunch the cell |
env-failure | teardown → next ladder rung (launch_attempts ≤ 3); attached boxes are never auto-replaced |
stall / unclear | sync evidence → teardown → needs:human — an unexplained box never keeps billing |
Hard rule: no adversarial verification, judge panels, or exploratory workflows during a run. Mid-run doubt = flag + stop for a human go/no-go.
8Results & publishing: the record leaves the laptop
/close runs a strict order, each step idempotent — so a crash anywhere resumes cleanly:
- 1 teardown check (money before paperwork), then SUMMARY row + launcher promotion + results PR → squash-merge onto the base branch.
- 2 tick the plan's checkboxes, then post the close comment (the verdict SSOT, with
the deterministic report URL), then flip
status:done. - 3 publish (
scripts/publish_run_report.py): one self-contained HTML page → Experiment Runs on this site, card inserted on the tab, small artifacts → the repo's gitignoredartifacts/<id>/, bulk artifacts → R2autonomous-harness-rlvr-compression/<id>/, then commit + push (the push IS the Cloudflare Pages deploy). A partial publish exits nonzero →needs:human, and cleanup is skipped. - 4 cleanup sweep: run dir, plan cache, stale handles, ledger compaction, PROGRESS ticks — all gone. The checkout keeps only the SUMMARY row.
/de-bloat (human-only, DEBLOAT_OPERATOR_ACK=1) remains as the manual
batch fallback for leftover dirs.
9Parallel issues = parallel windows in worktrees
claude --worktree 64-ema-sweep # session A, issue 64
claude --worktree 65-cadence-20 # session B, issue 65 — zero cross-contamination
- State paths always resolve to the primary checkout (
_lib.shand the publish script both anchor viagit worktree list), so all sessions share exactly one ledger (writes are lock-guarded). - The autosave Stop-hook commits only that session's branch.
- One issue per window. Never two sessions driving the same issue.
10Pause signals & failure modes
Durable pause = a label on the issue. needs:human (with the reason as
an issue comment) and awaiting:approval.
| symptom | meaning / fix |
|---|---|
| a stage refuses | it printed the named reason + the right next command — that's the design |
| READY FOR GPU comment | the ask-mode pause: resume with /execute N --attach <id> or /execute N --gpu auto |
needs:human label | read the comment, decide, re-run /execute N (it clears the label after you confirm) |
awaiting:approval | open a window, /approve N |
plan or runs/<id>/ deleted | commands degrade to labels + ledger; /go N names what's recoverable |
| box not torn down | bash .claude/skills/vast-teardown/run.sh <instance_id> |
| everything must stop NOW | touch ~/.claude-kill-switch — pauses every agent tool call; rm to resume |
11Money: reaping, burn, budget
backstops, in order
1 · /monitor tears down the moment results sync (the primary path — don't wait for backstops).
2 · Stop-hook reaper: verdict written / heartbeat stale 30 min / never-heartbeat 60 min / budget cap / stuck-PROVISIONED / orphan handles.
3 · Hourly cron reaper (session-independent): manage with
bash .claude/hooks/install-reaper-cron.sh [--status|--remove].
/statusorbash .claude/skills/vast-cost/run.sh— live $/hr on both accounts + untracked-box leak check.python scripts/check_budget.py --month— spend vsproject.yaml budget:caps.EXTERNALrows and boxes belonging to other users are never auto-destroyed.
12The hook suite (the harness's reflexes)
Hooks run without any agent deciding to run them. Config:
.claude/settings.json; scripts: .claude/hooks/. Every external call inside
them is timeout-bounded and none can block a session.
| hook | fires on | job |
|---|---|---|
kill-switch.sh | PreToolUse | instant pause of ALL agent tool calls while ~/.claude-kill-switch exists |
protect-upstream.sh | PreToolUse | refuses writes to upstream verl/ paths; exceptions only exp/* and autonomous-harness-* branches; research/ always writable |
sync-metrics.sh | PostToolUse (Bash) | pulls live log tails; heartbeat = content ADVANCE, not mtime — a dead-but-reachable box goes stale and gets reaped |
commit-on-stop.sh | Stop | autosaves uncommitted research/ work to the session's own branch |
teardown-finished-runs.sh | Stop | the money reaper (triggers above); EXTERNAL rows never touched; wrapper failure is FAILURE, never "already gone" |
on-session-start.sh | SessionStart | logs running-box count + live $/hr burn the moment a window opens |
13Design doctrine (what must survive refactors)
- Labels + ledger first, files second. Deleted files ⇒ named refusal or terminal derivation, never a retry loop. Run dirs are evidence, not state.
- Every retry is a ledger counter (
launch_attempts ≤ 3,fix_attempts ≤ 3,revise_depth ≤ iterations, monitor ≤ 12 cycles). Exhaustion ⇒needs:human, never a spin. - Verification doctrine: unbounded deliberation before approval; NONE autonomous during execution. Mid-run doubt ⇒ flag + stop.
- CPU-first, GPU-last: implementation finishes on the laptop; a box is provisioned last and never idles under code authoring; CPU checks are one bounded pass.
- Cheap eyes, expensive judgment: the mechanical watcher is the cheap model; the classifier is Opus, on demand. Every reasoning agent stays on the best model.
- Publish, then delete: the durable record is GitHub + this site + R2; the checkout keeps one SUMMARY row and one session file. Nothing else survives a close.
- One name everywhere:
<N>-<slug>is the run dir, ledger id, Vast label, WandB group, and branch suffix; cells are self-describing kebab slugs. - Self-hosting branch discipline: every issue gets
exp/<id>fromproject.yaml source_tree.base_branch, pushed before launch; /close PRs results back to that same base onshamanez/verl— never upstream, never main. No branch name is hardcoded in a skill or agent.
AAppendix
Unattended multi-day run (optional — not the default path)
/bg /goal Issue 64 is terminal: status:done and box TORN_DOWN — or the issue
gained a needs:human/awaiting:approval label. Each turn: run /go 64 once,
print status + evidence. Stop after 120 turns.
Always include the label escape + a turn bound. The default workflow needs neither: one issue per window, human nearby.
Manual baseline launch (bypass the harness)
Provision with .claude/skills/vast-provision/run.sh (locked template
verl-research-vllm020 — see project.yaml vast_template), ssh in, run
examples/grpo_trainer/vast_baseline_qwen25_1p5b_grpo_gsm8k.sh.
One-time prerequisites
ls -l ~/.config/verl-research/secrets.env # expect -rw------- (never echo values)
gh repo set-default --view # expect shamanez/verl-compression-research
which claude gh vastai uv jq aws