Files
improvise/.agents/skills/wave/agent-rules.md
T
Edward Langley 94260d58b8 docs: list every Conventional Commits type rule 8 allows (improvise-1xfb.2)
Answers the Codex P2 on #306: the type list read as exhaustive and left out
refactor, perf and style, all in use on main. Ad-hoc prefixes become a
standard type with a scope, and the user's quote is marked [sic].

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 19:35:56 -07:00

21 KiB
Raw Blame History

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 and 8 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".

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.
  • 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: 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.