b0c4105be3
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, so an unattributed post reads as the human's words. Format: _Posted by <Agent Stack> (<model>)_ Examples: - _Posted by Claude (Opus 4.5)_ - _Posted by Cursor Agent (claude-sonnet-5)_ AGENTS.md: - Added 'GitHub Attribution (Hard Rule)' section in multi-stack docs - Concrete format and examples - Clarified that a thread is not 'answered' until the reply has attribution .agents/skills/pr-watch/SKILL.md: - Added 'GitHub Attribution (Hard Rule)' subsection - Updated 'Answer substantive feedback with attribution' in obligations - Cross-references AGENTS.md for full rule context/cloud-agents.md: - Added 'GitHub Attribution (Hard Rule)' section - Same format and examples as AGENTS.md This ensures any agent reading the shared docs (Cursor, Claude, etc.) knows to attribute every GitHub write to the specific AI agent. Co-authored-by: Ed Langley <fiddlerwoaroof@users.noreply.github.com>
703 lines
38 KiB
Markdown
703 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. |
|
||
|
||
Wave agents **read** all three 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 -->
|