mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-10-28 16:42:51 +08:00
147 lines
8.6 KiB
Markdown
147 lines
8.6 KiB
Markdown
# Working as a crew
|
||
|
||
The protocol for a Claude Code session on this repository: a lead thread that delegates, role
|
||
subagents that do the work, a card that holds the state, a loop that ends. For the lead and for
|
||
whoever edits a role in `.claude/agents/`. The short form is the "Crew" section of `CLAUDE.md`.
|
||
|
||
Why it exists: the heaviest session on record put 484 Bash results and 20 screenshots through one
|
||
context, against 5 delegations, and a context that carries every gate log recalls less of the rest.
|
||
A subagent spends tens of thousands of tokens and returns one to two thousand.
|
||
|
||
## Roles
|
||
|
||
| Role | Model | Writes | Sees | Returns |
|
||
|---|---|---|---|---|
|
||
| lead (main thread) | Opus | one-sentence diffs only | the card, the return blocks | decisions |
|
||
| `developer` | Sonnet | inside `card.files` | the card, its rules file, a handoff if any | files + gates block |
|
||
| `tester` | Opus | nothing | the diff and the card | verdict block |
|
||
| `security` | Sonnet | nothing | the diff | findings block |
|
||
| `researcher` | Sonnet | nothing | the question | answer + ranked sources |
|
||
| `caveman:cavecrew-investigator` | Haiku | nothing | a "where is X" question | `file:line` table |
|
||
|
||
One `developer` at a time, sequentially: two writers on one card produce two designs for one
|
||
problem, which is the failure every published account of parallel coding agents reports.
|
||
|
||
The tester runs on a different model from the developer: a judge of the same model recognises its
|
||
own text and grades it softer. It never sees the developer's reasoning, only the diff. Set the
|
||
lead's model with `/model opus`; the roles name theirs in frontmatter.
|
||
|
||
## The card
|
||
|
||
The lead writes `../tmp/task-<id>/card.json` before the first delegation. JSON, because models
|
||
rewrite JSON less readily than Markdown, and a file outlives a compaction of the lead's context.
|
||
|
||
```json
|
||
{
|
||
"id": "0142-appearance-note",
|
||
"goal": "one sentence",
|
||
"grade": "task",
|
||
"hardware": false,
|
||
"files": ["luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js"],
|
||
"rules": [".claude/rules/js.md"],
|
||
"acceptance": [
|
||
{ "cmd": "npm run lint:js", "expect": "exit 0" },
|
||
{ "cmd": "node tools/page-modules.mjs", "expect": "exit 0" }
|
||
],
|
||
"live": [{ "gate": "spa-parity", "flags": "--pages /admin/system/system" }],
|
||
"round": 0,
|
||
"max_rounds": 2,
|
||
"history": []
|
||
}
|
||
```
|
||
|
||
| Field | Values |
|
||
|---|---|
|
||
| `grade` | `task`, or `release` — adds `owlab test` on both formats. Any diff touching the Makefile, `root/**`, `build-css.sh`, `tools/stage.sh` or a `strip-*` script is `release` |
|
||
| `hardware` | `false` until the maintainer says otherwise for this card |
|
||
| `history` | one entry per tester round: `{ "round", "verdict", "blocking": [ids], "diff_hash" }` |
|
||
|
||
## Return blocks
|
||
|
||
Every role returns at most 25 lines: paths, not contents; no diff, no code, no story of the
|
||
attempt. The shape is in each role's file. The lead quotes the block and never opens the log it
|
||
summarises; the role that produced a log is the role that read it.
|
||
|
||
A role works in caveman full throughout, the mode the lead runs in — a subagent inherits no session
|
||
hook, so each role carries the rule in its own "Voice" section. It covers every line the role emits,
|
||
not the block alone: a question back to the lead, a `BLOCKED` line, a note on a gate that did not
|
||
run. Commands, flags, paths, numbers, quoted errors and every negation stay exact; the compression
|
||
is the prose around them, and it never grows the output. What a role writes to disk — code,
|
||
comments, a changelog line, a handoff — is normal English prose, as everywhere else here.
|
||
|
||
## The loop
|
||
|
||
1. **Size the task.** A diff the lead can describe in one sentence, it makes itself. More than one
|
||
file, any gate beyond T0, or more than ~50 lines of tool output: the crew.
|
||
2. **`developer`, round 0.** Implements, runs T0, stages what it touched with `git add`, returns.
|
||
3. **`tester`.** Reads `git diff HEAD`, runs what the card asks, returns a verdict. Only
|
||
`blocking: true` findings start a round; the rest go into `history` for the maintainer.
|
||
4. **Stop** when all three hold: T0 green, no blocking finding, the last round touched no file
|
||
outside the previous round's set.
|
||
5. **Otherwise, one fix round.** The developer gets the blocking findings and its previous
|
||
`not_done`. The tester gets `git diff` (worktree against the index, which holds the previous
|
||
round) and the previous findings — never the full diff again. Two fix rounds at most.
|
||
6. **Stall.** The same blocking id in two consecutive verdicts, or the same `diff_hash` twice, ends
|
||
the loop: the card goes to the human. A third round on a stalled finding is the ping-pong the
|
||
loop exists to prevent.
|
||
7. **`security`**, once, on the final diff. Skipped for CSS-only and one-line diffs. Findings go to
|
||
the human and are never auto-fixed.
|
||
|
||
While a card is open the index belongs to the loop: commit with `git commit`, not `git commit -a`,
|
||
and read `git status` before staging anything else.
|
||
|
||
The numbers behind the caps: gains of iterative repair sit in the first two rounds; a review
|
||
without a new external signal makes the result worse; a stall counter of 2–3 is what production
|
||
orchestrators use. Sources, ranked: the research artifact of 2026-09-03.
|
||
|
||
## Permissions
|
||
|
||
| Change in `.claude/settings.json` | Why |
|
||
|---|---|
|
||
| `owlab up/down/sync/test/exec/status`: `ask` → `allow` | disposable containers; a loop that prompts at every sync is not a loop |
|
||
| `dev-sync.sh`, `ssh`: → `ask` | the hardware router is the one thing a wrong run breaks for a person |
|
||
| `gh pr comment/review`, `gh issue comment`, MCP review calls: deny removed | publishing is a rule in `CLAUDE.md`, given per action, not a matcher |
|
||
| `Read/Write/Edit(../tmp/**)`: `allow` | the scratch directory, cards and handoffs |
|
||
| `additionalDirectories: ["../tmp"]` | `../tmp` is outside the project root, and the three rules above do not apply until the directory is registered |
|
||
| `git stash/clean/restore/reset --hard/checkout -- `, `git add -A|--all|.`: `ask` | the loop owns the index between rounds; these are the verbs that discard a round's work or widen it past the card, and `/upstream-pr` needs them, so they prompt rather than deny |
|
||
|
||
`tools/bg.sh` carries its own fence: `ssh`, `scp`, `sftp`, `rsync`, `dev-sync.sh`, `git push`,
|
||
`git commit`, `git tag` and `gh pr|release|issue` are refused there. The wrapper is allow-listed and
|
||
the matcher reads the first word only, so without that fence `tools/bg.sh sh -c '… ssh …'` runs past
|
||
every `ask` rule above. `tester` and `security` also carry `disallowedTools: Edit, Write,
|
||
NotebookEdit`, which makes "read-only" a fence and not only a sentence in the prompt.
|
||
|
||
`.claude/rules/*.md` do load inside a subagent when it reads a matching file (probed 2026-09-03,
|
||
Haiku subagent, `styles/**`). The developer still reads its rules file by name from the card: a
|
||
rule has to be in front of it before the first edit, not after the first Read.
|
||
|
||
## Measuring the crew
|
||
|
||
```sh
|
||
node .claude/tooling/measure-run.mjs # newest transcript of this project
|
||
node .claude/tooling/measure-run.mjs latest 3
|
||
```
|
||
|
||
The `delegated:` line is the other half of the trade: a subagent writes its own transcript under
|
||
`<session>/subagents/`, so the lead's number alone would only ever say that the lead got lighter.
|
||
The crew trial of 2026-09-03 reads 152k characters in the lead against 1.54M across 17 subagent
|
||
transcripts — the work moved, it did not evaporate.
|
||
|
||
Per card the crew must show: lead tool-output under 15k characters, zero images in the lead, every
|
||
gate log opened by a role, rounds ≤ 2, no stall. Baseline before the crew, as the command above
|
||
prints it: 136,544 characters of tool result plus 11 images (1.96 MB of payload) in this project's
|
||
largest session, and 417,073 characters, 20 images and 484 Bash calls against 5 delegations in the
|
||
largest of `luci-app-footstrap-files`.
|
||
|
||
Two testers were run side by side on trial card 1 (2026-09-03), `tester` on Opus and `tester-b` on
|
||
Sonnet, same prompt and same diff. Opus returned three findings to Sonnet's one: it also caught
|
||
that the new changelog entry pointed at files still untracked, and it ran `computed-diff --control`
|
||
without being asked, which is what turned 2468 differences from a number into a causal one. Sonnet
|
||
cost 36k tokens against 47k and finished in half the time, and found nothing the other missed.
|
||
Opus stays; `tester-b` is deleted.
|
||
|
||
The loop itself was exercised on a second card the same day: round 0 carried a planted `wrap-regex`
|
||
defect, the tester returned NEEDS_WORK with two blocking findings, and the developer closed them in
|
||
one round by deleting the unreachable helper rather than wrapping its regex — wrapping would have
|
||
been polish on dead code. One fix round, no stall, stop condition met.
|