Files
small-packages/luci-theme-footstrap/docs/crew.md
T
2026-09-20 01:22:33 +08:00

8.6 KiB
Raw Blame History

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.

{
  "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

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

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.