Files
improvise/.agents/skills/wave/agent-rules.md
T
Edward Langley cc113c732f docs(context): answer the review of the repo-map split
- repo-stats reads the gotcha list from context/gotchas.md (the digest in
  repo-map.md no longer measures the doc-growth signal) and reports it as
  `docs.gotchas`; the test fixture and its derived expectations follow.
- AGENTS.md and the wave agent-rules protect gotchas.md and
  file-inventory.md like the repo map they were split from.
- doc-site.md's publication table lists the two new pages.
- file-inventory.md's Context docs rows describe the three files correctly.
- the code-review skill points at gotchas.md for the long form.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-09 21:42:24 -07:00

416 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <branch> 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...<branch>`.** 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/<branch>`. 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 <n> --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 Beads "Session Completion" block near the end of `AGENTS.md` does not
set your git policy.** This repository does not opt in to the block's
"team-maintainer" profile. On a wave branch that profile's pull and bare
`git push` would be refused (rule 4), or worse. Rules 1–5, 7, 8 and 9 are your
git policy. The block is bd's and bd regenerates it whole, so it is not
edited by hand: `AGENTS.md`, "Repository policy over the Beads blocks" (improvise-x1uk).
7. **Pin `gh/main` to a literal SHA when the task starts.** Run
`git rev-parse gh/main` once. Use that SHA, not the ref name, for every
merge, merge-base and diff. Remote refs are shared by every worktree of the
repository, so `gh/main` moves whenever another session fetches. Re-resolve
only on purpose, for example when you mean to merge a newer main. Rule 3's
check before a PR is such a time.
8. **Reconcile diverging history only by merge.** To take a newer main,
re-pin on purpose first (rule 7): `git fetch gh`, then `git rev-parse
gh/main`. Then run `git merge <new SHA>`. Merge commits and new
commits are what the rule wants.
**The default is a new commit.** The user: "As a general rule, you shuold
just add a new conventional commit. Small adjustments can ammend HEAD, but
when in doubt don't." [sic] (2026-09-25; improvise-1xfb.2). That narrows an
earlier ruling: "I don't mind amending on a branch that hasn't been pushed,
but don't do anything that requires a force push" (2026-09-25;
improvise-79h5).
- **Amend only HEAD, only for a small adjustment, and only while HEAD is
unpushed.** A typo, a file left out of the commit you just made, a
formatter's fix to it. When in doubt, add a new commit.
- **Never reorder, fold or squash commits, pushed or not**, and never move
them onto another base: no `git rebase` onto main and no `git reset
--soft gh/main`. Both incidents below were exactly that, on commits that
had not been pushed.
- **Once a commit is pushed**, nothing may rewrite it: no rebase, squash
(`git reset --soft`, `git merge --squash`, a `rebase -i` fixup), amend or
`git reset` that drops it. Anything that would need a force push is
forbidden, and so is the force push itself.
**Messages are Conventional Commits**: `type(scope): subject`, with the
scope optional. The types are Conventional Commits' own: `feat`, `fix`,
`refactor`, `perf`, `style`, `test`, `docs`, `ci` and `chore`. Do not
invent new ones. History also has ad-hoc prefixes (`tooling:`, `context:`,
`clippy:`, `checkers:`); write those as a standard type with a scope
instead, e.g. `chore(tooling):`. A ticket id goes at the end of the subject
in parentheses, as most recent conventional commits carry it: `fix(tui): the grid and the tile bar scroll
by the width they are drawn in (improvise-gtn7.1)`. A merge commit keeps
the message git writes for it. The message says what changed. The argument
for the change lives in `context/`, never only in the message
(`context/design-principles.md` §6).
The user's reason: "the merge structure of git history is a causal record"
(2026-09-25; improvise-1xfb.1). A merge keeps that record, and a rewrite
replaces it with a story. The rewrite is also where the damage comes from.
A squash or rebase computes a new commit against a ref another session can
move, and when that ref moves, the commit silently reverts whatever landed
in between. It has happened twice, and making the operation safer did not
stop the second:
- **#196:** an agent ran `git reset --soft gh/main` mid-task and staged a
reverse diff of #196. Only `git status` caught it (improvise-diho).
- **#303:** after #303 merged mid-session, the improvise-cgqt agent squashed
with `git reset --soft gh/main`. The staged commit reverted #303, and only
rule 3's numstat caught it before the push.
A cherry-pick copies a change and drops its causal link, so use a merge
wherever a merge does the job. That includes a branch that carries another
PR's unmerged commits, when that PR is open and will land as a merge
commit. Either stack on that PR, or wait for it to merge, re-pin, and merge
the new SHA. The foreign commits then leave your diff.
The one place a merge cannot help is commits that will never reach main as
they are. That means an abandoned or rebuilt PR, or commits that exist only
on a mirror or a local main (#148). Only then, cut a fresh branch from your
pinned SHA and `git cherry-pick -x` your own commits onto it (`-x` records
where each came from). Close the old branch rather than force-pushing it:
`AGENTS.md`, "Shipping a wave".
9. **Merge main as soon as you can, and keep merging it.** The user:
"Agents should merge main asap" and, when asked why, "Reduces conflicts"
(2026-10-09 UTC; bd memory `agents-merge-main-asap`, written by the
orchestrator; PR #335). Those two quotations are the user's words. The
timing below is the orchestrator's operationalisation of them.
- **When.** `git fetch gh`, then merge the new `gh/main` SHA (rule 7's
re-pin, done on purpose) at the start of a task, before the first review
request, before every push to a PR under review, and every time you
resume. Main only gets further from your base, and a conflict met early
is small. Never `origin/main`, never rebase, never squash (rule 8).
Commit the merge through `scripts/agent-git`.
- **After every merge of main, clean or not**, re-run the gates (see
"Gates") on the merged tree, not on each side, and diff the test names
against your pre-merge HEAD to see that none was lost. A clean merge is
the dangerous case: it can delete a test module or break a shared test
file with no conflict marker.
- **Re-measure your baseline** after a mid-task merge, and report it
against the merged SHA, so main's changes are not read as yours.
- **Resolve a conflict by hand.** For each conflicted file, diff the
result against both parents and the merge base, so a side's
non-conflicting changes are not lost to a whole-file `--ours` or
`--theirs`.
- **A stacked PR merges its base branch, not `gh/main`.** GitHub diffs it
against that base, so merging a `gh/main` ahead of the base puts main's
commits in its diff (the #145/#148 symptom, which rule 3's numstat
cannot see). The orchestrator retargets it to main once the base lands.
- **The exception is narrow.** A durability push of WIP (see "Commit
early") need not wait for a merge of main that conflicts. If you cannot
resolve the conflict yet, push the WIP to a separate wip branch, never
to the PR.
- **Each merge-and-push moves the head SHA** an in-flight review is aimed
at. Reviewers re-fetch the tip before posting, but do not merge main
only to push a reply.
- The mechanics are `AGENTS.md`, "Shipping a wave" (merge `gh/main`, never
`origin/main`) and "Merging many PRs at once" (diff each parent; two
clean changes to one test file can still fail together). Not restated here.
## 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. Make each step its own conventional commit, and leave it
that way: steps are not reordered or folded together afterwards. A small fix
to the step you just committed may amend it while it is unpushed. Anything
more, or any doubt, is a new commit (rule 8).
## Gates
```
direnv exec . ./scripts/fmt-workspaces.sh # NOT cargo fmt --all; exit 2 = the gate did not run
direnv exec . ./scripts/check-harness-lock.sh # offline; a Cargo.toml change can stale harness/opfs-contract's lock
direnv exec . cargo clippy --workspace --all-targets -- -D warnings
direnv exec . cargo test --workspace --no-fail-fast
direnv exec . python3 scripts/check-comment-runs.py --self-test
direnv exec . python3 scripts/check-comment-runs.py --check-evidence # also answers the comment cap
direnv exec . python3 scripts/check-ci-graph.py --self-test
direnv exec . python3 scripts/check-ci-graph.py
direnv exec . ./scripts/checkers.sh # every suite in scripts/, its own self-test included
direnv exec . tpl prolog-sessions/kb.pl -g "(kb_consistent -> halt(0) ; (kb_report, halt(1)))"
direnv exec . ./scripts/node-test.sh 'web/tests/*.test.js'
```
This list is the `test`, `js` and `kb` jobs of `.github/workflows/ci.yml`,
step for step, and this file is the one place it is written down. Change it in
the same PR that changes CI. The other CI jobs are conditional, below, or left
out on purpose:
- **`wasm`**: when you touch a crate it checks (next bullet).
- **`harness-lint`**: when you touch `harness/opfs-contract`, run
`cargo clippy --target wasm32-unknown-unknown --all-targets -- -D warnings`
from that directory. Its format half is already `fmt-workspaces.sh`.
- **`build` and `browser`**: when you touch `web/`, `harness/`, or the surface
the page calls in `improvise-wasm-client` or `improvise-worker-server`
(third bullet).
- **`pages-deploy`** is left out. It deploys, and it runs only on a push to
`main`. What guards it locally is `check-ci-graph.py`, which is in the list.
- **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.
`fmt-workspaces.sh` is the one most often lost this way: an unformatted tree
exits **1** and prints a diff, and `| tail` shows the diff's last lines and
exits 0. Read its exit status, or run it with `--fix` and commit what it
rewrites.
- **CI takes every tool from `nix develop .#ci`, and so should your scripts**:
no `/bin/…` or `/usr/bin/…` paths, and no bash-version or tool-version
probes (`AGENTS.md`, "CI takes its tools from the dev shell"). A test that
needs a restricted `PATH` builds one on purpose. It does not assume the
runner's `/bin` holds anything, because on the NixOS runners it holds only
`env` and `sh`.
- **`--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) or `improvise-gp`, also run
`cargo check --target wasm32-unknown-unknown -p <crate>` **for each crate**.
CI checks each one separately, so feature unification cannot hide a leak.
- If you touch `web/`, `harness/` or the page's wasm surface, run
`./harness/build.sh`, then
`./scripts/node-test.sh 'web/tests/bundle/*.test.js'`, then `npm ci` and
`npx playwright test` in `harness/`. The bundle tests sit outside the
`web/tests/*.test.js` glob and read the built `web/pkg`, so the standard list
never runs them. They failed only in CI once. A fresh worktree has no
`harness/node_modules`, and Playwright then reports a missing
`@playwright/test`, which reads like a broken checkout.
- **Playwright serves `harness/serve/`, not `web/`.** `harness/build.sh`
assembles that tree and it is gitignored, so after any change to `web/*.js`
— yours, or one a merge brought in — re-run `./harness/build.sh` before the
specs. Otherwise they test the old page. That failure looks exactly like a
real regression (`doctrine(rebuild_the_assembled_site_after_merging_web)`).
CI runs Chromium and Firefox only (`--project=chromium --project=firefox`),
so run those two locally.
- **If you change the grid's row or column count, also grep
`harness/fixtures/*.improv` and `harness/tests/*.spec.js`.** On PR #176 a
Playwright spec pinned the old collapsed-group row count. `cargo test` cannot
see that.
- **A PR that conflicts with its base gets no CI run.** So anything "left to
CI" is unchecked until the PR is mergeable (PR #177).
- **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.
- **Build each side's test list in a fresh `CARGO_TARGET_DIR`, every time.** If
a parent's tree comes from `git archive`, a reused target dir gives a wrong
answer, not just a slow one. Archive stamps every file with its commit time,
so cargo judges the sources older than the last build and lists the previous
tree's tests. On #188 that made three of the branch's own tests look
merge-only.
- **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=<your actor>` in every shell, then
`bd update <id> --claim`. Exit 13 means another actor holds it. Pick another;
do not retry.
- **Author every commit as `Claude (<BEADS_ACTOR>)`.** The user: "agents
should override git author name, I think to include an agent identifier
(matching BEADS_ACTOR)" (2026-09-25; improvise-1xfb.3). Make every commit
through the wrapper, merge commits included:
`BEADS_ACTOR=<your actor> scripts/agent-git commit …`, and the same for
`merge`. The wrapper sets the author name for that one command, and it refuses (exit 2) when `BEADS_ACTOR` is unset or empty. It
reads `BEADS_ACTOR` from its own environment, so the prefix works in a shell
that keeps nothing between commands.
- **The author email, the committer and the `Co-Authored-By` trailer stay as
they are.** The email is the owner's configured one, so GitHub still links
the commit.
- **Never set `user.name` with `git config`.** Worktrees share
`.git/config`, so it renames every agent at once. Do not use
`git -c user.name=…` either: `user.name` names the committer as well as
the author. Why the wrapper uses `GIT_AUTHOR_NAME`:
`context/test-evidence/tooling.md`.
- A plain `git commit --amend` keeps the author it amends, so amend with
plain git or with the wrapper.
- **A cherry-pick keeps its source's author**, through the wrapper or not:
git ignores `GIT_AUTHOR_NAME` there. That is right, because rule 8 allows
only `cherry-pick -x` of commits that keep whoever wrote them.
- Before you push, check `git log --format='%an <%ae> | %cn' <pinned
SHA>..HEAD`. Every commit **you made** must name your actor. A
cherry-picked commit, or one from a PR you stack on, keeps its own author.
- **The format is a proposal.** `Claude (<BEADS_ACTOR>)` is the default
proposed on improvise-1xfb.3's PR. The user may change it there. The format
lives in `scripts/agent-git` and nowhere else.
- **`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 <sha> 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.
- **Bound any test loop that walks a state machine until a step.** An
unbounded `while step != target { advance() }` hangs the suite when the
transition it counts on is broken. Loop a fixed number of times and assert
the step was reached, so the failure is a named assertion, not a timeout.
- **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/<area>.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.
- **Never resolve a conflict with `git checkout --ours` or `--theirs`.** Either
one takes the whole file from one side and silently drops the other side's
*non*-conflicting changes to it. That is how a `ci.yml` step vanished while
the merge message said it was unchanged (improvise-dndu). After resolving,
diff the merge against each parent and against the merge base
(`AGENTS.md`, "Merging many PRs at once").
- **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`, `context/gotchas.md`,
`context/file-inventory.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 <n>`, and do not pipe it. It also
shows inline comments, which is where Copilot puts most of its findings.
Copilot reviews here again (2026-09-24), so answer its findings as you answer
Codex's. How to tell a Codex review from ours: `AGENTS.md`, "Shipping a
wave".
- **As a PR's author, reply on the PR to every review finding you address.**
Name the commit that fixes it, or say why you declined it. Putting it only in
your hand-back to the orchestrator is not enough: the PR thread is where the
next reader looks.
- **As a reviewer, read the existing reviews first**, with
`./scripts/pr-reviews <n>`, unpiped. Reply on its thread to every Codex
review: fixed (name the commit), declined (say why), or already one of our
tickets (name it). Every review you post states the head SHA it reviewed.
- **Answer a review even if the PR has already merged.** Reviews arrive late,
and the thread is still where the next reader looks.
- **Do not merge, and do not close beads.** Your part ends at an open PR plus
your hand-back. What "done" means is in `AGENTS.md`, "Repository policy over
the Beads blocks". Avoid stacked PRs. If you must stack, say in the PR body which PR it is
stacked on. Once that base PR merges, check the stacked PR's `baseRefName`
(`gh pr view <n> --json baseRefName`) and, unless it is `main`, retarget it
with `gh pr edit <n> --base main` before anyone merges it: merging it
otherwise lands it in the stale base branch, not in `main`. Fifteen PRs did
that on 2026-09-24 (`AGENTS.md`, "Shipping a wave").
## 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-<you>/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.