docs: FLEET.md reconciled against crew@01fb49c — registry is the scope

The two scope passages said the opposite of merged behaviour: a reviewer's
registry was 'the org itself' and 'no repo filter may gate' a request.
duty-review.sh implements repos.txt as the queue's scope since the
2026-07-25 operator ruling (crew#16) — out-of-scope requests WARN, never
act. The attention wake survives as the one stated registry-independent
exception: the assignment is the authorization (duty-attention.sh).

Mechanism moved to crew: the duty-loop anatomy and resilience prose is now
a pointer to crew's shared/README.md, folding the two drifted facts (cron
runs tick.sh; hygiene self-schedules inside the duty tick). Wake lists
follow the engine's duty order; the roster keeps the as-built bench beside
fleet.roster's target with the delta stated; stamp updated to crew@01fb49c.

Closes #187

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
claude-bot-andresmgsl 2026-07-27 18:16:17 +00:00
parent d9c5b92dd1
commit 7a49de7efb
2 changed files with 163 additions and 99 deletions

249
FLEET.md
View file

@ -6,12 +6,13 @@
> the vendored doctrine set (`.ceremony/`) and is never mirrored to consumer > the vendored doctrine set (`.ceremony/`) and is never mirrored to consumer
> repos. The doctrine files (AGENTS.md, TRIAGE.md, BUILDER.md, REVIEWER.md, > repos. The doctrine files (AGENTS.md, TRIAGE.md, BUILDER.md, REVIEWER.md,
> LABELS.md, CONTRIBUTING.md) say what roles *must* do; this file says how the > LABELS.md, CONTRIBUTING.md) say what roles *must* do; this file says how the
> current bench *physically* does it. Last reconciled against the deployed > current bench *physically* does it — and since the duty engine converged
> duty scripts at > into [heavy-duty/crew](https://github.com/heavy-duty/crew) (private to the
> [`heavy-duty/crew@b2fd864`](https://github.com/heavy-duty/crew/tree/b2fd8642e7f7aa8dc9de6b44edadbe1dc557b140) > org; the fleet can read it), the *mechanism* lives there and this file only
> (private to the org; the fleet can read it), 2026-07-24 — a descriptive > points at it. Last reconciled against the merged engine at
> file with no reconciliation stamp gives the next reader nothing to diff, > [`heavy-duty/crew@01fb49c`](https://github.com/heavy-duty/crew/tree/01fb49cd717a0f4c83df286af86411c69e3c2363),
> which is exactly how the #149 drift went unnoticed. > 2026-07-27 — a descriptive file with no reconciliation stamp gives the next
> reader nothing to diff, which is exactly how the #149 drift went unnoticed.
## The roster ## The roster
@ -27,28 +28,56 @@ network path — GitHub is the only queue.
| `grok-bot-andresmgsl` | grok-box | Grok CLI | reviewer | | `grok-bot-andresmgsl` | grok-box | Grok CLI | reviewer |
| `kimi-bot-andresmgsl` | kimi-box | Kimi CLI | reviewer | | `kimi-bot-andresmgsl` | kimi-box | Kimi CLI | reviewer |
This table is the **as-built** bench — five boxes, two of them dual-role. It
is not the same thing as crew's
[`fleet.roster`](https://github.com/heavy-duty/crew/blob/01fb49cd717a0f4c83df286af86411c69e3c2363/fleet.roster),
whose own header declares it the **target** environment: seven single-role
boxes, the dual-role claude and codex boxes each split into a builder and a
reviewer member. The delta is exactly that split (plus each new box needing
its own identity at login); until it is deployed, this table is the record of
what actually runs, and `fleet.roster` is where it is going.
Review panel per PR = the reviewer bench minus the PR's author (recusal by Review panel per PR = the reviewer bench minus the PR's author (recusal by
construction). Only humans merge — enforced as permissions (the agents team construction). Only humans merge — enforced as permissions (the agents team
holds the triage role, not write), not as convention. holds the triage role, not write), not as convention.
## Anatomy of a duty loop ## Anatomy of a duty loop
Every box runs the same skeleton, adapted to its CLI: The mechanism is no longer described here. The five hand-rolled duty scripts
converged into crew's shared engine, and a prose mirror of running code in a
second repo is a second thing to keep true — this one drifted (it said cron
ran `duty.sh` directly and gave the hygiene sweep its own cron line; crew's
`duty.sh` records that separate line as the bug it fixed, sharing
`~/duty/work` unlocked). How a tick actually works — cron fires
[`bin/tick.sh`](https://github.com/heavy-duty/crew/blob/01fb49cd717a0f4c83df286af86411c69e3c2363/shared/bin/tick.sh),
the only cron target, which wraps
[`bin/duty.sh`](https://github.com/heavy-duty/crew/blob/01fb49cd717a0f4c83df286af86411c69e3c2363/shared/bin/duty.sh)
in a non-blocking `flock` with one evidence line per boundary; the boot gate
and crash recovery; the session runner; backlog hygiene self-scheduling
inside the duty tick under the same lock — lives with the code:
[`shared/README.md`](https://github.com/heavy-duty/crew/blob/01fb49cd717a0f4c83df286af86411c69e3c2363/shared/README.md)
is the map, provenance table included. Sessions stay stateless and
disposable — all state lives on the board (issues, PRs, labels) and in git
branches; detection is the engine's, judgment is the session's.
- **Tick:** cron `*/5` runs `~/duty/duty.sh` under a non-blocking `flock`; the What belongs here is what a wake *means*:
triage box adds an hourly hygiene sweep under its own lock. Holding the lock
is load-bearing: a tick that acquires it *knows* nothing else is running on - **The registry is the scope.** Every duty *queue* works the repos named in
this identity. the box's `~/duty/repos.txt` and only those: the boards triage sweeps, the
- **Poll:** the script reads `~/duty/repos.txt` and queries GitHub with `gh` bound on a builder's author-side duties, the reviewer's queue. **A review
for work matching the box's role. Whose registry that file is depends on request outside the registry is not authorization to review** — it is
the role: the triage box's `repos.txt` **is** its registry — adding a repo logged, a WARN naming the request, and never acted on; the repair is the
is adding a line — while a reviewer's registry is the org itself, and its operator adding the repo to the list. The rule changed on 2026-07-25
`repos.txt` is a backstop that cannot scope it (grok's copy says so in its (operator ruling, crew#16): the old doctrine — a reviewer's registry is
own first line). the org itself and no repo filter may gate a request — made every box's
- **Act:** when there is work, the script launches the box's CLI as a one-shot write surface the whole org, which no registry could bound: the drill's
session with a role prompt; the session does the work via `gh` as the box's containment interlock narrowed `repos.txt` and so confined triage and
own identity, then exits. Sessions are stateless and disposable — all state hygiene, but not review, the one module that submits verdicts.
lives on the board (issues, PRs, labels) and in git branches. [`lib/duty-review.sh`](https://github.com/heavy-duty/crew/blob/01fb49cd717a0f4c83df286af86411c69e3c2363/shared/lib/duty-review.sh)
states the rule and implements the WARN.
- **One wake is registry-independent, by design: attention** (next section).
The registry bounds what a box goes *looking for*, not what is handed to
this identity *by name*.
### Wake conditions ### Wake conditions
@ -62,9 +91,21 @@ sibling agent outranks self-directed continuation, and it is frequently the
very thing that unparks the work resume would otherwise pick up. The query is very thing that unparks the work resume would otherwise pick up. The query is
the authenticated-user endpoint — the authenticated-user endpoint —
`gh api "/issues?filter=assigned&state=open&labels=attention"` — one call, no `gh api "/issues?filter=assigned&state=open&labels=attention"` — one call, no
search index (the reviewer trigger below already records that the index search index (the review queue below already records that the index lags) —
lags), and like the review-request trigger it reaches repos `~/duty/repos.txt` and, **alone among the wakes, it reaches repos `~/duty/repos.txt` does not
does not name. name.**
That reach is deliberate, and it survives the registry rule above as its one
stated exception. An `attention` assignment is work handed to this identity
by name, and **the assignment is what carries the authorization** — there is
nothing here for a repo list to scope, because the box is not choosing where
to look.
[`lib/duty-attention.sh`](https://github.com/heavy-duty/crew/blob/01fb49cd717a0f4c83df286af86411c69e3c2363/shared/lib/duty-attention.sh)
queries the cross-repo endpoint on purpose and says so in its header; a fix
that bounds this wake to the registry would re-create the #16 incident below.
(Crew's `conf/repos-default.txt` header currently claims *every* duty module
is registry-bounded — that contradiction is crew's, raised there as a
discussion; this file records the exception as it is implemented today.)
Each demand gets **exactly one session, and the ack bounds it**: the Each demand gets **exactly one session, and the ack bounds it**: the
session's first act, before any of the demanded work, is the pickup comment session's first act, before any of the demanded work, is the pickup comment
@ -85,47 +126,68 @@ wake: [#16's 16:49Z
ruling](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5061051198) ruling](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5061051198)
authorized the last open acceptance criterion on a `claimed` issue and sat authorized the last open acceptance criterion on a `claimed` issue and sat
unowned for over an hour — the box answered every state signal that day and unowned for over an hour — the box answered every state signal that day and
never saw the comment, and the eventual pickup ran on a manual bridge. Like never saw the comment, and the eventual pickup ran on a manual bridge. The
the notifier's queue below, this wake is the spec for a box-side change only wake is no longer on paper: `duty-attention.sh` is deployed engine, and
the operator can make; until `duty.sh` polls it, the wake exists on paper — `duty.sh` runs it first on every box, whatever its roles.
though one consumer already polls for the label and no-ops while it is
absent, so the wiring can be verified live the day the row lands.
- **Triage:** new discussions to mint from, builder questions on issues, stray The engine's duty order is fleet-standard
issues to reconcile, `@`-mentions, hourly hygiene (stale claims, label ([`bin/duty.sh`](https://github.com/heavy-duty/crew/blob/01fb49cd717a0f4c83df286af86411c69e3c2363/shared/bin/duty.sh)):
invariants), and a `needs-ruling` standing **past 24h** — the ladder's last **attention → triage signals → review queue → resume → build → handoff →
rung makes the option triage's to pick, and this wake list is where triage rebase → worktree hygiene → backlog hygiene (hourly)** — attention
learns such an item exists (see the notifier section below). role-independent and first, then each duty family the box's roles enable.
- **Builders**, in priority order: **resume** (an open draft PR of mine, or a The earlier form of this file folded handoff and rebase into the other
claimed issue with my `build/*` branch but no PR — possible only if a builder wakes; they are duties of their own.
previous session died mid-work), a `ready` issue to claim, a completed
review round on my PR (act on whole rounds, never single verdicts), my PR - **Triage signals**, per registry repo: `needs-triage` issues,
fully approved (write the closing summary, flip to `state:needs-human`, queue-unlabeled strays, discussions without triage's voice, unread
request the human), my PR `CONFLICTING` (rebase; never act on `UNKNOWN` `@`-mentions (their own session), and `blocked` issues whose named blockers
post-merge flap). have all landed — a lead the session verifies, never a label the engine
- **Reviewers**, one candidate set from two merged sources. Source 1, flips. Backlog hygiene (stale claims, label invariants) runs hourly,
authoritative: every open PR in the `heavy-duty` org **plus the named bot self-scheduled inside the duty tick. A `needs-ruling` standing **past
forks** that lists me in `requested_reviewers`, enumerated straight from 24h** is still triage's to pick up — the ladder's last rung makes the
the pulls API — never `gh search`, whose index lags (cast#143, option triage's to choose — but, like the notifier queue below, that
incubator#25 and box#164 each sat unreviewed behind it). A review request detection row is on paper only today.
is authorization, so no repo filter may gate it. Source 2, backstop: the - **Review queue**: one candidate set, enumerated from the pulls pages of
`repos.txt` poll for an open PR by someone else whose head I have not yet every registry repo — object endpoints, never the search index for the
reviewed — it only **adds** candidates the sweep may have missed (an queue itself, whose lag left cast#143, incubator#25 and box#164 sitting
org-enumeration failure, say) and never concludes "nothing to do". The unreviewed — filtered to PRs listing me in `requested_reviewers`, deduped
sources are merged and deduplicated by (repo, PR) **before** acting, not by (repo, PR) before acting (the sequential shape double-announced on
run as sequential passes — the sequential shape double-announced on ceremony#32), and worked oldest-first. One search-backed **awareness pass**
ceremony#32, when the request sweep and the repo-list poll each acted on per tick reports requests outside the registry and never acts on them —
the same PR in one tick (operator protocol 2026-07-23) — and worked the scope rule above. One verdict per head, deduplicated against my own
oldest-first. Unchanged: one verdict per head, deduplicated against my latest review's SHA; a re-request at an unchanged head is answered with an
own latest review's SHA. auto-approve through the verdict gate rather than left as a stale blocker
(operator ruling 2026-07-23, ceremony#94).
- **Resume** (builders, checked before build): an open draft PR of mine, or
a `claimed` issue whose `build/*` branch exists on my fork with no open PR
— a session died between first push and PR creation. A branch whose PR
already **merged** is a post-merge wait, never resumed (#172,
incubator#55/#64).
- **Build**: a `ready` **unclaimed** issue (an assignee means mid-claim, not
pickable), or a completed review round on my PR — a changes-request with
no panel review request still outstanding; whole rounds, never single
verdicts.
- **Handoff**: a round of mine that converged — every panelist's latest
opinionated review approves the current head, no panel request
outstanding, mergeable right now, `state:needs-human` not already set.
Convergence is computed from `latestOpinionatedReviews`, never
`reviewDecision`, which stays empty without branch protection and silently
stalled rounds for a day (ceremony#26, #39).
- **Rebase**: my PR `CONFLICTING` — and only `CONFLICTING`; `UNKNOWN` is
GitHub's post-merge recompute flap and waits. A conflicting draft belongs
to resume.
- **Worktree hygiene**: a `build/*` worktree is removed only when its branch
has PR history and no PR on it remains open; a branch with no PR at all is
an in-flight claim and stays.
#### The operator notifier — the `needs-ruling` queue #### The operator notifier — the `needs-ruling` queue
The operator notifier (`notify.sh`, on the triage box) watches open PRs The operator notifier (`notify.sh`, a fleet singleton on the triage box; its
carrying `state:needs-human`. That poll never reads `needs-ruling`, which mechanism is crew's too) watches open PRs carrying `state:needs-human`. That
lives mostly on *issues* — so an escalation waits invisibly on the very human poll never reads `needs-ruling`, which lives mostly on *issues* — so an
it names. Not hypothetical: on 2026-07-23 alone, three escalations spent escalation waits invisibly on the very human it names. Not hypothetical: on
their whole lives outside the operator's view — [#16's fork-PR-workflows 2026-07-23 alone, three escalations spent their whole lives outside the
operator's view — [#16's fork-PR-workflows
question](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5053302689) question](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5053302689)
(raised 01:23Z, [ruled 09:24Z](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5056705884) (raised 01:23Z, [ruled 09:24Z](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5056705884)
— eight hours in which the board showed a `claimed` issue indistinguishable — eight hours in which the board showed a `claimed` issue indistinguishable
@ -135,7 +197,7 @@ and [epic #50's own 13:04Z
flag](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5058713181), flag](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5058713181),
which surfaced only because a human happened to look. This file records how which surfaced only because a human happened to look. This file records how
the fleet actually runs; that is why this wiring changed (#50 D16). The spec the fleet actually runs; that is why this wiring changed (#50 D16). The spec
for the box-side update: for the engine-side update:
- **The second query.** Alongside the `state:needs-human` PR poll, `notify.sh` - **The second query.** Alongside the `state:needs-human` PR poll, `notify.sh`
polls **open issues and PRs labelled `needs-ruling`** across every repo in polls **open issues and PRs labelled `needs-ruling`** across every repo in
@ -164,54 +226,43 @@ Nothing box-side ever sets, clears, or decides `needs-ruling` (#50 D9, D15):
the notifier and triage's past-24h wake above *report and pick up* what the the notifier and triage's past-24h wake above *report and pick up* what the
board already shows; the label itself moves only by the doctrine's hands. board already shows; the label itself moves only by the doctrine's hands.
`~/duty/repos.txt` and the duty scripts live inside each box and are the The duty engine is crew's shared tree, one source deployed to every box;
operator's to change; this descriptive edit is the spec for those box-side `~/duty/repos.txt` stays per-box, the operator's to edit — which is what
updates. The reviewers' request sweep is one such spec made real — deployed makes the registry rule an operator decision rather than a sweep's. Specs
on all four reviewer boxes since 2026-07-23 (the Reviewers wake above). The written in this file have a record of becoming engine: the attention wake
notifier's `needs-ruling` queue is still on paper only: `notify.sh`'s one and the reviewers' request sweep both started here as paper (the sweep's
label filter today is `state:needs-human`. org-wide form was then retired by the 2026-07-25 scope ruling). The
notifier's `needs-ruling` queue is the one still on paper: at the stamped
### Resilience crew SHA, `notify.sh`'s only label filter is `state:needs-human`.
- **Boot gate:** each tick compares the kernel boot id
(`/proc/sys/kernel/random/boot_id`) to a stored marker. First tick after any
reboot runs credential + disk probes; the marker is written only when auth
actually works, so a box with dead credentials re-checks loudly every tick
instead of silently skipping duty.
- **Crash-only resume:** there is no session state to restore. The recovery
path *is* the normal path: the resume wake condition reads the board, posts
`⟲ resuming from <sha>`, and continues from the worklog. Rebooting a box
never loses work that was pushed.
- **Checkpoint discipline (builders):** open the PR as draft at the first
commit with a `## Worklog` checkbox list; check off and push after every
step. The board and the branch are the only memory.
- **Worktree isolation:** builders build each PR in its own `git worktree`;
reviewers check out PR heads in throwaway detached worktrees and remove them
after the verdict. Main clones stay parked on the default branch, always
clean; stale worktrees are pruned by the boot gate.
### Conventions on the board ### Conventions on the board
- `🔎 reviewing head <sha>` — a reviewer announces work before starting, so - `🔎 reviewing head <sha>` — a reviewer announces work before starting, so
liveness is visible instead of hoped for. liveness is visible instead of hoped for.
- `⟲ resuming from <sha>` — a builder announces recovery after interruption. - `⟲ resuming from <sha>` — a builder announces recovery after interruption;
there is no session state to restore, so the recovery path *is* the normal
path: read the board, continue from the worklog. Rebooting a box never
loses work that was pushed.
- Checkpoint discipline (builders): open the PR as draft at the first commit
with a `## Worklog` checkbox list; check off and push after every step.
The board and the branch are the only memory.
- Claim ritual: comment on the issue + self-assign + label flip, before any - Claim ritual: comment on the issue + self-assign + label flip, before any
branch exists. branch exists.
- Handoff: the author closes an approved PR's round with a summary comment, - Handoff: the author closes an approved PR's round with a summary comment,
flips `state:needs-human`, and requests the human — merging is never the flips `state:needs-human`, and requests the human — merging is never the
fleet's job. fleet's job.
- Worktree isolation: builders build each PR in its own `git worktree`;
reviewers check out PR heads in throwaway detached worktrees and remove
them after the verdict. Main clones stay parked on the default branch,
always clean.
## Where this is going ## Where this is going
This wiring proved itself on day one (seven merged PRs, unanimous three-model This wiring proved itself on day one (seven merged PRs, unanimous three-model
review convergence on #39, and a full-fleet crash recovery). The plan: review convergence on #39, and a full-fleet crash recovery), and the plan it
carried has since half-happened: the five per-box duty scripts converged into
1. Once the ceremony machinery is complete and adopted, each agent will be **heavy-duty/crew** — the shared engine, the `crew` CLI, and the fixture
asked to write a **detailed, replicable description of its own setup** tests — so standing up a box is a bootstrap, not an archaeology dig. What
cron lines, duty script, prompts, probes — as durable documentation. remains is the roster: `fleet.roster` names the seven-box single-role target,
2. Those five descriptions get converged into a **solidified fleet-management and the bench above is still the five-box as-built. Until the split lands,
solution** (duty loops as reusable templates, likely living alongside the this file is the map of what runs — and crew is the map of how.
rig templates registry), so standing up this roster on a new repo — or a
whole new fleet — is a bootstrap, not an archaeology dig.
Until then, this file is the map.

13
changelog.d/187.md Normal file
View file

@ -0,0 +1,13 @@
### Fixed
- FLEET.md no longer says a review request outside the registry is
authorization: `repos.txt` is the scope for the review queue, out-of-scope
requests are logged and never acted on, and the attention wake is stated
as the one registry-independent exception, by design (#187).
### Changed
- FLEET.md's duty-loop mechanism is a pointer to crew's shared engine; the
wake lists follow the engine's duty order, the roster keeps the as-built
bench beside `fleet.roster`'s target, and the reconciliation stamp names
crew@`01fb49c` (#187).