cc113c732f
- 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>
704 lines
38 KiB
Markdown
704 lines
38 KiB
Markdown
- Always use tests to demonstrate the existence of a bug before fixing the bug.
|
||
- If you suspect that a bug exists, use a test to demonstrate it first:
|
||
- prefer unit tests testing a small amount of code to integration or e2e tests
|
||
- Option<...> or Result<...> are fine but should not be present in the majority of the code.
|
||
- Similarly, code managing Box<...> or RC<...>, etc. for containers pointing to heap data should be split
|
||
from logic
|
||
- @context/repo-map.md is your "road map" for the repository. use it to reduce exploration and keep it updated.
|
||
- @context/design-principles.md is also important for keeping the repository consistent.
|
||
- reconcile diverging history only by merge: the merge structure of git history is a causal record (the
|
||
user, 2026-09-25). "As a general rule, you shuold just add a new conventional commit. Small adjustments
|
||
can ammend HEAD, but when in doubt don't." (the user). So the default is a new Conventional Commit. Amend
|
||
only an unpushed HEAD, and only for a small fix. Never reorder, fold, squash or rebase commits, and never
|
||
force-push. The rule, the message convention and the incidents are
|
||
`.agents/skills/wave/agent-rules.md`, rule 8.
|
||
- always start responses with bananaS!
|
||
- **Knowledge base: use `scripts/kb`, not Prolog MCP.** The Prolog MCP is optional and not
|
||
available in all agent stacks (Cursor cloud agents do not have it). All agents must use
|
||
`scripts/kb add|replace|rm|query|check` for KB operations — see the `kb` skill at
|
||
`.agents/skills/kb/SKILL.md`. Reading `prolog-sessions/kb.pl` directly is fine for queries.
|
||
- save structured facts as you go — work done, interesting locations, important decisions — with
|
||
`scripts/kb add` (see the `kb` skill). Write a fact when you learn it; there is no reason to batch.
|
||
- query the knowledge base as needed and use Prolog reasoning when it makes sense.
|
||
- Prolog should be used as a design tool to reason about the code and the implementation of tickets and to
|
||
persist a formal model of it.
|
||
|
||
# Agent Instructions
|
||
|
||
This project uses **bd** (beads) for issue tracking. Run `bd prime` for full workflow context.
|
||
|
||
## Multi-Stack Agent Support
|
||
|
||
This repo supports multiple coding-agent stacks: Claude Code, Cursor (IDE and cloud), and
|
||
potentially others. All stacks share the same operational setup:
|
||
|
||
| Component | Location | Notes |
|
||
|---|---|---|
|
||
| Toolchain | `nix develop` / `direnv` | **Required.** Rust from flake, wasm target, beads pin, trealla |
|
||
| Skills | `.agents/skills/` | Canonical source; `.claude/skills/` and `.cursor/skills/` symlink here |
|
||
| Session bootstrap | `scripts/agent-bootstrap.sh` | Seeds nix inputs, installs bd, syncs tracker, warms cargo |
|
||
| Knowledge base | `scripts/kb` | **All stacks use this**, not Prolog MCP |
|
||
| Issue tracker | `bd` CLI | Synced from Dolt remote at session start |
|
||
|
||
### Toolchain Requirement
|
||
|
||
**Agents must work inside `nix develop` (or direnv) for this repo.** Bare system cargo that
|
||
fails `edition2024` or missing wasm target is a misconfigured session. The bootstrap seeds
|
||
nix flake inputs automatically (GitHub tarballs are blocked in cloud), so `nix develop`
|
||
should work after install completes.
|
||
|
||
Run commands inside the dev shell:
|
||
```bash
|
||
nix develop --command cargo test
|
||
nix develop --command cargo build --workspace
|
||
```
|
||
|
||
Or enter an interactive shell: `nix develop`
|
||
|
||
**Claude Code**: `.claude/hooks/session-start.sh` calls the bootstrap automatically.
|
||
|
||
**Cursor cloud**: `.cursor/environment.json` runs the bootstrap via `install`. Skills are
|
||
discovered from `.cursor/skills/` (symlinks to `.agents/skills/`).
|
||
|
||
**Local agents**: Run `scripts/agent-bootstrap.sh` manually, or use `direnv allow` for
|
||
automatic dev shell activation.
|
||
|
||
See `context/cloud-agents.md` for environment-specific notes (nix, network, etc.).
|
||
|
||
### GitHub Attribution (Hard Rule)
|
||
|
||
**Every GitHub write (PR, comment, review reply) must include an attribution line identifying
|
||
the AI agent.** Posts go out under the repository owner's account, and an unattributed post
|
||
reads as the human owner's words to everyone who sees it.
|
||
|
||
**Format:** Open the body with an italicized attribution line:
|
||
|
||
```
|
||
_Posted by <Agent Stack> (<model>)_
|
||
```
|
||
|
||
Examples:
|
||
- `_Posted by Claude (Opus 4.5)_`
|
||
- `_Posted by Cursor Agent (claude-sonnet-5)_`
|
||
- `_Posted by Claude Code_`
|
||
|
||
For PR bodies, the `🤖 Generated with …` footer that some tools add covers this. For
|
||
comments and review replies, add the line yourself — there is no automatic footer.
|
||
|
||
**This applies to:** PR creation, PR updates, issue comments, review comments, and review
|
||
replies. A thread is not "answered" until the reply carries this attribution.
|
||
|
||
### PR Tracking: Checks AND Reviews
|
||
|
||
For any PR an agent owns or is working on, **track both CI checks and GitHub comments/reviews**.
|
||
The `pr-watch` skill (`.agents/skills/pr-watch/SKILL.md`) is the agent-facing recipe.
|
||
|
||
**`scripts/pr-watch`** now prints reviews after checks are green (use `--no-reviews` to skip).
|
||
Reviews are a separate API surface from comments — Codex/ChatGPT reviews arrive as *reviews*,
|
||
not comments, and are easy to miss if you only read the comment thread.
|
||
|
||
1. **Run `scripts/pr-watch <n>`** — waits for checks, then prints all reviews/comments
|
||
2. **Read all review feedback** — reviews against an earlier head may still apply
|
||
3. **Answer substantive feedback with attribution** before merging (see format above)
|
||
4. **Re-check after each push** that new reviews target the current head SHA
|
||
5. **Don't wait indefinitely on Copilot/Codex** — reviews arrive as quota allows
|
||
|
||
To fetch reviews directly: `./scripts/pr-reviews <n>` — **do not pipe it** (exit code trap).
|
||
|
||
See the `pr-watch` skill for the full recipe, and "Shipping a wave" below for Copilot
|
||
request mechanics and how to answer declined findings.
|
||
|
||
## Quick Reference
|
||
|
||
```bash
|
||
bd ready # Find available work
|
||
bd show <id> # View issue details
|
||
bd update <id> --claim # Claim work atomically
|
||
bd close <id> # Complete work
|
||
bd dolt push # Push beads data to remote
|
||
```
|
||
|
||
## Non-Interactive Shell Commands
|
||
|
||
**ALWAYS use non-interactive flags** with file operations to avoid hanging on confirmation prompts.
|
||
|
||
Shell commands like `cp`, `mv`, and `rm` may be aliased to include `-i` (interactive) mode on some systems, causing the agent to hang indefinitely waiting for y/n input.
|
||
|
||
**Use these forms instead:**
|
||
```bash
|
||
# Force overwrite without prompting
|
||
cp -f source dest # NOT: cp source dest
|
||
mv -f source dest # NOT: mv source dest
|
||
rm -f file # NOT: rm file
|
||
|
||
# For recursive operations
|
||
rm -rf directory # NOT: rm -r directory
|
||
cp -rf source dest # NOT: cp -r source dest
|
||
```
|
||
|
||
**Other commands that may prompt:**
|
||
- `scp` - use `-o BatchMode=yes` for non-interactive
|
||
- `ssh` - use `-o BatchMode=yes` to fail instead of prompting
|
||
- `apt-get` - use `-y` flag
|
||
- `brew` - use `HOMEBREW_NO_AUTO_UPDATE=1` env var
|
||
|
||
## The Wave Workflow
|
||
|
||
Most work here is done by **waves**: an orchestrator chunks the open backlog,
|
||
fans several agents out into their own git worktrees, and shepherds each
|
||
agent's pull request through review to a merge on GitHub. This section is what
|
||
that costs to get right. Everything in it was learned by getting it wrong first.
|
||
|
||
### Who owns what
|
||
|
||
Three files are **orchestrator-only**, and the rule exists because they are
|
||
guaranteed conflicts — every agent has a reason to touch them and they merge
|
||
badly:
|
||
|
||
| File | Why |
|
||
|---|---|
|
||
| `.beads/` (incl. `issues.jsonl`) | bd owns it. See "Never hand-edit" below. |
|
||
| `prolog-sessions/kb.pl` | Written by `scripts/kb` only. See `context/code/kb-tool.md`. |
|
||
| `context/repo-map.md` | Every agent wants to add a gotcha; every wave would conflict. |
|
||
| `context/gotchas.md`, `context/file-inventory.md` | The long forms the map digests; same conflict, same owner. |
|
||
|
||
Wave agents **read** all of them freely (`bd show`, `bd ready`, grep `kb.pl`) and
|
||
**write** none of them. They report proposed repo-map lines and `kb.pl` facts as
|
||
*text* in their hand-back, and the orchestrator applies them. Repo-map lines
|
||
are batched at the end of the wave, after the review, because they conflict.
|
||
`kb.pl` facts are not batched: the orchestrator writes each one when it arrives.
|
||
**`kb.pl` is written by `scripts/kb add|replace|rm` and by nothing else** — not
|
||
the Prolog MCP, not an editor. Every write is gated on `kb_consistent` against
|
||
the candidate, so a bad delta leaves the file untouched, which is why a fact
|
||
can be written the moment it is learned. There is a `kb` skill; read it before
|
||
your first write. Reading is unaffected — `loadSession` at session start is
|
||
still how a session gets the model in hand.
|
||
|
||
### Partitioning a wave
|
||
|
||
Partition by **crate or directory**, not by ticket count, and tell each agent
|
||
explicitly which ground is *not* theirs. Two rules that fall out of the ones we
|
||
broke:
|
||
|
||
- **Tickets that share a file belong to one agent.** The four TUI widget
|
||
migrations all touch `src/draw.rs`; split across agents they are a guaranteed
|
||
conflict, so they go to one agent to do sequentially. Group first, then count.
|
||
- **An agent that must stray says so rather than guessing.** Give the boundary
|
||
and ask for a report when it has to be crossed.
|
||
|
||
Two agents each adding a workspace member will still collide in
|
||
`Cargo.toml`/`Cargo.lock`. That one is expected; keep both sides.
|
||
|
||
### Every wave agent's brief must include
|
||
|
||
The wave skill holds both halves, each in one place (improvise-1f8e):
|
||
|
||
- **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.
|
||
|
||
`.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
|
||
|
||
Run the **code-review skill** on the merged range and iterate until its backlog
|
||
is empty. This is not ceremony: in this repo the review's characteristic find is
|
||
not a wrong line, it is **a test that passes while the thing it names is
|
||
broken** — a vacuous fold test, a bit-witness that cannot see the bit, a
|
||
one-category fixture, a helper that fabricates the `_Measure` items the code
|
||
under test needs, a check-the-checker that injected malformed JSON and so proved
|
||
nothing. A green suite is not evidence. Ask the reviewer to *inject the fault*
|
||
and watch the check fail.
|
||
|
||
### Shipping a wave
|
||
|
||
**Do not push `main` to `gh/main`.** Each agent's work reaches GitHub as its own
|
||
branch and its own pull request, and lands on `main` by being merged there, once
|
||
CI is green and an adversarial review agent has passed the PR's head SHA. An ADR
|
||
PR also needs the user's explicit approval, and nothing else stands in for it:
|
||
"ADR pull requests" below. The orchestrator does not integrate agent branches
|
||
locally: its `main` only fast-forwards to `gh/main` after a merge. The procedure is
|
||
`.agents/skills/wave/SKILL.md`, "Shepherding". `origin` and `gitea` are mirrors
|
||
and take `main` directly.
|
||
|
||
**Because the mirrors take `main` directly, `origin/main` can run ahead of
|
||
`gh/main`, and a branch destined for a GitHub PR must merge `gh/main` — never
|
||
`origin/main`.** This is a trap in this repo's own topology rather than a slip:
|
||
the two remotes are both spelled `main`, and nothing about
|
||
`git merge origin/main` looks wrong. bd's generated Session Completion block
|
||
once prescribed exactly that command (see "Repository policy over the Beads blocks" below).
|
||
It cost a review round on PR #145,
|
||
where four unmerged commits — including another PR's whole diff — were pulled
|
||
into a branch based on `gh/main` and had to be rebuilt by cherry-picking.
|
||
Today the repair **starts with a merge**. If the foreign commits belong to
|
||
another open PR that will land as a merge commit, either stack on that PR (see
|
||
the stacked-PR rule below) or wait for it to merge. Then re-pin `gh/main` and
|
||
merge the new SHA, and the foreign commits drop out of your diff. Never revert
|
||
them in your branch: merging the branch would then land an undo of that PR.
|
||
|
||
`git cherry-pick -x` is sanctioned **only** when those commits will never reach
|
||
`main` as they are: an abandoned or rebuilt PR, or commits that exist only on a
|
||
mirror or a local `main`, as in #148 below. Only then, cut a fresh branch from
|
||
the pinned `gh/main` SHA and cherry-pick only your own commits. `-x` keeps the
|
||
link to each original. Open a new PR, and close the old one without
|
||
force-pushing it.
|
||
|
||
ChatGPT Codex caught the #145 base problem; nothing in CI could, because the
|
||
result compiles and passes. The tell is a PR diff naming files the ticket has no reason to touch,
|
||
so **read your own PR's file list before asking for review** — and diff against `gh/main...<branch>`, never the local equivalent. **The second vector is the worktree base**: a branch cut from the orchestrator's *local* `main` inherits whatever that checkout has merged but not yet pushed, and nothing in the branch says so. That hit PR #148 with another PR's whole diff — including `.beads/`, `kb.pl` and `context/repo-map.md`, the three files agents are forbidden to touch, so the review read as a rule violation rather than a base problem. Note the window
|
||
is transient — the two remotes are equal immediately after the orchestrator
|
||
syncs — which makes it worse, not better: the hazard is absent exactly when
|
||
you would think to check for it.
|
||
|
||
This is easy to drift out of, because merging locally and pushing feels like
|
||
finishing. It happened in this repo: 27 commits across six agent branches —
|
||
including a new crate, a new protocol type, and a type moving between crates —
|
||
went straight to `main` unreviewed, while `AGENTS.md` already said not to. If
|
||
you notice a batch has bypassed the loop, the cheap repair is a **review-only
|
||
PR**: base a branch at the last PR merge, open a PR with `main` as the head,
|
||
have an adversarial review agent review the diff, and land the findings as an
|
||
ordinary PR afterwards. It
|
||
cannot be merged — the commits are already in — so close it once its findings
|
||
are filed or fixed. Rewinding the default branch to fake a mergeable PR is not
|
||
worth it on a repo with other collaborators.
|
||
|
||
**Read the PR's reviews before merging it.** This is separate from CI and from
|
||
the adversarial agent, and it has been missed: two ChatGPT Codex reviews landed
|
||
on merged PRs and were not read until afterwards, one of them a live P2
|
||
(improvise-x1ns). Both were correct.
|
||
|
||
```bash
|
||
./scripts/pr-reviews <n> # bodies, inline comments and issue comments, in time order; do not pipe it
|
||
```
|
||
|
||
`gh pr view <n> --json reviews` is not a substitute: it shows review bodies
|
||
only, and most of Copilot's findings are inline comments.
|
||
|
||
Two things make a Codex review easy to miss. It arrives as a **review**, not a
|
||
comment, so a glance at the comment thread does not show it. And it posts under
|
||
the repository owner's account — the same account our own agents post under —
|
||
so **authorship does not distinguish it**. The tell is the trailing line
|
||
`_Automated review by ChatGPT Codex (…)_`; ours open with
|
||
`_Posted by Claude (…)_`. Check the `commit.oid` too: a review may target an
|
||
earlier head, and the finding may already be fixed — verify rather than assume
|
||
in either direction.
|
||
|
||
Reviews arrive **as the owner's quota allows**, so absence is not evidence.
|
||
Wait where a review is plausible. How a review is answered once found, by the
|
||
author and by a reviewer, is `.agents/skills/wave/agent-rules.md`, "GitHub".
|
||
|
||
**Copilot is reviewing this repository again, so read its findings and answer
|
||
them exactly as you answer Codex's.** On 2026-09-24 it posted inline findings on
|
||
#221, #251, #274, #295 and #298, after a lapse that began between #27 and #61
|
||
(measured 2026-09-20: #12, #21 and #27 carry 2, 8 and 4 Copilot reviews, and
|
||
#61, #66, #68 and #70 carry none, while both request paths reported success and
|
||
registered nothing). Two things carry over from the lapse:
|
||
|
||
- **Its findings arrive mostly as inline review comments**, not as a review
|
||
body. `scripts/pr-reviews <n>` reads bodies, inline comments and issue
|
||
comments together; `gh pr view --json reviews` shows only the bodies, so it
|
||
misses them. Answer each on its own thread, as agent-rules, "GitHub" says.
|
||
- **Do not wait on it.** It reviews as its quota allows, and an absent review
|
||
and an unrequested one still look identical from outside, which is
|
||
`doctrine(review_is_requested_not_inherited)` seen from the requester's side.
|
||
The review that gates a merge is still an adversarial agent the orchestrator
|
||
dispatches, given the head SHA. Copilot's findings are read *in addition*,
|
||
before the merge, like Codex's.
|
||
|
||
The request mechanics below are what worked before the lapse. Whether they work
|
||
now is not re-measured.
|
||
|
||
Copilot review must be **re-requested after every fix push**; it does not
|
||
re-review on its own. The mutation takes a **`botIds`** field — `userIds` fails
|
||
with *"Could not resolve to User node"*:
|
||
|
||
```bash
|
||
gh api graphql -f query='mutation($pr:ID!,$ids:[ID!]!){requestReviews(input:{pullRequestId:$pr,botIds:$ids,union:true}){clientMutationId}}' \
|
||
-f pr="$(gh pr view <n> --json id -q .id)" -f ids=BOT_kgDOCnlnWA
|
||
```
|
||
|
||
REST also works, with the login spelled `copilot-pull-request-reviewer[bot]` —
|
||
without the `[bot]` suffix it 422s as "not a collaborator".
|
||
|
||
In a worktree-isolated agent the shell guard refuses an inline
|
||
`gh api graphql -f query='mutation(...)'` as "too complex to verify". Put the
|
||
query in a file and pass `-F query=@file`; PR comment bodies likewise go
|
||
through `-F body=@file` rather than command substitution.
|
||
|
||
**Anything you post to GitHub goes out under the repository owner's account, so
|
||
say who is speaking.** Open every PR comment, review and issue body with a line
|
||
naming the model and that it is an agent posting in his session. A PR body's
|
||
`🤖 Generated with Claude Code` footer covers that case; comments have nothing
|
||
unless you add it, and an unattributed one reads as his own words to everyone
|
||
who sees it.
|
||
|
||
Two environment notes. **`gh` is not on `PATH` here** — the name is
|
||
shell-aliased to `git-hub`, a different tool — so use
|
||
`nix shell nixpkgs#gh --command gh …`. And **a Copilot review takes about six
|
||
minutes**; tool calls return fast, which makes elapsed time easy to misjudge, so
|
||
check `date -u` against the GitHub timeline before concluding a review is stuck.
|
||
(One agent closed and reopened a PR to "unstick" a review that was proceeding
|
||
normally.)
|
||
|
||
Across four PRs Copilot has found genuine bugs and no false positives, so its
|
||
findings are worth the loop. **But judge each one against the repo's doctrine
|
||
rather than applying it reflexively**. How a declined finding is answered is
|
||
agent-rules, "GitHub". The distinction that has mattered: a finding can be *correct*
|
||
and still be wrong to fix in that PR. On #12, a non-ASCII `truncate` panic was
|
||
applied (byte-identical on ASCII, so it cost no evidence) while a hint-line
|
||
padding bug in the same file was declined — fixing it would have rewritten the
|
||
exact-frame characterization tests that were the PR's only evidence the
|
||
migration changed nothing. Two right findings, different costs. File the
|
||
declined one.
|
||
|
||
**Under merge-as-soon-as-green, a review round can land after its own merge.**
|
||
GitHub deletes the branch when a PR merges, so a fix push to the old branch name
|
||
**silently succeeds and accomplishes nothing** — it re-creates a branch `main`
|
||
will never see, CI runs on it, and the commit is nowhere. The tell is the push
|
||
printing `* [new branch]` where you expected an `old..new` range. It happened on
|
||
#51: the third review round completed four minutes after the merge, and the
|
||
correction it prompted went to a re-created `wave/b-projection-gaps`. The repair
|
||
is a merge, not a cherry-pick. The PR merged with a merge commit, so the stray
|
||
branch's base is already an ancestor of `main`. Branch from the pinned
|
||
`gh/main` SHA, `git merge` the stray branch's SHA, open a follow-up PR, and
|
||
delete the stray re-creation. The merge brings exactly the fix commits, under
|
||
their own SHAs, with the link to the PR they answered. A cherry-pick would copy
|
||
them and drop that link. This recipe works only because PRs here never
|
||
squash-merge. Check `gh pr view <n> --json state` *before* pushing a fix to
|
||
a PR you have not looked at in a few minutes, rather than after.
|
||
|
||
**A stacked PR whose base PR has already merged merges into that stale base
|
||
branch, not into `main`.** GitHub merges a PR into whatever `baseRefName` says,
|
||
and it did not retarget these. On 2026-09-24 fifteen approved stacked PRs
|
||
(#255–#286) were merged into a base whose own PR had **already** merged, and
|
||
the merges read as done. Recovery took seven landing PRs (#289–#295), each
|
||
opened from a stack's tip against `main`, and their titles name exactly those
|
||
fifteen. Four more in that range (#259, #263, #272, #273) also merged into a
|
||
non-`main` base, but one whose PR was still open; they reached `main` when their
|
||
base did, which is stacking working as intended. The repository reports `delete_branch_on_merge: true` as measured on
|
||
2026-09-24, and the stale bases (`collapse-fu/h6z1`, `fix/enz6-advance-onto-fold`
|
||
and others) were still on GitHub afterwards, so that setting is not the guard.
|
||
The guard is a check before every merge:
|
||
|
||
```bash
|
||
gh pr view <n> --json baseRefName -q .baseRefName # must print main
|
||
gh pr edit <n> --base main # if not, once its base PR has landed
|
||
```
|
||
|
||
Then read the PR's diff again: a retargeted PR's diff now includes its base's
|
||
commits if the base did not land.
|
||
|
||
Merge only once a review of the PR's current head has passed — an adversarial
|
||
agent's — and every Codex and Copilot finding on the PR has an answer. What a
|
||
review must state is agent-rules, "GitHub". Merge with a **merge commit**. Never
|
||
use GitHub's squash or rebase merge: history is reconciled only by merge (the
|
||
rule at the top of this file). A PR's commits are individually meaningful, and
|
||
the fix-after-merge recipe above needs the PR's head to be an ancestor of
|
||
`main`.
|
||
|
||
### Merging many PRs at once
|
||
|
||
Three things went wrong when a day's approved PRs landed together, and each is
|
||
invisible to CI on either side alone.
|
||
|
||
**Land a batch as one integration PR, not one PR at a time.** Every PR appends
|
||
its bug evidence at the end of a `context/test-evidence/*.md` file. So each
|
||
merge makes every other open PR conflict at EOF, and merging one at a time
|
||
means re-resolving the same files once per PR — and a PR that conflicts gets no
|
||
CI run at all (agent-rules, "Gates"). On 2026-09-24 the approved PRs landed as
|
||
two integration PRs instead: #298 (nineteen PRs) and #299 (the browser-editor
|
||
stack). This is what "one PR per wave" above already intends.
|
||
|
||
**After resolving a conflict, diff the result against each parent and against
|
||
the merge base.** `git checkout --ours <file>` or `--theirs` takes the whole
|
||
file from one side, so it silently drops every *non*-conflicting change the
|
||
other side made to that file. #245 resolved a `ci.yml` conflict with #228 by
|
||
`git checkout --theirs`, took #228's whole file, and lost its own
|
||
`check-harness-lock --allow-network` step, while the merge message said the step
|
||
was unchanged (improvise-dndu). Nothing refused it: `check-ci-graph.py` counts
|
||
suites, not that step. Resolve by hand, then check the merge commit:
|
||
|
||
```bash
|
||
git diff <merge-base> HEAD -- <file> # both sides' changes together, and nothing else
|
||
git diff HEAD^1 HEAD -- <file> # what the second side brought
|
||
git diff HEAD^2 HEAD -- <file> # what the first side brought
|
||
```
|
||
|
||
**Two changes to one test file can merge cleanly and still fail together.** #287
|
||
(the NixOS runners) found that `/usr/bin:/bin` there holds only `env` and `sh`,
|
||
and gave section 5 of `checkers-test.sh` a hermetic `utilbin` PATH. #237 added
|
||
section 5b on a branch cut before that, still using `PATH=…:/usr/bin:/bin`. Each
|
||
passed CI alone, the merge was textually clean, and 5b failed on every PR after
|
||
it (#296). So a fix to an idiom — a hermetic PATH, a scrubbed environment, a
|
||
helper every case must call — has to be applied to **every case in the file**,
|
||
including cases that exist only on another open branch. When you merge, grep
|
||
the merged file for the old idiom. Then run the gates on the merged tree, not
|
||
on each side.
|
||
|
||
### CI takes its tools from the dev shell
|
||
|
||
These are the user's rulings of 2026-09-24:
|
||
|
||
- **CI runs on self-hosted NixOS microVM runners** (`runs-on: [self-hosted,
|
||
nixos, microvm]`). Every `run:` step runs in `nix develop .#ci`, through the
|
||
workflow's `defaults.run.shell`. A `uses:` step (an action such as
|
||
`actions/checkout`) is not a shell step and does not go through it. A missing tool goes into the `.#ci` shell in
|
||
`flake.nix`, not into a per-step `nix shell` wrapper.
|
||
- **No hard-coded `/bin` or `/usr/bin` paths.** On these runners those
|
||
directories hold only `env` and `sh`. Take tools from `PATH`.
|
||
- **No version floors and no probes.** Scripts and tests must not care which
|
||
bash or standard utilities are present. `checkers.sh` no longer pins a bash
|
||
(it did, at 5.1, until #297). A script that needs a newer feature fails where
|
||
it uses it. The argument is `context/code/tooling.md` §5.
|
||
- **Playwright's browsers come from nix.** The dev shell sets
|
||
`PLAYWRIGHT_BROWSERS_PATH` to nixpkgs' browsers, so there is no install step
|
||
and no version check. `npm ci` stays, because `@playwright/test` is not
|
||
nix-provided.
|
||
|
||
A test that fakes an environment must build it on purpose, for example a
|
||
hermetic `utilbin` of linked tools. It must not assume the runner's `/bin`
|
||
holds anything.
|
||
|
||
### ADR pull requests
|
||
|
||
**Only the user's explicit approval accepts an ADR (`context/adr/`), and only
|
||
it lets an ADR PR merge.** "An ADR isn't approved until I say it is" (the
|
||
user). Green CI, every review answered and a passing adversarial review
|
||
("Shipping a wave") are still required. They are gates, never approval: an ADR
|
||
PR that passes all of them is still unapproved and does not merge. The steps,
|
||
modelled on PR #122 (ADR 004):
|
||
|
||
1. The PR proposes the ADR with its **open questions numbered**.
|
||
2. The user answers them on the PR.
|
||
3. The answers are folded into the ADR, in a section headed **"What the user
|
||
decided"**.
|
||
4. The PR merges only after the user comments **"Approved"**, and only while
|
||
the gates above still hold. Nobody else's comment or review stands in for
|
||
it. "Read the PR's reviews before merging it" still
|
||
applies after the approval. On #122 the user's own review landed as the PR
|
||
merged.
|
||
|
||
**An open ADR PR is watched, not visited.** The user answers the numbered
|
||
questions inline, and reviews arrive as they arrive. An answer left unread for
|
||
hours is a failure (the user, 2026-09-24). So while an ADR PR is open the
|
||
orchestrator keeps a background poll for new non-agent comments and reviews
|
||
since its last pass, and resumes the steward agent on each new item. The
|
||
steward folds each answer in and replies on its thread.
|
||
|
||
### Never hand-edit `.beads/issues.jsonl`
|
||
|
||
bd manipulates that file and auto-exports to it. Editing it — or even
|
||
`git checkout`-ing it, which fires the post-checkout hook — breaks auto-export
|
||
and desynchronises `export-state.json`. Use `bd` commands, let auto-export run,
|
||
and commit whatever it leaves.
|
||
|
||
### Cite a ticket ID you have read back
|
||
|
||
`bd create` assigns the ID; it is not derivable from the title. An orchestrator
|
||
that files with output suppressed and then writes the ticket into a gotcha or an
|
||
ADR **will invent one** — six invented IDs survived into `context/` in a single
|
||
session before an agent tried one and found nothing. Read the ID back after
|
||
filing, and before pushing docs run the audit:
|
||
|
||
```bash
|
||
./scripts/bd-audit-ids
|
||
```
|
||
|
||
It exits 0, printing `all N cited ids resolve`, only when every cited id is in
|
||
what `bd list --all` returns. A dead id is exit 1, and each one is named. A bd
|
||
that is missing, fails, or lists no issues is exit 3. The comparison is
|
||
positive because the recipe the script replaced was not: it searched `bd show`
|
||
for "not found" lines and printed "all resolve" when there were none, so a bd
|
||
that never ran read as a clean audit (improvise-52eq). Its suite,
|
||
`scripts/bd-audit-ids-test.sh`, runs in `checkers.sh` and checks both halves:
|
||
a dead id, and a bd that does not answer.
|
||
|
||
In the script, `git ls-files` picks the files, and grep only reads them. The file list is the
|
||
tracked files plus the untracked ones, minus anything git ignores. That leaves
|
||
out two things a `grep -r` over the same roots reads:
|
||
|
||
- **Build output.** After `./harness/build.sh`, `harness/serve/` holds
|
||
rendered copies of `context/`, and `grep -r` reported three dead ids from them
|
||
that no tracked file contains (improvise-8haj).
|
||
- **Other agents' worktrees.** In the orchestrator's checkout
|
||
`.claude/worktrees/` holds every agent's worktree, dozens of copies of the
|
||
repository with their unpushed work.
|
||
|
||
`-d skip` is there for the skills' symlinks, and it changes no answer. The
|
||
`.claude/skills/*` entries are symlinks to directories under `.agents/skills/`,
|
||
and `git ls-files` lists each one, so each reaches grep as a command-line
|
||
operand. The grep has no `-r`, so it never reads a directory operand. It prints
|
||
`grep: <path>: Is a directory` on stderr instead, and `-d skip` only silences
|
||
those lines. The output goes through `sort -u`, so a duplicate could not change
|
||
the answer anyway. Measured with BSD grep 2.6.0 and GNU grep 3.12: 3092 matches
|
||
with or without the flag, 462 distinct ids. Do not add `-r`: GNU grep then
|
||
follows those symlinks and reads the skills a second time (3107 matches).
|
||
|
||
Plain `git grep` is not a substitute: on macOS its `-E` silently ignores `\b`
|
||
and finds a fraction of the ids.
|
||
|
||
Three things the earlier version of this recipe got wrong, each of which let a
|
||
real defect through:
|
||
|
||
- **It scanned two paths** (`context/repo-map.md context/adr/*.md`), so it
|
||
found one dead id in the repo map and missed both copies of it in
|
||
`harness/tests/shell.spec.js`. Ticket IDs are cited in code comments,
|
||
in `README.md` and in `web/`, not only in `context/`.
|
||
- **`[a-z0-9]{3,4}` cannot see a commit SHA wearing the prefix.** A seven-hex
|
||
commit sha cited as `improvise-<sha>` survived in
|
||
`crates/improvise-core/src/workbook.rs` precisely because it was too long
|
||
to match. An ID-shaped non-ID is worse than a dead ID, because the
|
||
diagnosis is identical and the repair is not.
|
||
- **The exclusion list named only `core` and `wasm`**, so every other crate
|
||
name would have been reported missing. The script's exclusions also cover the
|
||
non-ticket tokens this repo really contains: a package name, a temp file
|
||
name, the prose word "improvise-side", and the example crate name in the
|
||
`kb` skill's `crate(...)` fact. **They are anchored `^…$` for the
|
||
same reason the recipe is read as code**: unanchored, the `gp` alternative is
|
||
a *prefix* match, so a future real ticket whose id happens to begin `gp` is
|
||
silently excluded and the audit reports nothing while skipping it. An
|
||
exclusion list is a filter, and a filter that over-matches fails in the
|
||
direction that looks like success. (Anchoring it is also what caught the
|
||
example id this very paragraph originally spelled out — see the note below.)
|
||
- **A release artifact's name is ID-shaped, and `improvise-installer.sh`
|
||
escaped only by being ten characters long.** `improvise-web.tar.gz`
|
||
(improvise-oujb) is three, so it matches `{3,8}` and resolves to nothing;
|
||
`web` is therefore excluded in the script. The general point is that the length
|
||
bound, not any rule, is what has been keeping `dist`'s artifact names out of
|
||
this audit — so **a new artifact whose name is a short lowercase word must be
|
||
added to the exclusion list in the same commit**, or it arrives as a dead id
|
||
that no amount of `bd` archaeology explains.
|
||
|
||
The recipe's own prose spells no id as an *example*, because a document that
|
||
lists example ids makes itself a source of audit noise. The ids it does cite,
|
||
improvise-8haj and improvise-oujb, are the tickets behind two of the rules
|
||
above. They resolve, and the audit checks them like any other.
|
||
|
||
One environment note: run the audit by its name. Which `bd` command forms a
|
||
worktree-isolated agent's shell guard allows has varied between sessions. In
|
||
one session it refused the old recipe's `bd show $(cat …)`, and also
|
||
`xargs bd show < file` and `sh file`. In another, both `xargs` forms ran.
|
||
`./scripts/bd-audit-ids` passes `bd` no argument computed at runtime, and it
|
||
ran in the session that refused the other three.
|
||
|
||
A dead ID is worse than no ID: it reads as a promise that the follow-up exists.
|
||
|
||
### Before filing a ticket, search
|
||
|
||
Agent-filed issues duplicate work that is already open, sometimes already
|
||
in progress. Two duplicates were filed in this repo within minutes of the
|
||
hand-backs that prompted them. `bd search` the core concept *and* scan
|
||
in-progress issues first.
|
||
|
||
## Repository policy over the Beads blocks
|
||
|
||
Everything between a `BEGIN BEADS …` marker and its `END` is generated by bd,
|
||
and bd replaces it whole. `bd init` and `bd setup factory|opencode|mux` rewrite
|
||
the integration block, and the last three call it "stale" and invite the
|
||
rewrite whenever bd's template has moved on. `bd setup codex` rewrites the
|
||
Codex block. So a hand edit inside the markers is silently reverted by the next
|
||
regeneration. Repository policy lives here, outside them:
|
||
|
||
- **Work is not done until it is in a pull request and that pull request has
|
||
merged.** A pushed branch is not done, and neither is a green one. In a wave,
|
||
the merge is the orchestrator's, after an adversarial review passes the head
|
||
SHA ("Shipping a wave"). An ADR PR merges only on the user's explicit
|
||
approval as well ("ADR pull requests"). So a wave agent's part ends at an
|
||
open PR plus its hand-back, and the ticket is done, and its bead closed, only
|
||
when the merge lands.
|
||
- **This repository does not opt in to the Beads "team-maintainer" profile.**
|
||
So the pull (`git pull`, or `git pull --rebase` in bd's "full" profile) and
|
||
the bare `git push` that the block lists for that profile are not
|
||
instructions here. Work reaches `main` as "Shipping a wave" describes,
|
||
and a wave agent pushes with the explicit refspec in
|
||
`.agents/skills/wave/agent-rules.md`, "Base, branch, push".
|
||
- **The no-markdown-TODO and no-`MEMORY.md` rules have one exception:**
|
||
`context/design-principles.md` and `context/repo-map.md` are kept by hand
|
||
(the repo map by the orchestrator only; see "Who owns what").
|
||
|
||
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:46cd31e7 -->
|
||
## Beads Issue Tracker
|
||
|
||
This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
|
||
|
||
### Quick Reference
|
||
|
||
```bash
|
||
bd ready # Find available work
|
||
bd show <id> # View issue details
|
||
bd update <id> --claim # Claim work
|
||
bd close <id> # Complete work
|
||
```
|
||
|
||
### Rules
|
||
|
||
- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
|
||
- Run `bd prime` for detailed command reference and session close protocol
|
||
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files
|
||
|
||
**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/core-concepts/sync-concepts.md for details and anti-patterns.
|
||
|
||
## Agent Context Profiles
|
||
|
||
The managed Beads block is task-tracking guidance, not permission to override repository, user, or orchestrator instructions.
|
||
|
||
- **Conservative (default)**: Use `bd` for task tracking. Do not run git commits, git pushes, or Dolt remote sync unless explicitly asked. At handoff, report changed files, validation, and suggested next commands.
|
||
- **Minimal**: Keep tool instruction files as pointers to `bd prime`; use the same conservative git policy unless active instructions say otherwise.
|
||
- **Team-maintainer**: Only when the repository explicitly opts in, agents may close beads, run quality gates, commit, and push as part of session close. A current "do not commit" or "do not push" instruction still wins.
|
||
|
||
## Session Completion
|
||
|
||
This protocol applies when ending a Beads implementation workflow. It is subordinate to explicit user, repository, and orchestrator instructions.
|
||
|
||
1. **File issues for remaining work** - Create beads for anything that needs follow-up
|
||
2. **Run quality gates** (if code changed) - Tests, linters, builds
|
||
3. **Update issue status** - Close finished work, update in-progress items
|
||
4. **Handle git/sync by active profile**:
|
||
```bash
|
||
# Conservative/minimal/default: report status and proposed commands; wait for approval.
|
||
git status
|
||
|
||
# Team-maintainer opt-in only, unless current instructions forbid it:
|
||
git pull
|
||
bd dolt push
|
||
git push
|
||
git status
|
||
```
|
||
5. **Hand off** - Summarize changes, validation, issue status, and any blocked sync/commit/push step
|
||
|
||
**Critical rules:**
|
||
- Explicit user or orchestrator instructions override this Beads block.
|
||
- Do not commit or push without clear authority from the active profile or the current user request.
|
||
- If a required sync or push is blocked, stop and report the exact command and error.
|
||
<!-- END BEADS INTEGRATION -->
|
||
|
||
<!-- BEGIN BEADS CODEX SETUP: generated by bd setup codex -->
|
||
## Beads Issue Tracker
|
||
|
||
Use Beads (`bd`) for durable task tracking in repositories that include it. Use the `beads` skill at `.agents/skills/beads/SKILL.md` (project install) or `~/.agents/skills/beads/SKILL.md` (global install) for Beads workflow guidance, then use the `bd` CLI for issue operations.
|
||
|
||
### Quick Reference
|
||
|
||
```bash
|
||
bd ready # Find available work
|
||
bd show <id> # View issue details
|
||
bd update <id> --claim # Claim work
|
||
bd close <id> # Complete work
|
||
bd prime # Refresh Beads context
|
||
```
|
||
|
||
### Rules
|
||
|
||
- Use `bd` for all task tracking; do not create markdown TODO lists.
|
||
- Run `bd prime` when Beads context is missing or stale. Codex 0.129.0+ can load Beads context automatically through native hooks; use `/hooks` to inspect or toggle them.
|
||
- Keep persistent project memory in Beads via `bd remember`; do not create ad hoc memory files.
|
||
|
||
**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/core-concepts/sync-concepts.md for details and anti-patterns.
|
||
<!-- END BEADS CODEX SETUP -->
|