diff --git a/.agents/skills/wave/SKILL.md b/.agents/skills/wave/SKILL.md index dffa68a2..fbb9bd74 100644 --- a/.agents/skills/wave/SKILL.md +++ b/.agents/skills/wave/SKILL.md @@ -7,6 +7,8 @@ description: Dispatch parallel agents to work through open beads and shepherd ea An orchestrator chunks the open backlog, fans agents out, reviews what comes back, and merges. `AGENTS.md` has the history; this is the operating procedure. +This file is the brief checklist's one home, and `agent-rules.md` beside it +holds the rules each agent follows. ## Every agent gets its own worktree. No exceptions. @@ -17,9 +19,8 @@ here. If you discover it mid-wave, stop every agent, have each create a worktree and copy across **only files it can prove it touched**, then `git checkout --` those files in the primary tree. -In the worktree: `direnv allow`, or `bd` reports a version *mismatch* rather than -failing cleanly. `direnv status` prints `Found RC allowed 0`, and `0` means -allowed — it reads like the opposite. +In the worktree `.envrc` must be allowed, or `bd` fails confusingly: +`agent-rules.md`, "Discipline per ticket". ## Partitioning @@ -33,32 +34,41 @@ it must cross. Two agents adding a workspace member will still collide in ## What every brief must carry +**Point the agent at [`agent-rules.md`](agent-rules.md) and tell it to read it in +full first.** That file is the agent's standing rules: base and push mechanics, +the gate list, the claim, the vacuity shapes, evidence, the files it must not +write, GitHub conventions, the hand-back and disk cleanup. Do not restate them +in the brief. A restated rule drifts from its source, and the brief is the copy +nobody corrects. The brief adds only what varies per wave: + 1. **The expected base commit, by name.** Worktrees do not reliably branch from - HEAD — one was created three merges behind and only noticed because it was - told to check. -2. **The baseline**, so "N passing" is checkable rather than asserted: Rust test - count, harness counts, node counts. -3. **Its ground, and the ground that is not.** -4. **Never write** `.beads/**`, `prolog-sessions/kb.pl`, `context/repo-map.md`. - Agents read them freely and report proposed text; the orchestrator applies it. -5. **The commit trailers and PR footer**, spelled out. -6. **Do not push to `main`, do not merge.** The orchestrator merges. -7. **What to report**: branch and head SHA, PR number, what changed per ticket, - *what could not be verified and why*, proposed repo-map/kb text, and **what it - learned that the ticket did not predict** — consistently the most valuable - part, and omitted unless asked for explicitly. + HEAD. One was created three merges behind and was only noticed because it + was told to check. The agent checks against it: `agent-rules.md` rule 1. +2. **A reference baseline**, so "N passing" is checkable and a drop is + visible: Rust test count, harness counts, node counts, **and the machine**. + The same commit once measured `improvise-io`'s binary at 1.4s on one machine + and 3.7–5.2s on another, so a figure with no machine is a claim about the + suite, which it is not. The agent still measures its own. +3. **Its ground, and the ground that is not.** Name any orchestrator-only file + it is exceptionally allowed to edit. +4. **Its `BEADS_ACTOR` and its tickets.** +5. **The attribution line, commit trailers and PR footer**, spelled out. + They name the agent (Claude's line opens `_Posted by Claude ()…_`) + and this session's URL, which is why the shared `agent-rules.md` cannot + hold them. +6. **An explicit ask for what the ticket did not predict.** `agent-rules.md` + lists it in the hand-back. Say it in the brief anyway: it is consistently + the most valuable part of a hand-back, and agents omit it unless asked. -## Method to require, because this repo's defect is a test that passes +When a hand-back teaches a new rule, add it to `agent-rules.md` in the same +wave, not to the next brief. -- Failing test first, then the fix. -- Every guard proved by **injecting the fault it names** and watching it fail - **by name**. An injection that does not compile reads as a clean run, so - require a positive signal the build ran. -- `cargo test --workspace` is **fail-fast**; `--no-fail-fast` for any claim about - blast radius. -- A textually clean merge can **delete tests** with no conflict marker, and - `cargo check` will not notice. After any merge that moves a test module, diff - test **names** — a count is masked by the other side's additions. +## Method to require + +`agent-rules.md`, "Discipline per ticket" and "Gates": failing test first, a +rival injected and watched failing by name, `--no-fail-fast`, and test *names* +diffed after any merge. Hold the hand-back to it. This repo's characteristic +defect is a test that passes. ## Shepherding @@ -72,7 +82,9 @@ it must cross. Two agents adding a workspace member will still collide in commit reads as a pass. - Merge on GitHub, then `git fetch gh && git merge --ff-only gh/main`. - Anything posted to GitHub goes out under the owner's account: open every - comment with a line saying a Codex agent wrote it. + comment with a line naming the model and saying an agent wrote it. That + wording is agent-neutral because this one file serves Claude and Codex + (`.claude/skills/wave` is a symlink to `.agents/skills/wave`). ## Between waves diff --git a/.agents/skills/wave/agent-rules.md b/.agents/skills/wave/agent-rules.md new file mode 100644 index 00000000..28245bc9 --- /dev/null +++ b/.agents/skills/wave/agent-rules.md @@ -0,0 +1,198 @@ +# Standing rules for a wave agent + +Every work-unit agent reads this **in full before its first command**. A brief +points here and adds only what is specific to its wave: the expected base +commit, the agent's ground, its `BEADS_ACTOR`, and its tickets. Every rule below +was bought by a failure. + +This file holds each agent-side rule **once**. The brief checklist is +`SKILL.md` beside it. Where `AGENTS.md` ("The Wave Workflow") carries a longer +history, the rule here points there. Change a rule here, not in a brief, and +not in a second copy. + +## Base, branch, push + +1. **Verify your base against the commit your brief names, first.** Run + `git log --oneline -1` and compare. Worktrees do not reliably branch from + the orchestrator's HEAD: one was created three merges behind, and it was + only noticed because the agent was told to look. +2. **Branch from `gh/main`, never `main` or `origin/main`.** `git fetch gh && + git checkout -b gh/main`. A worktree handed to you may already sit + on unpushed commits. Why: `AGENTS.md`, "Shipping a wave" (the two remotes + both spelled `main`, and the worktree-base vector). +3. **Before opening a PR, check `git diff --numstat gh/main...`.** Do + not use `gh pr view --json files`: that is the union of your commits' files, + not the effective diff. +4. **Push with an explicit refspec:** `git push gh HEAD:refs/heads/`. A + branch cut from `gh/main` tracks `gh/main`, so a bare push targets `main` and + is refused. +5. **Before pushing a fix to an existing PR, check `gh pr view --json + state`.** GitHub deletes a merged PR's branch. A push to it then prints + `* [new branch]` and lands nowhere. Why: `AGENTS.md`, "Shipping a wave". +6. **The "Session Completion" block near the end of `AGENTS.md` does not apply + to you.** It says `git merge origin/main` and then a bare `git push`. On a + wave branch the first is the #145 trap and the second is refused (rule 4). + Rules 1–5 replace it. The block sits between bd's generated + `BEGIN/END BEADS INTEGRATION` markers, so it is not edited by hand + (improvise-x1uk). + +## Commit early: a worktree is not durable storage + +Commit a WIP as soon as your first test run is green, and again after every +meaningful step. Push your branch (rule 4) once it is worth keeping. An agent +finished a whole ticket with every gate green and lost all of it, uncommitted, +when its worktree was deleted in a disk cleanup. A local commit survives a +deleted worktree. Tidying history before the PR is fine; losing it is not. + +## Gates + +``` +direnv exec . cargo test --workspace --no-fail-fast +direnv exec . cargo clippy --workspace --all-targets -- -D warnings +direnv exec . ./scripts/fmt-workspaces.sh # NOT cargo fmt --all; exit 2 = the gate did not run +direnv exec . python3 scripts/check-comment-runs.py --check-evidence +direnv exec . ./scripts/checkers.sh +direnv exec . ./scripts/node-test.sh 'web/tests/*.test.js' +``` + +- **Never `cargo fmt --all`.** It reaches only **one** of this repo's two + workspace roots, because `harness/opfs-contract` declares its own + `[workspace]`. The obvious repair is worse: `cargo fmt -p opfs-contract` from + the repo root exits **1**, *"not a member of the workspace"*, which looks + exactly like a real formatting failure (improvise-b4ks). Both spellings fail + in the direction that looks like success. +- **Run every gate under `direnv exec .`.** `./scripts/checkers.sh` scrubs the + environment to an allowlist, so a bare run drops `tpl` off `PATH` and reports + spurious failures. +- **Never pipe a gate.** `$?` must be the gate's own. And + `check-comment-runs.py --check-evidence` prints two lines with the comment + cap **first**, so `| tail -1` silently drops half of it. +- **`--no-fail-fast`**, because plain `cargo test --workspace` stops at the + first failing binary. Use it for any claim about blast radius. +- If you touch a browser-bundle crate (core, formula, io, protocol, command, + wasm-client, worker-server), also run + `cargo check --target wasm32-unknown-unknown -p ` **for each crate**. + CI checks each one separately, so feature unification cannot hide a leak. +- If you touch `web/` or `harness/`, run the Playwright suite in `harness/`. +- **Measure your own baseline** before changing anything, and report it with + the machine it was measured on. A figure from the brief is a reference, not + your baseline. +- **Merge `gh/main` and re-run every gate before calling a PR green.** Then + check that no test *disappeared*. Diff test **names**, not counts: a + textually clean merge can delete a test module with no conflict marker, and + the other side's new tests mask the loss in a count. +- **The shell guard refusing a compound command means NEITHER part ran.** Split + the command. After a refusal, re-check the state it was meant to produce. +- **Re-run the full suite after reverting an injected fault.** A revert applied + to the wrong occurrence once shipped two faults past every other gate. + +## Discipline per ticket + +- **Claim first:** `export BEADS_ACTOR=` in every shell, then + `bd update --claim`. Exit 13 means another actor holds it. Pick another; + do not retry. +- **`bd` needs `.envrc` allowed in your worktree.** Without it you get a + **version mismatch**, not a clean failure. Run `direnv allow` in the worktree, + or rely on the `[whitelist] prefix` in `~/.config/direnv/direnv.toml`. + `direnv status` reports `Found RC allowed 0`, and `0` means **allowed** (the + enum is 0=Allowed, 1=NotAllowed). It reads like the opposite. +- **Reproduce at HEAD before fixing.** A dozen tickets were stale-open in one + campaign, one fixed two days before it was filed. If it does not reproduce, + find the fixing commit, confirm with `git merge-base --is-ancestor HEAD`, + report the SHA, and close nothing. +- **Write a failing test first, then inject a rival and watch it fail.** Run + it; do not reason about it. Report *where* it failed, by case name. An + injection that does not compile reads as a clean run, so get a positive + signal that the build ran. +- **Check your test against every vacuity shape seen here:** + - an assertion the default state already satisfies; + - `f(x) == f(x)`: calling a pure `Cmd` twice against one immutable context; + - a disjunctive assertion whose other branch is the bug's own symptom; + - a rival that fails the *setup*, which proves the neighbouring question + (e.g. setup and subject share an `EffectKind`); + - a fixture that is right by accident (expected value 1, group at row 0, + bystander already on the relayed value); + - a fixture or stub that lacks the structure the hazard needs, or fails in a + wider shape than the real fault; + - hand-picked fixture cases where the set could be **enumerated**: four + vacuities in `fmt-workspaces-test.sh` were each a case nobody thought of + (improvise-90ea); + - a rendered-output test at a width where the thing asserted is never drawn; + - a fold test that batches differently from the real producer (a + `Binding::Sequence` is ONE effect vec); + - **the cursor at index 0**: "the item under the cursor" and "the first item" + are indistinguishable there, and that was four of five findings in one + review. Seat the cursor at a nonzero index of a list with at least two + elements; + - a determinism check against ONE context instance: every `HashMap` iterates + the same way twice. Build two equal contexts independently + (improvise-hzjo); + - two derivations of one fact where the fixture makes them agree (drawn vs + committed, edit text vs value, `1200` vs `1.23456789`). Use a value where + they disagree. +- **Record each bug in `context/test-evidence/.md`**, keyed by test name + (module-qualify the name when the leaf is not unique). It is append-at-EOF, so + **expect a conflict**. Keep both sides and verify line by line against both + parents: `--check-evidence` cannot see a dropped entry. +- **Comment runs are at most two lines.** Reasoning goes in `context/`, not in + comments (`context/design-principles.md` §6). +- **If the acceptance criteria look wrong, argue it** rather than following + them silently. Three agents inverted theirs in one campaign, and all three + were right. + +## Files you must not write + +`.beads/**` (never commit `issues.jsonl`, never `git checkout` it), +`prolog-sessions/kb.pl`, `context/repo-map.md`: `AGENTS.md`, "Who owns what". +Also `AGENTS.md`, unless your brief grants you a named section. Report proposed +text for any of these in your hand-back. + +## GitHub + +- `gh` is shell-aliased to a different tool. Use + `nix shell nixpkgs#gh --command gh …`, and pass bodies with `-F body=@file`. +- **Everything posted goes out under the repository owner's account.** Open + every PR body and comment with the attribution line your brief gives you, + verbatim. It names your agent and model, which is how our posts are told + apart from an automated Codex review (`AGENTS.md`, "Shipping a wave"). +- The attribution line, commit trailers and PR footer all come from your brief. + They name the agent and the session, so this shared file cannot hold them. +- Read reviews with `./scripts/pr-reviews `, and do not pipe it. How to tell + a ChatGPT Codex review from ours, and why Copilot is not coming: `AGENTS.md`, + "Shipping a wave". Answer each Codex review on its thread. +- **Do not merge, and do not close beads.** Avoid stacked PRs. If you must + stack, never let the base branch be deleted. + +## Hand-back, per ticket + +Report: + +- your branch, head SHA and PR number. Review is dispatched against the SHA; +- what changed, per ticket; +- the claim result; +- your measured baseline; +- whether the ticket reproduced (the SHA if it did not); +- the rival and where it failed; +- what you could not verify, and why; +- proposed repo-map and `kb.pl` text; +- **what the ticket did not predict**, consistently the most valuable part. + +## Disk + +Each worktree's `target/` grows to 3–6G, and a full disk (ENOSPC) once stopped +every agent. **When you hand back, delete your own build output, and only +yours.** + +Run `pwd` and `git rev-parse --show-toplevel` first, and read the answer. Then +delete by **literal absolute path**, typed out: +`rm -rf /Users/.../.claude/worktrees/agent-/target`. + +**Never interpolate a computed path into `rm -rf`.** One agent's worktree had +been removed, so `$(git rev-parse --show-toplevel)` printed nothing and the +command became `rm -rf "/target"`. A guarded one-liner was tried, but the +worktree shell guard refuses it as too complex, which pushed agents back to an +unguarded form. A literal path is the version that is both allowed and safe. + +Keep your branch and any uncommitted work. Delete only `target/` (and +`harness/opfs-contract/target` if you built it). If you see ENOSPC, stop and +report rather than deleting anything outside your worktree. diff --git a/.claude/skills/wave b/.claude/skills/wave new file mode 120000 index 00000000..0c2044da --- /dev/null +++ b/.claude/skills/wave @@ -0,0 +1 @@ +../../.agents/skills/wave \ No newline at end of file diff --git a/.claude/skills/wave/SKILL.md b/.claude/skills/wave/SKILL.md deleted file mode 100644 index 91b1ef74..00000000 --- a/.claude/skills/wave/SKILL.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -name: wave -description: Dispatch parallel agents to work through open beads and shepherd each to a merged PR. Use when an orchestrator is running a wave, partitioning the backlog, or writing a brief for a work-unit agent. ---- - -# Running a wave - -An orchestrator chunks the open backlog, fans agents out, reviews what comes -back, and merges. `AGENTS.md` has the history; this is the operating procedure. - -## Every agent gets its own worktree. No exceptions. - -Dispatch with `isolation: "worktree"`. Without it every agent edits the **same** -working tree: one `git status` shows four agents' half-finished files, any commit -sweeps up the others' work, and any `git checkout` destroys it. This has happened -here. If you discover it mid-wave, stop every agent, have each create a worktree -and copy across **only files it can prove it touched**, then `git checkout --` -those files in the primary tree. - -In the worktree: `direnv allow`, or `bd` reports a version *mismatch* rather than -failing cleanly. `direnv status` prints `Found RC allowed 0`, and `0` means -allowed — it reads like the opposite. - -## Partitioning - -By **crate or directory**, never by ticket count. **Tickets that share a file go -to one agent**, to be done sequentially — four widget migrations all touching -`src/draw.rs` are a guaranteed conflict split four ways. Group first, then count. - -Tell each agent what ground is *not* theirs, and to report rather than guess when -it must cross. Two agents adding a workspace member will still collide in -`Cargo.toml`/`Cargo.lock`; keep both sides. - -## What every brief must carry - -1. **The expected base commit, by name.** Worktrees do not reliably branch from - HEAD — one was created three merges behind and only noticed because it was - told to check. -2. **The baseline**, so "N passing" is checkable rather than asserted: Rust test - count, harness counts, node counts. -3. **Its ground, and the ground that is not.** -4. **Never write** `.beads/**`, `prolog-sessions/kb.pl`, `context/repo-map.md`. - Agents read them freely and report proposed text; the orchestrator applies it. -5. **The commit trailers and PR footer**, spelled out. -6. **Do not push to `main`, do not merge.** The orchestrator merges. -7. **What to report**: branch and head SHA, PR number, what changed per ticket, - *what could not be verified and why*, proposed repo-map/kb text, and **what it - learned that the ticket did not predict** — consistently the most valuable - part, and omitted unless asked for explicitly. - -## Method to require, because this repo's defect is a test that passes - -- Failing test first, then the fix. -- Every guard proved by **injecting the fault it names** and watching it fail - **by name**. An injection that does not compile reads as a clean run, so - require a positive signal the build ran. -- `cargo test --workspace` is **fail-fast**; `--no-fail-fast` for any claim about - blast radius. -- A textually clean merge can **delete tests** with no conflict marker, and - `cargo check` will not notice. After any merge that moves a test module, diff - test **names** — a count is masked by the other side's additions. - -## Shepherding - -- Wait on checks with `scripts/pr-watch --expect `. Never hand-roll a - poll; see the `pr-watch` skill for the three ways they report green while wrong. -- Review before merging. Copilot's quota may be exhausted and its request is a - **silent no-op**; an absent review and an unrequested one both read as - "pending". Dispatch an adversarial reviewer and give it the head SHA, the base, - and a test-merge, so it reviews the tree that will exist. -- A re-review is of the **delta**, at a named SHA. A review of a superseded - commit reads as a pass. -- Merge on GitHub, then `git fetch gh && git merge --ff-only gh/main`. -- Anything posted to GitHub goes out under the owner's account: open every - comment with a line saying a Claude agent wrote it. - -## Between waves - -Run the code-review skill on the merged range. The characteristic find here is -not a wrong line, it is **a test that passes while the thing it names is -broken** — a vacuous fold test, a one-category fixture, a helper that fabricates -what the code under test needs. Ask the reviewer to inject the fault and watch -the check fail. diff --git a/AGENTS.md b/AGENTS.md index 935a7099..4be26a9d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -100,52 +100,19 @@ Two agents each adding a workspace member will still collide in ### Every wave agent's brief must include -1. **Verify your base commit first.** Worktrees do **not** reliably branch from - the orchestrator's HEAD — one was created three merges behind and only - noticed because it was told to look. `git log --oneline -1` against a named - commit, reset onto `main` if behind. Name the expected commit in the brief. -2. **The baseline test count**, so "1090 passing" is checkable rather than - asserted, and a drop is visible. Tell the agent to **measure its own** and - cite the machine: the same commit measured `improvise-io`'s binary at 1.4s on - one machine and 3.7–5.2s on another, so a figure with no machine is a claim - about the suite, which it is not. -3. **The gate list, and it must not say `cargo fmt --all`.** That reaches only - **one** of this repo's two workspace roots — `harness/opfs-contract` declares - its own `[workspace]` — and the obvious repair is worse: `cargo fmt - -p opfs-contract` from the repo root exits **1**, *"not a member of the - workspace"*, which is indistinguishable from a real formatting failure - (improvise-b4ks). Both spellings fail in the direction that looks like - success. The gate is: +The wave skill holds both halves, each in one place (improvise-1f8e): - ``` - direnv exec . ./scripts/fmt-workspaces.sh # exit 2 = the gate did not run - ``` +- **The brief checklist** — what an orchestrator must put in a brief — is + `.agents/skills/wave/SKILL.md`, "What every brief must carry". +- **The agent's standing rules** — base and push, gates, claim, vacuity shapes, + evidence, forbidden files, GitHub, hand-back, disk, commit early — are + `.agents/skills/wave/agent-rules.md`. A brief points there rather than + restating them. - Every gate goes through `direnv exec .` — `./scripts/checkers.sh` scrubs the - environment to an allowlist, so a bare invocation drops `tpl` off `PATH` and - reports spurious failures. And **never pipe a gate**: `$?` must be the gate's - own, and `check-comment-runs.py --check-evidence` prints two lines with the - comment cap **first**, so `| tail -1` silently drops half of it. -4. **Commit frequently, so the work is safe in git.** A WIP commit after the - first green test, and again after every meaningful step. A worktree is not - durable storage: an agent finished a whole ticket with every gate green and - lost all of it, uncommitted, when its worktree was deleted during a disk - cleanup. Tidying the history before the PR is fine; losing it is not. -5. **Do not merge to `main`.** The orchestrator merges. Pushing the agent's own - branch (`git push gh HEAD:refs/heads/`) is encouraged once it is - worth keeping. -6. The commit-message trailers, spelled out. -7. **What to report**: branch and commits, what changed per ticket, *what could - not be verified and why*, and proposed repo-map/`kb.pl` text. Ask explicitly - for the thing the agent learned that the ticket did not predict — that is - consistently the most valuable part of a hand-back, and agents omit it unless - asked. - -`bd` works in any worktree, but `.envrc` must be allowed there or you get a -**version mismatch** rather than a clean failure — `direnv allow` in the -worktree, or rely on the `[whitelist] prefix` in `~/.config/direnv/direnv.toml`. -Note `direnv status` reports `Found RC allowed 0`, and `0` means **allowed** -(the enum is 0=Allowed, 1=NotAllowed). It reads like the opposite. +`.claude/skills/wave` is a symlink to `.agents/skills/wave`, so Claude and Codex +read the same files. A new rule goes into one of those two files, not here and +not into the next brief: a restated rule is the copy nobody corrects, and this +list was once a third copy that drifted from the skill's. ### Between waves