docs(examples): add builder-adversary-lean — context hygiene + per-gate review

Isolates the two effects conflated in builder-adversary-stateless: keeps all the
CONTEXT HYGIENE (compact/diffs/lean loads) but ENFORCES full per-gate review
granularity (one claim per gate, one independent verdict per gate, no batching).
Tests whether the token saving is real efficiency vs reduced scrutiny.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 21:42:12 +00:00
parent 985d33dd51
commit e0425e6108
8 changed files with 240 additions and 0 deletions

View File

@ -0,0 +1,30 @@
# Builder/Adversary example — context-lean + full per-gate review
The [`builder-adversary-stateless`](../builder-adversary-stateless/) variant added **context
hygiene** (compact at each checkpoint, read diffs not trees, lean loads) and, in benchmarking,
happened to also do *fewer* review rounds — so its token saving was partly leaner context and partly
*less scrutiny*. This variant **isolates the two**: it keeps all the context hygiene but **requires
full per-gate review granularity** — one `claim(<gate>)` per gate and one independent Adversary
verdict per gate, no batching.
The point: if this variant keeps most of the token saving *despite* doing as many (or more) review
passes than the original, then the saving is real efficiency (lower carried/reloaded context), not a
reduction in adversarial scrutiny.
So vs the others:
| variant | context hygiene | review granularity |
|---|:--:|---|
| builder-adversary | no | as the agents choose |
| builder-adversary-min | no | as the agents choose |
| builder-adversary-stateless | yes | as the agents choose (tended to batch → fewer rounds) |
| **builder-adversary-lean** | **yes** | **per-gate, enforced (no batching)** |
Everything else — pattern, AI-as-adversary cold verification, the `claim(`/`review(` handoff,
`machine-docs/` coordination — is identical. The `agent-orchestrator-benchmark` repo runs it
head-to-head with the others on the same multi-phase task.
```bash
python3 ../../agents.py status --config agents.toml
python3 ../../agents.py up --config agents.toml # needs `claude` on PATH
```

View File

@ -0,0 +1,92 @@
# examples/builder-adversary-lean — context hygiene + ENFORCED full per-gate review.
#
# Like builder-adversary-stateless (CONTEXT HYGIENE: compact at every checkpoint, read diffs not
# trees, spill bulk to files, adversary loads only {plan, STATUS, diff}) BUT the prompts also require
# per-gate review granularity — one claim per gate, one independent Adversary verdict per gate, no
# batching. This isolates "leaner context" from "fewer review passes". Loop agents not resumed →
# fresh session per phase. See README.md.
#
# python3 ../../agents.py status --config agents.toml
# python3 ../../agents.py up --config agents.toml # needs `claude` on PATH
[watchdog]
signal_interval = 30
heavy_interval = 300
limit_probe_fallback = 300
limit_reset_slack = 45
stall_grace = 180
[defaults]
session_prefix = "blean-" # REQUIRED — sessions: blean-builder, blean-adv, …
log_dir = ".ao-state"
backend = "claude" # set to "demo" for a dependency-free mechanics-only run
model = "claude-sonnet-4-6"
watch = "heal"
[backend.claude]
bin = "claude"
flags = "--dangerously-skip-permissions"
remote_control = true
supports_resume = true
prompt_delivery = "arg"
process_name = "claude"
submit_key = "Enter"
stall_idle = 300
active_re = "esc to interrupt|Running tool|⠇|⠙|· \\d+"
limit_re = "spend limit|usage limit|limit reached|reached your .*limit|out of (credits|tokens)"
fatal_re = "redacted_thinking|blocks cannot be modified|cannot be modified"
[backend.demo]
bin = "echo '[demo] {session} up (kickoff: {kickoff})'; exec sleep 1000000"
prompt_delivery = "exec"
[[agent]]
name = "builder" # tmux session: blean-builder
kind = "loop"
role = "builder"
dir = "./work"
watch = "heal+stall"
[[agent]]
name = "adversary"
session = "blean-adv"
kind = "loop"
role = "adversary"
dir = "./work-adv"
watch = "heal+stall"
[[agent]]
name = "orchestrator" # tmux session: blean-orchestrator
kind = "persistent"
model = "claude-opus-4-8"
resume = true
watch = "heal"
prompt = "You supervise this Builder/Adversary project. On startup: read machine-docs/ for the current phase's STATUS/REVIEW, confirm both loops + the watchdog are up, report the phase and any open findings/VETO. Then stay available; intervene only if the pair is stuck."
[[agent]]
name = "reporter" # tmux session: blean-reporter
kind = "task"
model = "claude-opus-4-8"
watch = "none"
enabled = false
prompt = "The phase sequence is complete. Read machine-docs/ across all phases, write a short machine-docs/REPORT.md (what was built, each gate's final verdict, deferred items), then go idle."
[[service]]
name = "cleanlogs"
command = "python3 ../../agent-log.py follow-all"
dir = "."
[loop]
state_file = "phase-idx"
resume_phase = true
auto_advance = true
done_marker = "## DONE"
kickoff_template = "prompts/kickoff.md"
roles_dir = "prompts"
handoff = { repo = "./work", claim_pings = "adversary", review_pings = "builder", inboxes = ["ADVERSARY-INBOX.md", "BUILDER-INBOX.md"], claim_pattern = "^claim", review_pattern = "^review", state_subdir = "machine-docs" }
on_complete = { trigger_file = ".run-report-on-complete", run = "reporter" }
phases = [
{ id = "wc", plan = "plans/wc.md", status = "STATUS-wc.md" },
{ id = "json", plan = "plans/json.md", status = "STATUS-json.md", models = { builder = "claude-opus-4-8" } },
]

View File

@ -0,0 +1,2 @@
# Coordination / loop-state files live here at runtime (phase-namespaced STATUS / REVIEW / BACKLOG /
# JOURNAL, plus the ADVERSARY-INBOX.md / BUILDER-INBOX.md side-channels). The loop pair populates it.

View File

@ -0,0 +1,32 @@
# Phase `json` — machine-readable output
**Mission.** Extend the `wc.py` from the previous phase with a `--json` mode, without regressing any
`wc`-phase behaviour. Single source of truth for this phase.
(The phase config gives the Builder `claude-opus-4-8` for this phase — an example of a per-phase
model override; the Adversary stays on the default model.)
## Definition of Done
- **D1 — json output.** `python wc.py --json FILE` prints a single JSON object:
`{"lines": N, "words": N, "chars": N, "file": "FILE"}` (valid JSON, parseable by `json.loads`).
With stdin (no FILE), `"file"` is `null`.
- **D2 — composes with flags.** `--json` honours `-l/-w/-c`: only the requested counts appear as keys
(plus `file`). E.g. `wc.py --json -l FILE``{"lines": N, "file": "FILE"}`.
- **D3 — no regression.** Every `wc`-phase gate (D1D4 there) still passes unchanged.
- **D4 — tests green.** `test_wc.py` is extended for the JSON cases and `pytest -q` is all-green.
## How the Adversary verifies (cold)
```bash
pytest -q # D4 + D3 regression
printf 'a b c\nd e\n' > /tmp/f.txt
python wc.py --json /tmp/f.txt | python -c 'import sys,json; d=json.load(sys.stdin); \
assert d=={"lines":2,"words":5,"chars":10,"file":"/tmp/f.txt"}, d; print("ok")' # D1
python wc.py --json -l /tmp/f.txt # D2: expect {"lines": 2, "file": "/tmp/f.txt"}
```
The Builder restates the exact commands, expected JSON, and commit sha in
`machine-docs/STATUS-json.md`. When every DoD item has a fresh PASS in `machine-docs/REVIEW-json.md`
and there is no `## VETO`, the Builder writes `## DONE` to `STATUS-json.md` — this is the last phase,
so the watchdog then fires the one-shot `reporter` (see `agents.toml` `[loop].on_complete`).

View File

@ -0,0 +1,43 @@
# Phase `wc` — a word-count CLI
**Mission.** Build a small, dependency-free `wc` clone in Python: a script `wc.py` in the work repo
that counts lines, words, and characters, plus a `pytest` suite. This is the single source of truth
for the phase — the Builder builds to the Definition of Done below; the Adversary cold-verifies it.
This task is deliberately tiny and fully local (no network, no services) so the example exercises the
loop-pair *protocol* — claim → cold-verify → PASS/FAIL handshake — not infrastructure.
## Definition of Done
Each Dn is an independent gate. The Builder claims it (`claim(Dn): …`); the Adversary records a fresh
PASS in `machine-docs/REVIEW-wc.md` after re-running the check from its own clone.
- **D1 — default output.** `python wc.py FILE` prints exactly `<lines> <words> <chars> <FILE>`
(counts whitespace-separated words, `\n`-terminated lines, and bytes for `chars`), matching GNU
`wc` on ASCII input.
- **D2 — flags.** `-l`, `-w`, `-c` restrict the output to that single count (e.g. `wc.py -l FILE`
prints `<lines> <FILE>`). Flags may combine; output order is lines, words, chars.
- **D3 — stdin.** With no FILE argument, `wc.py` reads stdin and prints the counts with no filename.
- **D4 — tests green.** A `test_wc.py` runs under `pytest -q` with **0 failures**, covering: an empty
file (`0 0 0`), a multi-line fixture, the no-trailing-newline case, and each flag.
## How the Adversary verifies (cold)
From a fresh clone of the work repo:
```bash
pytest -q # D4: must be all-green
printf 'a b c\nd e\n' > /tmp/f.txt
python wc.py /tmp/f.txt # D1: expect "2 5 10 /tmp/f.txt"
python wc.py -l /tmp/f.txt # D2: expect "2 /tmp/f.txt"
printf 'a b c\nd e\n' | python wc.py # D3: expect "2 5 10"
```
Expected outputs are above — the Builder must restate them (and the exact commands, plus the commit
sha) in `machine-docs/STATUS-wc.md` so the Adversary can re-run without reading the Builder's
reasoning. Any mismatch is a FAIL with repro steps in `machine-docs/REVIEW-wc.md`.
## Out of scope (defer to a later phase or DEFERRED.md)
Multibyte/`-m` char counting, `--files0-from`, multiple-file totals, locale handling. JSON output is
the next phase (`plans/json.md`).

View File

@ -0,0 +1,16 @@
You are the **Adversary**, one of two independent loops: **DISBELIEVE the Builder**. Coordinate ONLY through git. The phase plan is the SSOT for what to verify.
Loop: run `/loop` (no interval). Verify a CLAIMED gate promptly (the watchdog pings you when the Builder claims one); idle otherwise. Cap waits at 10 min; before going idle your LAST line MUST be exactly `WAITING-UNTIL: <ISO-8601 UTC>`. Compact at ~80%.
Verify cold from your OWN clone: re-run the plan's DoD check yourself and try to break it (edge cases, bad input) — don't trust the Builder's word. From STATUS take only what you need to re-run (command, expected result, shas); ignore its reasoning and don't read JOURNAL until after your verdict (it anchors you). Judge from the plan, the code, and your own run.
Git: `pull --rebase`, commit, push; never `--force`. Prefix verdicts `review(<id>): PASS|FAIL …` — pings the Builder. Write only REVIEW.md (+ your findings). Record "<id>: PASS @<ts>" + evidence, or FAIL + repro steps. You hold veto: write "## VETO <reason>".
REVIEW GRANULARITY (required): verify every claimed gate in its OWN independent cold pass and write a separate `review(<gate-id>): PASS|FAIL` per gate — never batch verdicts, never skip a gate. The CONTEXT HYGIENE below governs only HOW you load context (compact, diffs), NOT how much you scrutinise: keep full per-gate rigor and your break-it probes.
CONTEXT HYGIENE — your durable state is REVIEW + git, so the conversation is disposable scratch; keep it small so you don't pay to reload it every turn:
- Per gate, load only what you need to judge it: the plan, the Builder's STATUS, and the diff since the last verified sha (`git diff <sha>..HEAD`). Don't re-read the whole repo or earlier gates.
- After writing each verdict (a durable checkpoint), run `/compact` — lossless here; you reload from REVIEW + git.
- Spill bulk to files: pipe long verification/test output to a file and read back only the part you need.
Begin: read the plan, then enter the loop (clone the work repo into your dir if it exists yet).

View File

@ -0,0 +1,19 @@
You are the **Builder**, one of two independent loops; coordinate ONLY through git. Read the phase plan (the SSOT) and build to its DoD.
Loop: run `/loop` (no interval), one unit of work per wake. Cap every wait at 10 min; before going idle your LAST output line MUST be exactly `WAITING-UNTIL: <ISO-8601 UTC>` (≤10 min out) or the watchdog reboots you. Compact at ~80% context.
Git: `pull --rebase`, smallest change, commit, push; never `--force`. Prefix a gate claim `claim(<id>): …` — the watchdog pings the Adversary on it; use `feat/fix/status/…` otherwise. Before you claim, the tree MUST be clean (committed AND pushed): the Adversary cold-verifies from a fresh clone.
REVIEW GRANULARITY (required): claim each DoD gate INDIVIDUALLY — one `claim(<gate-id>)` per gate, the moment that gate is met. Do NOT batch several gates into one claim. Granular claims keep the Adversary's verification thorough (one independent cold pass per gate).
STATUS (in machine-docs/) must give the Adversary: WHAT is claimed (gate id + DoD items), HOW to verify (exact command), the EXPECTED result, WHERE (commit shas/paths). Reasoning goes in JOURNAL, NOT STATUS — the Adversary won't read JOURNAL before judging. Write only your files (code, STATUS, JOURNAL, build backlog); REVIEW is the Adversary's.
Done: write "## DONE" only when REVIEW shows a fresh PASS for every DoD item and there's no "## VETO". Never weaken/skip/delete a test; verify for real, no "should work".
CONTEXT HYGIENE — your durable state is git + STATUS/JOURNAL, so the conversation is disposable scratch; keep it small so you don't pay to reload it every turn:
- After each gate is committed+pushed (a durable checkpoint), run `/compact` — it's lossless here, you reload what you need from git + STATUS.
- Read DIFFS, not trees: `git diff <last-sha>..HEAD` and only the files you're touching; don't re-read the whole repo.
- Spill bulk to files: pipe long build/test output to a file and read back only the part you need — don't dump it into the conversation.
- On a fresh wake, reconstruct from the plan + STATUS + a diff; don't rebuild context by re-reading everything.
Begin: read the plan, then enter the loop.

View File

@ -0,0 +1,6 @@
*** PHASE {phase_id} ***
Plan (this phase's single source of truth): {plan} — read it fully now; it defines the mission and the Definition of Done (DoD).
Loop state goes under machine-docs/ (create if missing), phase-namespaced: {status}, REVIEW-{phase_id}.md, JOURNAL-{phase_id}.md, BACKLOG-{phase_id}.md. Never at the repo root.
Done = the Builder writes "## DONE" to machine-docs/{status} ONLY after every DoD item has a fresh Adversary PASS in machine-docs/REVIEW-{phase_id}.md.
=== role ===