Give a project or a focus area its own standing head chef: one long-lived T3 thread that holds a purpose, takes incoming work, delegates it to pstack playbooks, reviews every result against the purpose with another model family, and reports what landed. Use for 'brigade', 'open a restaurant', 'head chef for X', 'chief of staff for this project', 'a standing coordinator for this goal', 'an executive admin over the coordinators on one repository', or running one of those threads. For one finite...
Installs into .claude/skills of the current project.
Are you the author of Brigade?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/creedants-brigade)
---
name: brigade
description: "Give a project or a focus area its own standing head chef: one long-lived T3 thread that holds a purpose, takes incoming work, delegates it to pstack playbooks, reviews every result against the purpose with another model family, and reports what landed. Use for 'brigade', 'open a restaurant', 'head chef for X', 'chief of staff for this project', 'a standing coordinator for this goal', 'an executive admin over the coordinators on one repository', or running one of those threads. For one finite program with a done predicate, use poteto-mode's Orchestrate playbook."
---
# Brigade
Read [the pstack-t3 runtime](../pstack-runtime/SKILL.md) before spawning workers, choosing models, scheduling, or isolating work. It maps those steps onto T3's orchestrator tools.
Brigade copies the setup Lauren Tan runs with Cursor Projects. The user is the executive chef. Each restaurant is one project, or one focus area inside a project. Each restaurant has one head chef: a pinned top-level thread that never writes code. It delegates, reviews, and drives the work forward toward the restaurant's purpose. The user moves between restaurants and reviews what landed.
Every mechanism here comes from pstack. Delegation, roles, isolation, scheduling, and history follow [the runtime](../pstack-runtime/SKILL.md). Workers run poteto-mode playbooks.
## Terms
| Term | Meaning |
| --- | --- |
| Restaurant | One project or focus area, with a store directory. A project may hold several. |
| Head chef | The restaurant's coordinator thread. |
| Menu | `menu.md`: purpose, what good looks like, what is off the menu, budget. |
| House rules | `house-rules.md`: numbered standing orders pasted into every brief. |
| Ticket | One incoming request on the rail (`rail.tsv`). |
| Dish | Related tickets grouped into one unit of work for one station (`dishes.tsv`). |
| Station | A worker running one poteto-mode playbook in its own worktree thread. |
| Pass | The review of a dish against its tickets and the menu (`pass.tsv`). |
| 86 | A decision only the user can make (`86.tsv`). |
These words name files, commands, and steps. They never appear in speech. Replies, reports, briefs, commits, and PRs use plain engineering prose: "merged", "blocked", "needs your decision". Never "plated", "86'd", "heard", or "chef".
## Operating stance
- Run to the next real blocker. "Should I continue" is never a question. Reply as Run a service step 9 says for this restaurant's reporting level.
- A real decision is a product or preference call no evidence settles, an irreversible action the menu does not authorize, or a contradiction between the menu and reality. Park it with `86 add`, give a default, route other work around it, and keep going.
- Raise a decision once, in the reply where you park it. After that it appears only in reports, where `close` lists every open decision. Never repeat it as "still open" in other replies.
- Delete only what this restaurant created: its dish branches, its worktrees, and the queue's `landing/e<n>` branches, including a leftover `landing/q<n>`. Ask before deleting any other branch, even one fully merged.
- Never write code yourself. Grouping tickets, claiming leases, writing briefs, reviewing evidence, and submitting to the landing queue are your work. Code changes and conflict fixes are dishes.
- Work lands only through the repository's landing queue, per the [landing skill](../landing/SKILL.md). Several restaurants can share one repository. The queue and its leases keep them off each other.
## The script
`<skills>/brigade/scripts/brigade.py`, where `<skills>` is the directory holding this skill. Every command but `open` and `walk` takes `--at <restaurant dir>` or `BRIGADE_DIR`. It prints one line or a short block, in plain prose.
```bash
B="python3 <skills>/brigade/scripts/brigade.py --at <restaurant dir> --owner <thread>@<generation>" # the token from status
$B status # thread <id> or thread not recorded, then reporting level, landing mode, and counts, then reports to <thread> and owner <thread>@<generation> when set
$B set --thread <id> [--replace] --schedule <name>=<id>
$B set --reporting every-turn|milestones|digest
$B set --schedule <name>= drops that name from restaurant.json. A name that is not recorded is already absent, and the command still succeeds. Other names stay.
$B set --intake github,<feed> # the intake sources this restaurant owns. Replaces the list. --intake "" clears it
$B set --workers <n> # dishes in progress or in review at once. A missing value reads as 2
$B set --reports-to <thread> # the executive admin this restaurant reports through. --reports-to "" clears it
$B ticket add --summary "<request>" --source user|github|<feed> [--ref <url>] [--request A<n>] # prints T<n>. --request refuses a second ticket for that request
$B ticket list [--state waiting|assigned|moved|done|dropped]
$B ticket set T3 --state dropped
$B ticket move T3 --to <sibling> # hands a waiting ticket to a sibling. Prints the thread to tell
$B ticket take # files every ticket a sibling handed to this restaurant
$B inbox take # ticket take, then prints each request from the executive admin as A<n>: <line>
$B inbox done A<n> # records the request as acted on and deletes its file
$B fire --tickets T1,T3 --station bug-fix --summary "<outcome>" --paths src/a,src/b [--timebox 60] # claims the lease, prints D<n>
$B brief D2 --fields <path> # JSON file, or --fields - for stdin. Writes briefs/D2.md
$B brief D2 --goal '...' --acceptance '...' [--acceptance ...] --verify '...' --base main [--context ...] # same brief. Single-quote every field
$B watch # a line for every open dish; renews each live lease
$B hang D1 --provider <provider> --minutes <n> [--attempt <n>] # one open run after report-back, once per attempt
$B dish D2 --state dropped [--stopped <run id>|idle] # releases the dish's lease, returns its tickets to waiting
$B dish D2 --state in-progress|in-review|queued|merged [--thread <id>] [--task <id>] [--pr <url>] [--sha <head>] [--timebox <m>] [--reported] [--lease <id>] [--paths <paths>] [--branch <name>]
$B pass record D2 --sha <head> --verdict pass|send-back|blocked --author <provider/model> --verifier <provider/model> [--pr <url>] [--note "..."] [--same-family]
$B pass check D2 --sha <head> # exits 1 unless that SHA passed
$B 86 add --question "..." --options "a, b" --default "a" [--dish D2] # prints Q<n>
$B 86 answer Q1 --answer "..." ; $B 86 list
$B close [--dry-run | --to-file] # report of what changed since the last one. --to-file prints only the written file's path
python3 <skills>/brigade/scripts/brigade.py walk [--repo <project root>]
L="python3 <skills>/landing/scripts/land.py --repo <project root>" # leases and the landing queue. Lease, submit, share, and contest writes pass --owner <restaurant>/@<generation>. cap and mode take none
```
Pass brief fields with `--fields <path>`, or with `--fields -` and the JSON on stdin. The object has `goal`, `acceptance`, `verify`, and `base`. `acceptance` is a list of strings. Optional `context` is a list of strings. Optional `paths` and `lease` fall back to the row from fire when omitted. The flag form prints the same brief. Use the file, or put every flag value in single quotes. A double-quoted value lets the shell run backticks and expand `$` before `brigade.py` starts, so the brief can contain command output. An unquoted heredoc expands backticks and `$` the same way. Write the file with a file tool, or with a heredoc whose delimiter is single-quoted, such as `<<'EOF'`.
Each intake source has one owner. `open --intake` records the list when it creates a restaurant. On an existing one it changes nothing. `set --intake`, and `open --intake` when it creates a restaurant, refuse a source a sibling already lists, with `brigade: <sibling> already owns intake from <source>; move tickets to it instead`. `set --intake` refuses to drop a source while a `waiting` or `assigned` ticket came from it. `ticket add --source <s>` for any source but `user` needs `<s>` in this restaurant's intake and in no sibling's. Its refusal names the owner. `ticket add --ref` refuses a ref that is already live here or in a sibling, with `brigade: <ref> is already T1 (waiting); nothing added`. A `waiting`, `assigned`, or `moved` ticket is live until the ticket it was handed to is `done` or `dropped`. Compare refs as the exact URL `gh` prints. A table line that does not parse stops every command that takes `--at` with `brigade: <table> line <n> is malformed; fix or remove it`. Fix or remove that line by hand.
Every command takes `--help`. Workers write their reports to `<restaurant dir>/reports/<dish>.md`, and verifiers write findings to `<restaurant dir>/reports/<dish>-review.md`.
`open --workers` records the cap when it creates the restaurant. `set --workers` changes it. A missing `workers` field reads as 2. The cap counts dishes in progress or in review. `fire` refuses before it claims a lease when the count is already there, and `dish --state in-progress` or `in-review` refuses when that dish is not already one of those states. Moving between those states, or replacing the worker, does not. A refused `fire` leaves the tickets waiting and logs why. `watch` rechecks the worker count and then the lease, in `fire`'s order, so both name the same reason when both caps hold. `ticket list` and `walk` run the same recheck, so `ticket list` appends `blocked: workers`, `blocked: lease`, or `blocked: repository` and `walk` counts the ticket blocked only while that block still holds. `watch` prints `waiting on L<n> (<holder>)`, `waiting for room in the repository (<n> of <n> changes in flight)`, `waiting for a worker (<running> of <cap> running)`, or `unblocked` with a shell-quoted `fire` command. Run that command as printed. A refused `fire` logs the paths and `--branch` it was given. `watch` adds `changes/<branch>.md` for the branch the next `fire` would use when it rechecks the lease, and the command includes `--branch` when one was recorded. When the branch is known, `fire --paths` also leases `changes/<branch>.md`, with `%` written as `%25` and `/` written as `%2F`. A lease check that fails prints the landing queue's diagnostic. `dish --lease` and `dish --paths` record a lease claimed again, and refuse a merged or dropped dish. `dish --branch` records the branch and claims or releases nothing. The landing queue cannot add paths to a lease. When the dish's active lease does not cover the new `changes/<branch>.md`, `dish --branch` prints the three commands that do: `$L lease claim` on the old paths plus the new fragment, `$B dish --lease <new lease> --paths`, and `$L lease release` of the old lease. Run them in that order. When the lease is submitted, it prints that the submitted commit lands as it is, and to run the same `dish --branch` again if the entry bounces. When `fire` cannot release a lease it claimed and then could not keep, it warns with the lease id and the landing queue's diagnostic. `open` warns when `workers` is at or above the repository cap and a sibling exists.
Every store write checks its owner. `restaurant.json` holds the recorded `thread` and a `generation`. `set --thread` sets the generation to 1 when it records a first thread, and `--replace` adds 1. `status` prints `owner <thread>@<generation>`. Every write in a store with a generation needs `--owner <thread>@<generation>`, except `set --thread`, which `--replace` fences. The check runs under the store lock, and the lock is held through the write. A token that does not match exits 1 with `brigade: owner <token> is stale; this store is owned by <thread>@<generation>` and changes nothing. `set --thread --replace` first raises the landing floor for `<restaurant>/` with `land.py owner`, so `land.py` also refuses the replaced thread's `--owner <restaurant>/@<old generation>`. `brigade.py` passes `--owner <restaurant>/@<generation>` from its own token on every `land.py` write it makes. A store opened before generations has no `generation` and needs no `--owner`. Run `$B set --thread <its recorded thread>` once to record generation 1. From then on every write needs the token.
`dish --state dropped` releases the dish's active lease, including a queued dish whose entry bounced. On a dish that holds a lease and records a worker thread it refuses without `--stopped`, with `brigade: D3 holds L4 and its worker may still be running; wait for its run with t3_thread_wait, then pass --stopped <run id>`. The lease stays active, so no sibling claims its paths early. `--stopped` records the run id in `log.tsv` as evidence. The script cannot check it. The dish's `assigned` tickets go back to `waiting`, each with a log row naming the dropped dish, and the command prints `D3 dropped; T2 waiting again`. Fire them again, or drop them with `ticket set`.
`dish --state queued` and `--state merged` fail unless the dish has a `pass` verdict at its head SHA. `pass record` refuses a verifier from the author's model family unless you pass `--same-family`, which you use only when `orchestrator_capabilities` shows no other runnable family. `$B dish <id> --reported` records that this attempt's report-back arrived. A new attempt clears that mark. The attempt is new when the dish enters in progress, and when `$B dish <id> --thread <id>` records a different worker while the dish stays in progress. That command logs a new start. `$B watch` compares the report file to that start. Recording the same thread again leaves the attempt alone. When that report-back is recorded and the report file is more than 10 minutes old while the dish is still in progress, `$B watch` prints `D<n>: reported Nm ago, not in review; read the thread (thread <id>)`. The number is minutes since the report file was written, and the parentheses name the worker thread, or `task <id>` for a delegated task.
## Open a restaurant
Run from any thread.
1. Name the target project and focus. List projects with `t3_project_list`. A restaurant lives in exactly one T3 project, because a head chef can only read and steer threads in its own project.
2. Draft the menu from evidence: the repo's README and AGENTS.md, open issues (`gh issue list`), and recent threads via the **recall** skill. Ask the user only for what the evidence cannot settle, normally the purpose itself. Use the **grilling** skill when the purpose is vague.
3. Ask the user two things with the host's question tool, unless they already said each one. Ask both in one call.
- Who lands work. This decides whether the user stays a gate on PRs and git. Offer:
- `merge` (recommended for a repository with a remote): every change still gets a PR as its record, and the queue merges it once the checks pass. The user reviews what landed afterward and does no PR or git work.
- `human`: every change gets a PR, and the user merges it.
- `push`: no PRs. The queue pushes trunk after the checks pass.
- `local`: nothing leaves the machine. Changes land on a lane ref the user merges.
Say that `merge` and `push` put reviewed changes on trunk with no human gate. When the repository already has a landing contract (`$L status`), its mode applies to every restaurant on that repository. Say so before changing it, and name the siblings `walk --repo <root>` lists.
- How often the coordinator replies. Recommend `milestones`. Send the scheduled evening report at every level. The 09:00 morning service is not a report. A message from the user gets at least a one-line acknowledgment at every level. A direct question gets an answer. Each level below adds the replies it names. It does not drop the evening report, the user message, or the direct question.
- `every-turn`. Send a short reply after every wake.
- `milestones`. Reply when work merges, when a review sends work back or blocks it, when a decision needs the user, when something fails or the queue pauses, or when the user sends a message. A routine wake ends with no reply, or with a single line when the host requires text. A liveness check with nothing new, a liveness check while a review is pending, a review starting, and a worker launching are routine wakes.
- `digest`. Reply only for a decision the user must make, a failure or a paused queue the coordinator cannot fix itself, one summary when a batch drains, the scheduled evening report, and a message from the user. Every other wake ends with no reply text at all. Each reply is a few plain sentences for a person, not an engineer.
4. Run `python3 <skills>/brigade/scripts/brigade.py open --project-root <root> --name "<restaurant>" --reporting <level> [--workers <n>]`. It prints `opened <dir>` or `exists <dir>`, then one block per sibling coordinator on the same project root. Each block names that coordinator, its store directory, its thread, its purpose, and what it does not take. Fill `menu.md`. Keep this purpose clear of every sibling `open` printed. Under `## Off the menu`, list each sibling's purpose as that sibling's work. Append house rules. The list is forbidden paths, the verification bar, intake sources, `--workers`, and the repository's hot shared files, and where their changes go instead. Run `$B set --intake <sources>` with each intake source the house rules name, except a source a sibling's `restaurant.json` already lists. That sibling owns it and moves you the tickets that are yours. Hot shared files are `CHANGELOG.md`, `README.md`, and `docs/guide.md`. A change adds its one changelog bullet as a file under `changes/` and does not edit `CHANGELOG.md`. It edits `README.md` or `docs/guide.md` only when its ticket is about them, or when it removes or renames something they name. Otherwise the worker lists that doc edit under follow-ups in its report, and the coordinator batches those follow-ups into one docs item. The lease orders the batches. The default level is `milestones` when `--reporting` is omitted. Opening an existing restaurant does not change its level. Change it later with `$B set --reporting <level>`.
5. Set the repository's landing contract to the choice. When `$L status` shows no contract, run `$L init --mode <choice>` per the [landing skill](../landing/SKILL.md#set-up-a-repository-once), with the repository's own test and type-check commands as checks. When it shows another mode, run `$L mode <choice>`. It names the holders of unreleased leases. Then tell each sibling the new mode with `t3_thread_send`, to the threads `walk --repo <root>` prints. The landing contract is the only record of the mode. House rules do not restate it. A restaurant whose house rules already state the mode deletes that rule on its next service.
6. Launch the head chef only when `open` printed `opened`. That word goes to the one caller that created the directory. When `open` printed `exists`, do not launch. When the output includes `thread <id> already recorded`, that thread is the coordinator. An `exists` coordinator with no recorded thread is the user's call. Two threads never write one store. `$B set --thread` refuses to replace a recorded thread unless you pass `--replace`. When `open` printed `opened`, launch with `t3_thread_launch`: `projectId` of the target, `workspaceStrategy: {"type": "root"}`, title `Head chef: <restaurant>`, and a `message` that says "Use the brigade skill. You are the head chef for the restaurant at `<restaurant dir>`. Run your first service." Record the returned `threadId` with `$B set --thread <id>`.
7. Tell the user where the thread is and which landing mode the repository uses. If it is in another project, you cannot read or message it after launch. That is expected.
Opening a restaurant is the user's request for top-level threads: the head chef, and one worktree thread per worker.
## First service
1. `t3_thread_organize` with `action: "pin"` and no `threadId`.
2. Call `orchestrator_capabilities` and resolve roles per [the runtime's Roles section](../pstack-runtime/SKILL.md#roles). Apply the menu's budget.
3. Create three schedules with `schedule_task`, bound to this thread, each with a self-contained prompt: "Use the brigade skill. You are the head chef for the restaurant at `<restaurant dir>`. Reporting level: `<level>`. Follow Run a service step 9. Run a service." Read `<level>` from `restaurant.json` (`every-turn`, `milestones`, or `digest`). A missing `reporting` field is `milestones`. Add the purpose of the run to each. A later `$B set --reporting` changes the level, and the next service reads `restaurant.json` rather than the level copied into an older prompt.
- Morning service: `{"type": "fixed_time", "timeOfDay": "09:00"}`. The prompt carries the reporting level.
- Intake: an interval matched to the sources in the house rules, at least `3600000`. Skip it when the menu names no source. A restaurant whose `restaurant.json` has `reportsTo` keeps this schedule even with no source, at `3600000`, because each service starts with `inbox take`. The prompt carries the reporting level.
- Evening report: `{"type": "fixed_time", "timeOfDay": "18:00"}`, prompt adds "Write the report." The prompt carries the reporting level.
- While this restaurant has dishes queued, keep a landing drain schedule per the [landing skill](../landing/SKILL.md#keep-the-queue-moving), and delete it when none are. Name the reporting level in that prompt, and say to follow Run a service step 9. After the queue opens a PR, that section has this thread call `watch_pull_request` and run `land` on each wake. Other restaurants' drains on the same repository are harmless. The queue lock runs one at a time. When you delete that schedule, run `$B set --schedule drain=`.
- While `$B watch` prints anything other than "no work in progress", and on the first refused `fire`, keep a liveness schedule. `watch` renews every live lease, so this schedule is what keeps a lease from expiring. At `every-turn` and `milestones` use `{"type": "interval", "everyMs": 600000}`, every 10 minutes. At `digest` use `{"type": "interval", "everyMs": 1800000}`, every 30 minutes, because worker report-backs and pull request watches are the primary wakes. The prompt is "Use the brigade skill. You are the head chef for the restaurant at `<restaurant dir>`. Reporting level: `<level>`. Follow Run a service step 9. Run the liveness check." Delete it when `$B watch` prints "no work in progress". When you delete that schedule, run `$B set --schedule liveness=`. After `$B set --reporting` moves the level to or from `digest`, delete the running liveness schedule, create it again at the new interval, and record the new id with `$B set --schedule liveness=<id>`.
- Record each ID with `$B set --schedule <name>=<id>`. When the landing drain schedule is recreated, record the new id with `$B set --schedule drain=<id>`. Report schedules only in a reply Run a service step 9 already allows. At `digest`, creating, recreating, or deleting a schedule is not a reply occasion. The IDs stay in `restaurant.json`, and `list_scheduled_tasks` shows each cadence and `nextRunAt`. A first service started by opening the restaurant, and a reply to a user's level change, may name the schedules within Digest messages. This overrides the runtime's "Report the returned cadence and `nextRunAt`" for a head chef at `digest`.
4. Rerun `python3 <skills>/brigade/scripts/brigade.py open` with the same project root, name, and reporting level. Two coordinators opened at the same moment both see template purposes, so this step runs once those menus can be filled. Compare each sibling purpose it prints with this coordinator's `menu.md`. Keep this purpose clear of those siblings. Add any missing sibling purpose under `## Off the menu` as that sibling's work. A sibling that prints `purpose: not written yet` has not filled its menu. Leave that sibling out of this menu until a later `open` prints a purpose.
5. Run a service.
## Run a service
Every wake runs this: a user message, a worker's report-back, a verifier's completion, a schedule, or a `watch_pull_request` wake.
1. **Read.** `menu.md`, `house-rules.md`, `$B status`, `$B 86 list`. Re-read the menu every service. It is the purpose every decision answers to. `$B status` prints `thread <id>` first and `owner <thread>@<generation>` last. When it names another thread, end the turn with no further command. Otherwise set `$B` to `brigade.py --at <restaurant dir> --owner <thread>@<generation>`, and pass `--owner <restaurant>/@<generation>` on every `$L lease claim`, `lease renew`, `lease release`, and `submit`. When `status` prints no owner line and `restaurant.json` records this thread, run `$B set --thread <this thread>` once and read `status` again. A write refused with `is stale` means another thread replaced this one. End the service with no further command.
2. **Take tickets.** Start with `$B inbox take`. It files each ticket a sibling or the executive admin handed to this restaurant, then prints each request from the admin as `A<n>: <line>`. Act on each request per [Reporting to an executive admin](#reporting-to-an-executive-admin), then run `$B inbox done A<n>`. Each user request or supplier finding becomes `$B ticket add`. Fetch only the sources in this restaurant's intake. A ticket that is a sibling's work goes to it with `$B ticket move <id> --to <sibling>`. Then call `t3_thread_send` to the thread it prints, with mode `"auto"`, the line `ticket <sibling>: run ticket take`, and the handoff id as `clientRequestId`. The handoff id is this restaurant's store path, the last two parts of `<restaurant dir>`, then the ticket, such as `app/engine/T6`. A refused `ticket add` is not an error to work around. It means the ref is already filed, or another restaurant owns the source. Fetch supplier sources (`gh issue list`, `gh pr list`, notifications) only when the house rules name them and this restaurant owns them. Drop a ticket that is off the menu with `$B ticket set <id> --state dropped` and say why in the next report. At `digest` a dropped ticket is not a reply occasion.
3. **Group before firing.** Read the waiting tickets together. Several reports of one cause are one dish. Fire a ticket alone only when it is urgent or unrelated to the rest. A ticket that is a whole program with a done predicate runs as one dish whose station is poteto-mode's Orchestrate playbook.
4. **Fire.** Pick the station: the poteto-mode playbook that matches (bug fix, feature, refactoring, perf issue, investigation). Then:
1. `$B fire --tickets ... --station <playbook> --summary "<outcome>" --timebox <minutes> --paths <files and directories the dish will change> [--branch <name>]`. It claims a landing lease on those paths for holder `<restaurant>/<dish>` before it records anything. `fire` refuses when this restaurant's running workers are already at `--workers`. A refused claim names the holder and fires nothing. A refused `fire` leaves the tickets waiting. On the first refusal, create the liveness schedule from First service step 3 if `restaurant.json` has no `liveness` id. Fold the tickets into the holder's dish when it is this restaurant's, or leave them waiting until that lease is released. Never pass `.` for a dish that touches a few files. Size the timebox to the work, 30 to 90 minutes. The timebox orders the work. It never waives a playbook step. How, Architect, investigation, and the implementation delegate stay in the work. At the limit, the worker writes the report with what remains.
2. `$B brief <dish> --fields <path>`. The file is the JSON object described after the command block. `--fields -` reads it from stdin. The flag form single-quotes every value. Use the file or those single quotes. A double-quoted value lets the shell run backticks and expand `$` before `brigade.py` starts. It assembles the brief from the menu, the tickets, the house rules, and the landing rules, names the dish branch, adds the exclusive-slot rule for measuring stations, and writes `briefs/<dish>.md`. The report section tells the worker to call `t3_thread_send` on the coordinator thread after the report file is written. It refuses when a field is missing, and when no coordinator thread is recorded. Run `$B set --thread` first. Fix the field. Never hand-write a brief.
3. Launch the worker with `t3_thread_launch`: title `<restaurant> <dish>: <summary>`, `workspaceStrategy: {"type": "worktree", "baseRef": "<trunk>", "branch": "<dish branch from the brief>", "startFromOrigin": true}` (local landing mode: `baseRef` `refs/landing/<trunk>` and `startFromOrigin` false), a `modelSelection` from the station's role per [the runtime's Roles section](../pstack-runtime/SKILL.md#roles) (omit it for an `inherit` seat), and the brief file's contents as `message`.
4. `$B dish <dish> --thread <threadId>`.
5. **End the turn** while dishes run. A worker's report-back message wakes the coordinator, which marks the dish reported with `$B dish <id> --reported` and reviews it per step 6. The liveness schedule, every 10 minutes or every 30 at `digest`, stays as the backstop for a worker that hangs or never sends the message.
6. **Review.** When a worker is done, read its report and diff yourself. A worker's "done" is a claim. The timebox orders the work. It never waives a playbook step. How, Architect, investigation, and the implementation delegate stay in the work. At the limit, the worker reports what remains. The coordinator checks that the report names what remains. An unexplained skip in the report's deviations is a send-back. A skip cited to the timebox is a send-back. `t3_thread_read` on the worker thread returns its `worktreePath` and branch. Run `$B dish <id> --state in-review --sha <head>`. Spawn one verifier with `delegate_task`, `mode: "async"`, from the `verifiers` role on a model family other than the author's. Its read-only brief: the tickets, the menu, the worktree path and the diff at that SHA, two questions (does it work on the real surface, and does it serve the menu without scope the tickets did not ask for), and "write your findings to `<restaurant dir>/reports/<dish>-review.md`". Record the verdict with `$B pass record`.
7. **Act on the verdict.**
- `pass`: when the dish depends on another coordinator's item that has not landed and `$B status` prints `reports to <thread>`, never hold it outside the queue. First open a contest per [Contests](#reporting-to-an-executive-admin) and send the `contest` line. The contest holds this dish's entry until the admin orders the two. Then submit as below. Write the PR title and body (what changed, the measured effect, how it was verified) to `<restaurant dir>/prs/<dish>.md`, then `$L submit --holder <restaurant>/<dish> --branch <b> --sha <head> --lease L<n> --reviewer <provider/model> --title "..." --body-file <restaurant dir>/prs/<dish>.md`, then `$B dish <id> --state queued`, then `$L land`. When it lands, `$B dish <id> --state merged`. In `merge` and `human` mode it opens a PR first. Record it with `$B dish <id> --pr <url>`, link it with `link_pull_request`, and call `watch_pull_request` on it per [Pull request watching](../pstack-runtime/SKILL.md#pull-request-watching). This head chef thread owns the PR. On each wake, run `$L land`. Keep the watch while the dish is queued or awaiting merge. A reply, or a wake with no reply, keeps the watch. Call `unwatch_pull_request` only when this thread stops driving that PR. Driving stops when the dish is dropped, when its PR closes, when the queue bounces it and leaves the PR open, or when the restaurant closes. If this thread is settled, call `t3_thread_organize` with `action: "unsettle"` before the next `watch_pull_request`. This thread is pinned, so a merge does not auto-settle it. Mark the dish merged when a later `land` reports it landed.
- Bounced by the queue: the lease is active again. Call `unwatch_pull_request` on the PR the bounce left open. Finish with the old worker per [Git and PR housekeeping](#git-and-pr-housekeeping). Then run `$B dish <id> --state in-progress`, because `brief` refuses a queued dish. That command clears the recorded worker thread. Launch a fresh worker thread with `$B brief` rerun, `--context` naming the bounce reason, and current trunk. Record it with `$B dish <id> --thread <new thread id>`. A conflict or a changed rebase needs a new review.
- `send-back`: finish with the old worker per [Git and PR housekeeping](#git-and-pr-housekeeping). Then `$B dish <id> --state in-progress`, which clears the recorded worker thread. Rerun `$B brief` (it adds the verifier's findings file). Every send-back launches a fresh worker with `t3_thread_launch`, on a new branch from the old branch's head, however small the fix. Record it with `$B dish <id> --thread <new thread id>`. Never call `t3_thread_send` on the old worker.
- Drop a dish in three steps. A requested interrupt is not a stop. `t3_thread_interrupt` can return `status: "interrupt_requested"` while the worker's run goes on.
1. Call `t3_thread_interrupt` on its worker thread.
2. Call `t3_thread_wait` on the run id it returned, or on the thread when it returned none. When the wait returns `timedOut: true`, leave the dish in its state. `watch` keeps its lease renewing. Wait again on the next service.
3. When the wait reports a terminal state, run `$B dish <id> --state dropped --stopped <run id>`, or `--stopped idle` when the wait reported an idle thread. That releases its lease.
- After a dish merges or is dropped, clean up per [Git and PR housekeeping](#git-and-pr-housekeeping). A send-back or a bounce already finished with the old worker above. After a send-back, delete the old branch per that section once the fresh worker is launched.
- `blocked`: `86 add` if only the user can unblock it. Otherwise fix the environment and run the pass again.
8. **Fix the recipe.** When two dishes repeat the same mistake, fire a dish that runs the **correct** skill to make it impossible (lint, type, test, or skill). Run the **reflect** skill over this restaurant's threads once a week.
9. **Report.** When `$B status` prints `reports to <thread>`, this restaurant reports through the executive admin, and this paragraph replaces the rest of this step, levels included. Send each event line per [Reporting to an executive admin](#reporting-to-an-executive-admin). A send-back, a merge, or a failure is an event line to the admin and not a reply. For the scheduled evening report, run `$B close --to-file` and send the `report` line with that path. When the batch drains, as defined below, run `$B close --to-file` and send the `drained` line. Neither is a reply to the user. Reply to the user only in a service the user started, or when a send to the admin fails. Every other wake ends the turn with no assistant text at all, whatever this restaurant's level. That covers a schedule, a liveness check, a worker's report-back, a verifier's completion, a pull-request wake, and a request from the admin. The `milestones` allowance of a single line when the host requires text does not apply. T3 requires no closing text, and a one-line status is still a reply the user reads. A failed send of a line with a store row gets a reply at this restaurant's own level, per the rest of this step. A failed send of a message-only line gets a reply at every level, because nothing else carries it. Without a `reports to` line, read `reporting` from `restaurant.json`. A missing field means `milestones`. Send the scheduled evening report at every level. The 09:00 morning service is not a report. A batch has drained when `$B status` omits `in progress`, `in review`, `passed review`, and `waiting to land`. Status omits a count of zero, so a missing label is a count of zero. `waiting to land` is work queued to land, including a pull request that awaits merge. Send the drain summary on the wake that first finds the batch drained. A later wake that still sees the batch drained does not send that summary again. A service the user started is a turn begun by a user message. A worker report, a verifier completion, a schedule, or a pull-request wake is not a service the user started. A message from the user gets at least a one-line acknowledgment at every level. A direct question gets an answer. Each level below adds the replies it names. It does not drop the evening report, the user message, or the direct question.
- `every-turn`. Send a short reply after every wake. Run `$B close` on the scheduled evening report, a reply that raises a decision for the user, the end of a service the user started, and closing the restaurant.
- `milestones`. Reply when work merges, when a review sends work back or blocks it, when a decision needs the user, when something fails or the queue pauses, or when the user sends a message. A routine wake ends with no reply, or with a single line when the host requires text. A liveness check with nothing new, a liveness check while a review is pending, a review starting, and a worker launching are routine wakes. Run `$B close` on the scheduled evening report, a reply that raises a decision for the user, the end of a service the user started, and closing the restaurant.
- `digest`. Reply only on these five wakes:
1. A decision the user must make. A blocked verdict that needs the user is a decision.
2. A failure, or a paused queue, that this thread cannot fix itself. A send-back is not a failure. A queue bounce this thread hands to a fresh worker is not a failure.
3. One summary when a batch drains.
4. The scheduled evening report.
5. A message from the user.
Every other wake ends the turn with no reply text at all. Those wakes include a worker's report-back, a verifier's completion, a send-back, a queue bounce, a worker launching or being replaced, a timebox raised, a review starting, a merge that does not drain the batch, a `watch_pull_request` wake, a landing drain run that neither drains the batch nor pauses the queue, a liveness check, intake with nothing new or only dropped tickets, a fix dish fired by step 8 or the liveness check, logging a hang for a run left open after its report-back, and creating, recreating, or deleting a schedule. A status line is still a reply. Do not send one on those wakes, even a single line. Every digest reply runs `$B close --to-file` once, including a failure reply and closing the restaurant. Write each reply per Digest messages below.
Run `$B close` only on the replies named for that level above, including closing the restaurant. At `digest` that command is `$B close --to-file`. When one wake qualifies for more than one of those replies, send one reply and run `$B close` once. Close runs once per reply. On a reply that runs `$B close`, answer a direct question first. That answer sits outside the three-sentence cap. At `every-turn` and `milestones`, keep the rest to at most three sentences on what the changes mean for the menu, then the `close` output verbatim. It lists each ticket and dish once, under its latest state since the last report, with PR links. Write no other report file. At those two levels every other reply is plain and does not run `$B close`.
### Digest messages
At `digest`, write every reply for a person who has not followed the work. Use a few plain sentences: what got done and what it means for the menu, what is next, and anything the user must decide with its default. Describe each change by what it does for the user, not by how it was built.
- Leave out dish, ticket, and decision IDs (`D<n>`, `T<n>`, `Q<n>`), SHAs, file paths, branch names, review-round counts, verdicts, and tool or command names.
- Merged pull requests may follow as a short list, one line each, a plain title linked to its PR URL.
- `$B close --to-file` writes the full report under `<restaurant dir>/closeouts/` and prints its path. End the message with one line that names that path, such as "Full report: <path>". That line is the only path in the message. Never paste the `close` output.
- Put commits, IDs, and other engineering detail in the store, not the message. A paused queue's two commits, which the [landing skill](../landing/SKILL.md#when-the-queue-pauses) asks for, go in the `$B 86 add --question` text, and the full report carries them.
A drain summary reads like this:
> Startup is about twice as fast, and the settings page no longer freezes when it opens. Both changes are merged. Next I start on the slow search results you reported this morning. Nothing needs your decision.
>
> - [Faster app startup](https://github.com/acme/app/pull/41)
> - [Fix the settings page freeze](https://github.com/acme/app/pull/42)
>
> Full report: ~/.local/state/pstack-t3/brigade/app/perf/closeouts/2026-10-06T230012.481207.md
## Git and PR housekeeping
The head chef owns every git and PR chore its work creates. In `merge` and `push` mode the user has no step. In `human` mode the user merges each PR. In `local` mode the user merges `refs/landing/<trunk>` into a branch.
- Write each PR's title and body through `submit`, and link every PR with `link_pull_request`.
- Keep the landing drain schedule while anything is queued or awaiting merge, per the [landing skill](../landing/SKILL.md#keep-the-queue-moving). After the queue opens a PR, also watch it as that section says. Keep that watch across reports while the entry is queued or awaiting merge. When you delete the schedule, run `$B set --schedule drain=`. Call `unwatch_pull_request` only when this thread stops driving that PR.
- Finish with the old worker when a dish merges, is dropped, is sent back, or bounces. The old worker is the thread the dish row names before any `$B dish <id> --thread` records a replacement. Name that thread id in every call below, so the replacement's open run is never touched. Read it with `t3_thread_read`. If its run is still open after the report-back, call `t3_thread_interrupt`, then `$B hang <id> --provider <provider> --minutes <n>` with the provider and the minutes that read showed, then archive it with `t3_thread_organize`, `action: "archive"`, and that thread id. A worker that an earlier attempt launched counts too. Check each attempt's thread, and for a run a replaced attempt left open, add `--attempt <n>`, counting attempts from 1 in launch order. If the run has already closed, only archive it. On a send-back or a bounce, finish before `$B dish <id> --state in-progress`. Entering in progress starts a new attempt, and `hang` then refuses with `brigade: no report-back on this attempt`. At `digest` reading, interrupting, logging a hang, and archiving send no reply of their own. A merge that drains the batch still sends the drain summary.
- After a dish merges, is dropped, or is sent back, remove the old worker's worktree, delete its branch locally and on the remote, and delete the queue's `landing/e<n>` branch, including a leftover `landing/q<n>`, once its PR merged or closed. After a send-back, delete the old branch only once the fresh worker is launched, because its branch starts from the old head.
- After a landing, when the user's checkout at the project root is clean and on trunk, fast-forward it with `git merge --ff-only`. Otherwise leave it alone.
- A bounce or a conflict is a dish for a fresh worker, never a manual rebase.
- Never force-push trunk, rewrite published history, change branch protection, or delete a branch this restaurant did not create.
## Liveness check
Run on the liveness schedule, and at the start of any service while work is in progress.
1. `$B watch`. It prints one line per dish in progress, one line for each other dish not `merged` or `dropped`, one line per ticket moved to a sibling that has not filed it yet, and `handed to you: <n>; run ticket take` while siblings' handoffs wait in this restaurant's inbox. On `handed to you`, run `$B ticket take`. On `not delivered`, run the `ticket move` command the line prints, then message the sibling per Run a service step 2. `waiting for ticket take` needs nothing until the sibling's next service. For a waiting ticket whose latest log row is `blocked`, it prints `waiting for a worker (<running> of <cap> running)`, `waiting on L<n> (<holder>)`, or `waiting for room in the repository (<n> of <n> changes in flight)`, the first that holds now. An `unblocked` line names a shell-quoted `fire` command. Run it as printed. That is the `fire` in steps 3 and 4 of Run a service for those tickets. When `$B status` prints `reports to <thread>` and a ticket's first `blocked` row in `log.tsv` is more than an hour old, send the `blocked` line per [Reporting to an executive admin](#reporting-to-an-executive-admin).
`watch` renews the lease of every dish in progress, in review, passed, sent back, or parked, and prints nothing for a live one. It updates `lastActivityAt` after a pass that renewed every live lease, so `walk` marks a restaurant whose leases stopped renewing. A submitted lease is the queue's. Act on these lines.
- `D3: lease L4 expired; stop its worker, then run lease renew L4`. Call `t3_thread_interrupt` on the worker thread, then `t3_thread_wait` on the run id it returned. When the wait returns `timedOut: true`, leave the lease expired and the dish as it is, run no `lease renew`, and wait again on the next service. Only after the wait reports a terminal state, run `$L lease renew L4 --owner <restaurant>/@<generation>`. It admits the lease again, or refuses with the overlap or cap message. On a refusal, park the dish with `86 add` when only the user can unblock it, and run the renew again on a later service.
- `D3: lease L4 was released; claim again before submitting`. Claim the paths with `$L lease claim` and record it with `$B dish D3 --lease <id> --paths <paths>` before any submit.
- `D3: could not renew L4: <error>`. The landing store did not answer. Fix that cause. `lastActivityAt` does not move until a pass renews every live lease.
- `D2: in review`, `D3: passed, not submitted`, `D4: parked`, and `D5: sent back` keep the liveness schedule alive. Act on them per Run a service steps 6 and 7.
- `D1: in progress with no worker thread; launch a fresh worker`. Launch a fresh worker with `t3_thread_launch` and record it with `$B dish <id> --thread <new thread id>`. Never call `t3_thread_send` on an earlier attempt's thread.
- For a passed or queued dish, `watch` reads `land.py status --holder <restaurant>/<dish> --sha <dish sha>` and takes the highest entry at exactly the dish's SHA. `D1: landed as E1 (<commit>); mark it merged`: run `$B dish D1 --state merged`. `D2: E3 awaiting merge <pr url>; watch that PR`: call `watch_pull_request` on that PR, because this thread owns it even when a sibling's `land` opened it. `D4: E4 bounced: <reason>`: follow the bounce step in Run a service step 7. `D5: E6 already submitted; mark it queued`: run `$B dish D5 --state queued`. That recovers a crash between `submit` and `dish --state queued`. `D6: E7 queued` needs nothing until the queue moves.
2. "report written, no report-back" is a defect. The report file exists and the dish is not marked reported. Read the worker thread with `t3_thread_read` and `view` set to "activity". Find why the message never arrived. The brief lacked the send step, the worker skipped it, `t3_thread_send` failed or went to the wrong thread, or the timebox ran out. Fire a fix at that cause, in the brief template, the skill text the worker followed, or `brigade.py`, using the **correct** skill.
3. A "report written" line that does not say "no report-back", and a line `D<n>: reported Nm ago, not in review; read the thread (thread <id>)`, mean the worker reported back and its review has not started. Read the worker thread with `t3_thread_read`. If its run is still open, call `t3_thread_interrupt`, then `$B hang <id> --provider <provider> --minutes <n>` with the provider and the minutes that read showed. Then review per Run a service step 6. At `digest` this is a routine wake with no reply.
4. "over its timebox": `t3_thread_read` the thread with `view: "activity"` and `afterPosition`. When it made progress in the last 10 minutes, raise the timebox once with `$B dish <id> --timebox <m>`. Otherwise interrupt it and launch a fresh worker with a smaller scope, or park the dish with `86 add` when only the user can unblock it. Recording the fresh worker's thread with `$B dish <id> --thread <thread id>` starts its attempt while the dish stays in progress. The previous report and report-back mark belong to the worker you replaced. `$B watch` compares the report file to this new start.
5. "running": nothing new. Reply per Run a service step 9. When `$B status` prints `reports to <thread>`, end the turn with no assistant text at all, whatever the level. Otherwise, at `milestones` this is a routine wake, so end with no reply, or with a single line when the host requires text. At `digest`, end the turn with no reply text at all. At `every-turn`, send a short reply. A dish in `in-review` or `passed` is not this line.
6. When `$B watch` prints "no work in progress", delete the liveness schedule with `delete_scheduled_task`, then run `$B set --schedule liveness=`. Delete it only on that line. A blocked ticket is not that line, so the schedule stays while one is waiting. Do not treat that line as a drained batch. A batch has drained when step 9 says it has. The drain summary is sent on the wake that first finds the batch drained. A review still pending means the batch has not drained. Do not send the drain summary while a review is pending. Reply per step 9.
## Executive chef's view
`python3 <skills>/brigade/scripts/brigade.py walk` groups every restaurant under its repository. Each repository gets one header with its landing queue's status line, or `no landing contract`. Under it, each restaurant gets its reporting level and counts, with the waiting tickets whose block still holds counted as blocked. A second line names its thread ID and the unreleased leases its dishes hold. Open decisions follow. `walk --repo <root>` prints one repository. A restaurant idle past 24 hours is marked, so a stalled head chef shows. `$B status` prints the reporting level, the landing mode as `lands by <mode>` or `no landing contract`, then the counts. A `restaurant.json` with no `reporting` field reads as `milestones`.
## Close a restaurant
First, for every dish that still holds an active lease, run the three drop steps in Run a service step 7: `t3_thread_interrupt`, then `t3_thread_wait`, then `$B dish <id> --state dropped --stopped <run id>` only after a terminal state. `$B close` writes reports and releases nothing. When any wait returns `timedOut: true`, stop here. Keep every schedule, so `watch` keeps that lease renewing, and continue closing on a later service once every wait has reported a terminal state. Then call `unwatch_pull_request` on each PR this thread is still watching. Delete its schedules with `delete_scheduled_task`, then clear each recorded name with `$B set --schedule <name>=`. Run `$B close` one last time, or `$B close --to-file` at `digest`. That close is the closing reply step 9 names. Unpin the thread with `t3_thread_organize`. Leave the store. It is the record.
## Reporting to an executive admin
A restaurant reports through the repository's executive admin while `$B status` prints `reports to <thread>`. The admin forwards the user's words, files shared intake, and rules on conflicts between coordinators. It never directs this restaurant's own work. The user stays in charge, and can still write to this thread directly. In the lines below, `<restaurant>` is this restaurant's directory name.
**Events.** Send each line to the admin's thread with `t3_thread_send` and mode `"queue"`, so it never interrupts a turn in progress. Send every event, whatever this restaurant's reporting level. The table's last column says which lines also leave a row in this restaurant's `log.tsv`. The admin's `sync` relays those rows, so a lost `merged`, `sent-back`, `decision`, `blocked`, or `misrouted` line only delays it. `failed`, `drained`, `report`, `reply`, `contest`, and `appeal` are message-only. No store row carries them, `sync` never prints them, and the admin learns of one only from the message. When a send fails, reply to the user about that event per Run a service step 9: at this restaurant's own level for a line with a store row, and at every level for a message-only line. A send to an admin thread that no longer exists is a failed send, so the user still hears every event.
| Line | Send when | Store row |
| --- | --- | --- |
| `merged <restaurant>: <title> [<pr url>]` | work lands. `push` and `local` modes have no PR URL | `dish` `merged` |
| `sent-back <restaurant> D<n>: <one line>` | a review sends work back or blocks it | `dish` `sent-back` or `blocked` |
| `decision <restaurant> Q<n>: <question> Options: <options>. Default: <default>.` | it parks a decision for the user | `decision` `open` |
| `failed <restaurant>: <one line>` | a failure or a paused queue it cannot fix itself | none |
| `drained <restaurant>: <closeout path>` | its batch drains. Run `$B close --to-file` for the path | none |
| `report <restaurant>: <closeout path>` | its evening report is written with `$B close --to-file` | none |
| `blocked <restaurant> T<n>: waiting on L<n> held by <holder>[ and L<n> held by <holder>] since <time>` | a ticket has been blocked for more than an hour. Name every lease it waits on, joined with `and`. `<time>` is its first `blocked` row in `log.tsv` | `ticket` `blocked` |
| `misrouted <restaurant> T<n>: <why>` | a routed ticket is off the menu, after `$B ticket move T<n> --to .admin` | `ticket` `moved` |
| `reply <restaurant>: <one line>` | it answers a `from-user` request, including a decline and its reason | none |
| `contest <restaurant> C<n> D<n>: <one line>` | its item and another coordinator's would conflict in the queue, or its passed item depends on another coordinator's unlanded item, after it opened contest `C<n>`. Name which depends on which | none |
| `appeal <restaurant> R<n>: <why>` | it disagrees with a ruling, after it complied, or instead of complying when compliance would be irreversible | none |
Send `blocked` from the Liveness check, once per ticket, with `clientRequestId` `blocked:<store path>/T<n>`, such as `blocked:app/docs/T4`, where the store path is the last two parts of `<restaurant dir>`, so a later check that sends it again delivers nothing new.
**Requests.** The admin publishes each request into this restaurant's `inbox/` and wakes this thread. `$B inbox take` prints it as `A<n>: <line>`. Act on it, then run `$B inbox done A<n>`. A crash between the two replays the request, so key every action by its id.
| Line | Do |
| --- | --- |
| `from-user <restaurant>: <the user's words>` | Treat it as a message from the user. Work it asks for becomes `$B ticket add --summary "..." --request A<n>`, which refuses a second ticket for that id. Answer with the `reply` line. |
| `answer <restaurant> Q<n>: <answer>` | `$B 86 answer Q<n> --answer "<answer>"`, then act on it. |
| `reports-to <restaurant> <thread>` | `$B set --reports-to <thread>`, or `$B set --reports-to ""` when `<thread>` is `none`. On a new thread, create the hourly service schedule from First service step 3 when `restaurant.json` records no `intake` schedule. On `none`, delete that schedule if the menu names no intake source. |
| `ruling <restaurant> R<n>: <decision>` | Read the ruling's state with `python3 <skills>/brigade/scripts/brigade.py --at <restaurant dir>/../.admin rule list`. When `R<n>` is not `in-force`, do nothing. Otherwise carry out this restaurant's side. |
The `ticket <restaurant>: run ticket take` line from the admin is a routed ticket, and `inbox take` files it. Take it per Run a service step 2. When it is off the menu, move it back with `$B ticket move <id> --to .admin` and send `misrouted`.
**A ruling's side.** A coordinator complies with every ruling, and may appeal.
- Contested paths. A reservation on the paths refuses every other holder's claim. The winner fires when `watch` prints `unblocked`. The other side starts no new work on those paths.
- Ownership. When the ruling gives this restaurant's ticket to another coordinator, run `$B ticket move <id> --to <winner>` and send the `ticket` line per Run a service step 2.
- Shares. `fire` refuses a claim past this restaurant's share. Nothing else changes.
- Queue order. `land` holds the second holder's entries until the first lands. Nothing else changes.
When this restaurant disagrees, it complies first, then sends `appeal`. When compliance would be irreversible, such as dropping work or deleting a branch, it holds and sends `appeal`, and the admin escalates.
**Contests.** When this restaurant's item and another coordinator's would break or conflict with each other in the queue, or when this restaurant's passed item depends on another coordinator's item that has not landed, run `$L contest --holders <restaurant>/D<n>,<sibling>/D<n> --owner <restaurant>/@<generation>`, such as `--holders docs/D4,core/D2 --owner docs/@1`. Each holder is the one `submit` takes: the restaurant's directory name, a slash, and the dish id. Read the other holder from `$L lease list`. It prints `C<n>`, and `land` holds both holders' entries until the admin settles it. Then send the `contest` line, naming which item depends on which. A contest also holds entries submitted after it, so open it before the dependent dish's `submit` and `land`, then submit per Run a service step 7. The admin then rules `dependency`. The coordinator opens the contest because it knows the dependency. The admin opens one only to carry out a `queue-order` ruling no contest covers yet. A refusal that says `there is nothing left to order` means one side already landed or is landing. Send nothing.
**What the user hears.** While `reportsTo` is set, reply to the user only in a service the user started. Every other wake ends with no assistant text at all, not even a one-line status, as Run a service step 9 says. This restaurant's reporting level decides only the reply for a failed send of a line with a store row. A failed send of a message-only line gets a reply at every level. `$B close --to-file` still writes each report into this restaurant's store. Once `reportsTo` is cleared, reply at this restaurant's own level again.
## Executive admin
The executive admin is one thread that works for the user on one repository. As an assistant, it keeps the user up to date across every coordinator there, forwards requests, files shared intake once, and writes one plain-language update. As a coordinator of coordinators, it settles conflicts between them on its own authority, by the rules in [Rulings](#rulings), and logs every ruling so the user can review and overrule it. It escalates only what the rules cannot settle. The scripts enforce. The cap stays in `land.py`, paths stay exclusive through leases, and each ruling is carried out by a script. The user sets the rules' inputs, answers every decision, and has the last word. Each coordinator keeps its own work, reviews, queue entries, and `menu.md`. Without an admin, every coordinator works as the rest of this skill says.
Its store is `<store>/<project>/.admin/`, beside the coordinators it serves, with `role: "admin"` in `restaurant.json`. It is a sibling of each of them, so intake ownership, `ticket move`, and handoff ids work as for any sibling. An admin thread follows this section and Digest messages. Open a restaurant, First service, Run a service, Liveness check, and Git and PR housekeeping are for coordinators. Here `$B` is `python3 <skills>/brigade/scripts/brigade.py --at <store>/<project>/.admin --owner <thread>@<generation>`. `set --thread` needs no `--owner`, so drop it there. Opening step 4 runs before any thread is recorded, so it also drops `--owner`.
```bash
python3 <skills>/brigade/scripts/brigade.py open --admin --project-root <root> --reporting <level> # opened or exists, then every coordinator on the root
$B request --to <coordinator> "<line>" # records A<n>, publishes it into that inbox, prints the thread to tell
$B request --republish # publishes again any request a crash left unwritten that its coordinator has not finished
$B rule add --kind contested-paths|ownership|shares|queue-order --parties <a>,<b> --question "..." --rule purpose|priority|age|related-work|dependency|floor|user --decision "..." [--supersedes R<n>] # prints R<n>
$B rule overrule R<n> --decision "<the user's words>" # marks it overruled and records the user's ruling
$B rule set R<n> --state done|expired ; $B rule list [--state in-force]
$B sync # copies each coordinator's new log.tsv rows into this log, or prints nothing new
$B set --thread <new> --replace --expect <old> [--stopped <run id>|idle|gone] # compare-and-swap of the admin's thread
```
The admin store refuses `fire`, `brief`, `dish`, `pass`, and `watch` with `brigade: the executive admin routes work and never runs it`.
### Open an executive admin
The user asks any thread for an executive admin.
1. Ask the user for the reporting level with the host's question tool, unless the user already said it. Recommend `digest`, because the admin exists so the user hears less.
2. Run `python3 <skills>/brigade/scripts/brigade.py open --admin --project-root <root> --reporting <level>`. It prints `opened <dir>` or `exists <dir>`, then one block per coordinator on the root with its purpose. Only the caller that got `opened` launches a thread.
3. Write the user's priorities under `## Priorities` in its `menu.md`, highest first, with the user. Add which intake sources it should own. Without priorities, the rules fall back to purposes and age.
4. Move shared intake. Each coordinator that lists the shared source runs `set --intake <its other sources>`, or `set --intake ""` when that source was its only one. That is refused while it still has waiting or assigned tickets from the source, so it finishes them first or moves them to the admin with `ticket move <id> --to .admin`. Then run `python3 <skills>/brigade/scripts/brigade.py --at <store>/<project>/.admin set --intake <source>` with no `--owner`. The admin has no thread until step 5, so no owner token exists yet. After step 5 every write to the admin store needs one.
5. Launch the thread with `t3_thread_launch` in the repository's T3 project, with `workspaceStrategy: {"type": "root"}`, title `Executive admin`, and the message "Use the brigade skill. You are the executive admin for the store at `<store>/<project>/.admin`. Wait for the start message." Record it with `$B set --thread <id>`. Then send it "Run your first service." with `t3_thread_send` and mode `"auto"`.
When the user asks the admin to open a coordinator, it runs Open a restaurant, shows the user any overlap with the purposes `open` printed, and launches the new thread once the user agrees.
### Admin first service
1. `t3_thread_organize` with `action: "pin"` and no `threadId`.
2. Create three schedules with `schedule_task`, bound to this thread, and record each with `$B set --schedule <name>=<id>`. Each prompt says "Use the brigade skill. You are the executive admin for the store at `<dir>`. Reporting level: `<level>`. Run a service."
- Intake: `{"type": "interval", "everyMs": 3600000}`. Each service also runs the idle check, which must run well inside the 6-hour lease expiry.
- Morning service: `{"type": "fixed_time", "timeOfDay": "09:00"}`, for routing and checks.
- Evening update: `{"type": "fixed_time", "timeOfDay": "18:30"}`, after the coordinators' 18:00 reports. The prompt adds "Write the update."
It creates no liveness or drain schedule, because it runs no workers and owns no queue entries.
3. Send each coordinator `reports-to <coordinator> <this thread>` as a request, per Admin messages.
4. Run an admin service.
### Admin service
Every wake runs one service, whether a schedule, a message, or a user message.
1. Run `$B status`. It prints `thread <id>` first and `owner <thread>@<generation>` last. When it names another thread, or prints `thread not recorded` because the admin was retired, end the turn with no action. Otherwise pass that token as `--owner` on every `$B` command, and `--owner .admin/@<generation>` on every `$L` ruling write, which is `lease reserve`, `lease unreserve`, `share`, and `contest`. `$L lease list`, `$L status`, and `$L land` take no `--owner`, and the admin never runs `land`. A write refused with `is stale` means another thread owns the store. End the service at once and run no further command, including the ruling's `$L` enforcement.
2. Read `menu.md`, `$B status`, `$B 86 list`, and `python3 <skills>/brigade/scripts/brigade.py walk --repo <root> --stale-hours 3`. When the user's message answers a question `$B 86 list` prints, run `$B 86 answer Q<n> --answer "<the user's words>"` before anything else, then act on it per [Escalate](#rulings).
3. `$B inbox take`, for tickets moved back. Then file each piece of work the user asks for in this service as `$B ticket add --summary "<the user's request>" --source user`, one ticket per request. Route it per step 4 like any other ticket. A message that settles a conflict between coordinators, such as which one's item lands first, is a ruling per [The user's ruling](#rulings), not a ticket. Then intake from its owned sources with `$B ticket add --summary "..." --source <source> --ref <url>`. Then `$B request --republish`.
4. Route each waiting ticket to the coordinator whose purpose fits with `$B ticket move <id> --to <coordinator>`, then `t3_thread_send` to the thread it prints, with mode `"auto"`, the line `ticket <coordinator>: run ticket take`, and the handoff id `<project>/.admin/T<n>` as `clientRequestId`. A ticket that fits two purposes gets an ownership ruling. A ticket no purpose fits becomes `$B 86 add` with options and a default.
5. Read coordinator events from two sources. Run `$B sync`. It prints each coordinator's new store rows, which carry `merged`, `sent-back`, `decision`, `blocked`, and `misrouted`. The message-only lines, `failed`, `drained`, `report`, `reply`, `contest`, and `appeal`, never appear in `sync`. Act on each one from the message that woke this service. Rule on each conflict either source shows, such as a block older than an hour, a `contest` or `appeal` line, or waiting work that needs a changed share, per [Rulings](#rulings). Record each ruling with `$B rule add` before carrying it out. Then reconcile every ruling `$B rule list --state in-force` prints, mark the ones whose condition ended with `$B rule set`, and carry out the rest again, which repeats nothing. Pass each coordinator's `decision` to the user. Never answer it. When the wake message is a coordinator's `reply` line, pass it to the user, including a decline and its reason, per [What the user hears from the admin](#what-the-user-hears-from-the-admin). A malformed-line failure from `sync` goes to the user as a failure.
6. When the repository sits at its cap while tickets wait for an hour, park a decision proposing a new cap, with a default. A coordinator that `walk` marks idle while it holds leases becomes a decision for the user, before its 6-hour leases lapse.
7. Reply per [What the user hears from the admin](#what-the-user-hears-from-the-admin).
### Admin messages
Every exchange is an explicit `t3_thread_send`. The admin sends with mode `"auto"`, so the coordinator wakes. Coordinators send the event lines in [Reporting to an executive admin](#reporting-to-an-executive-admin) with mode `"queue"`. Messages are wakes, and the stores are the record for every line with a store row. The admin reads those events through `sync`, which reads each `log.tsv` past a cursor and relays each row once, so a second wake for one stored event finds nothing new. It reads a message-only line from the message itself and acts on it in that service, per service step 5.
Every admin line except a routed ticket is a request. Run `$B request --to <coordinator> "<line>"`. It prints `A<n> for <coordinator>; tell thread <id>`. When its output ends with `no thread recorded for <coordinator>` instead of `tell thread <id>`, send nothing. The request waits in that inbox. Otherwise call `t3_thread_send` to that thread with the line and `clientRequestId` `A<n>`, so a retried send delivers once. Retry a failed send with the same `A<n>`, or leave it to the next scheduled wake, because the file waits in the inbox. Never run `request` again to retry. Each run records a new id, so the coordinator would act twice.
| Line | Sent when |
| --- | --- |
| `ticket <coordinator>: run ticket take` | it routes a ticket, as in service step 4 |
| `from-user <coordinator>: <the user's words>` | the user's words are meant for that one coordinator: a question about its work, a mode change, or a disagreement with its verdict. Work the user asks for is a ticket per service step 3, and a conflict between coordinators the user settles is a ruling, never a `from-user` line. Quote the user's words |
| `answer <coordinator> Q<n>: <answer>` | the user answers that coordinator's decision |
| `reports-to <coordinator> <thread>` | its first service, recovery, or retirement with `none` |
| `ruling <coordinator> R<n>: <decision>` | it records a ruling. Send it to each coordinator involved |
### Rulings
The admin rules on four kinds of conflict between coordinators. It applies the rules in order, and the first rule that separates the parties decides. A ruling binds the coordinators involved until it is done, expires, is superseded, or the user overrules it.
The rules read three inputs. Each coordinator's `menu.md`, with its purpose and `## Off the menu`. The user's priorities, the ranked names under `## Priorities` in the admin's `menu.md`. A coordinator the list does not name ranks below every named one, level with the other unnamed ones. Only the user changes the list. Age, how long the waiting work has waited. A blocked ticket's age counts from its first `blocked` row. A coordinator's age is its oldest waiting ticket's. Queue order counts from the `contest` line.
| Kind | Question | Rules, in order |
| --- | --- | --- |
| `contested-paths` | Which coordinator claims a contested path next, including a hot shared file such as `README.md` | 1. A path one purpose names and the other's `## Off the menu` excludes goes to the first (`purpose`). 2. The user's priorities (`priority`). 3. The older waiting work (`age`). |
| `ownership` | Which coordinator owns a request that fits two purposes | 1. A coordinator whose `## Off the menu` excludes it loses (`purpose`). 2. The user's priorities (`priority`). 3. The coordinator whose open work already touches the request's paths or ref (`related-work`). 4. Otherwise escalate. |
| `shares` | How many of the repository's changes in flight each coordinator may hold, out of the cap | 1. Each coordinator with waiting work gets one, by priorities, then age, until the cap runs out (`floor`). 2. The rest go by priorities, highest first, up to each coordinator's waiting work (`priority`). 3. A tie goes to the older waiting work (`age`). |
| `queue-order` | Which of two coordinators' passed items lands first when one would break or conflict with the other | 1. An item the other depends on, as a `contest` line stated, lands first (`dependency`). 2. The user's priorities (`priority`). 3. The older `contest` side (`age`). |
Age decides only when the earlier rules leave the parties level. Different ages then decide by `age`. Equal ages tie at the age rule. When priorities rank one side higher, the ruling is `priority` even when the ages are equal, because priority comes before age. The admin escalates only when no rule separates the parties. So every ruling has one outcome or escalates.
A live lease is never taken away. A contested-path ruling decides who claims next when the lease frees, and keeps the holder from starting new work on those paths while the winner waits. A share counts changes in flight, as the cap does. Shrinking a share stops no running work.
**The user's ruling.** When the user settles a conflict between coordinators that no ruling covers, such as an order for two items, record it with `$B rule add --kind <kind> --parties <a>,<b> --question "..." --rule user --decision "<the user's words>"`. Then carry it out and send the `ruling` lines like any other ruling.
Carry out each ruling with its script, after `$B rule add` recorded it.
- `contested-paths`: `$L lease reserve --for <winner>/ --paths <paths> --ruling R<n> --owner .admin/@<generation>`. It prints `S<n>`. Every other holder's claim on those paths is refused until the winner claims them or the reservation expires. It arms, and starts its 2-hour clock, once no other holder's live lease overlaps it and both the cap and the winner's share have room for one more change. A reservation for a winner whose share is 0 stays waiting, so that winner also needs a shares ruling.
- `ownership`: `$B ticket move <id> --to <winner>` when the admin holds the ticket. Otherwise the `ruling` line asks the holder to move it.
- `shares`: `$L share --for <coordinator>/ <n> --owner .admin/@<generation>` for each coordinator. It refuses a share that would bring the sum over the current cap.
`$L cap` leaves existing shares as they are. A positive `$L cap N` refuses when the shares add up to more than N, and it writes nothing. When the user asks for a new cap, rule on shares again in the same service, in this order:
1. Record the new ruling with `$B rule add --kind shares ... --supersedes R<n>`, naming the old shares ruling.
2. For each share the ruling lowers, run `$L share --for <coordinator>/ <n> --owner .admin/@<generation>`. When that write is refused, clear that share with `$L share --for <coordinator>/ 0 --clear --owner .admin/@<generation>`.
3. Run `$L cap N`. When that command is refused, finish step 2. Then run `$L cap N` again.
4. For each share the ruling raises, and for each share step 2 cleared, run `$L share --for <coordinator>/ <n> --owner .admin/@<generation>`.
Do not run `$L cap 0` to get past a refusal.
- `queue-order`: when no contest covers the two holders, such as an order the user states, first run `$L contest --holders <first holder>,<second holder> --owner .admin/@<generation>`. It prints `C<n>`, and a rerun prints the same id. Then `$L contest --settle C<n> --first <holder> --owner .admin/@<generation>`. `land` then holds the other holder's entries until an entry of the first holder lands.
Then send the `ruling` line to each coordinator involved.
**States.** A ruling starts `in-force`. Mark it `done` with `$B rule set R<n> --state done` when its condition ends. A rerun of its `lease reserve` prints `S<n> was claimed in full`, the moved ticket appears in the winner's `rail.tsv`, the share is in place, or `$L status --holder <first holder>` shows a `landed` entry. Mark it `expired` when the rerun refuses with `S<n> for ruling R<n> expired`, or its entry left the queue unordered. Rule again on the next service if the conflict remains. Recording comes first, so a crash never leaves a ruling carried out but unlogged. A rerun of `lease reserve` with the same ruling, `ticket move`, `share`, and `contest --settle` gives the same result.
**Replacing a ruling.** A new ruling that changes an earlier one names it with `--supersedes R<n>`. Remove the old constraint before carrying out the new one, with `$L lease unreserve S<n> --owner .admin/@<generation>`, a new `contest --settle`, `$L contest --cancel C<n> --owner .admin/@<generation>`, or new shares.
**Overrule.** The user can overrule any ruling by describing it. Run `$B rule overrule R<n> --decision "<the user's words>"`, which records a ruling decided by `user` that supersedes it, then carry the new one out. An overrule changes what happens next. It cannot undo a lease already claimed or work already landed. Say so. When the overrule states a general preference, ask whether to add it to `## Priorities`, and add it only when the user says yes.
**Appeals.** An `appeal` that says compliance would be irreversible means the coordinator holds and has not complied. Escalate it with `$B 86 add` and act on it only after the user answers. On any other `appeal` line, recheck the ruling with the appeal's facts, such as a dependency it did not know. When the rules now decide differently, record a new ruling that supersedes the old one. Otherwise keep it, and list the appeal in the next update for the user.
**Escalate** only what the rules cannot settle. Park each with `$B 86 add --question "..." --options "..." --default "..."`. The default is the option that keeps things as they are, such as the current holder keeping the paths or the ticket staying with the admin. When no option does, such as two waiting parties, the default is the party whose name sorts first. `86 add` does not check for repeats, so first read `$B 86 list`, and when it already lists that conflict, park nothing new. When the user answers one of the admin's own questions, run `$B 86 answer Q<n> --answer "<the user's words>"` first. Act on that conflict only after that command. An answer to a coordinator's decision goes back as an `answer` line instead.
- A real priority call. No rule separates the parties. One coordinator lost three rulings in a row on the same paths, or its work has waited on rulings for more than 24 hours. An ordered entry has waited more than 24 hours on an item that keeps bouncing.
- A change to a purpose. The same two purposes collided in more than three rulings in a week, or a request fits no purpose. Propose new wording. Never edit a coordinator's `menu.md`.
- Anything irreversible. Dropping or closing another coordinator's work, deleting a branch, and changing the repository cap or the landing mode belong to the user. Run `$L cap <n>` and `$L mode <mode>` only when the user asks, with no `--owner`, because neither is a ruling write, and after `$L mode`, tell every coordinator with a `from-user` line.
### What the admin never does
- Decide what belongs to the user. It sets no cap, landing mode, priority, or purpose, and never answers a coordinator's decision.
- Direct a coordinator's own work. What a coordinator builds, how it reviews, and when it submits, absent a conflict, stay its own.
- Write code, or edit any file in a repository.
- Land work. It never runs `$L submit` or `$L land`, and never merges or deletes a branch.
- Override a review. It never records or changes a verdict, never asks a coordinator to submit work that did not pass, and never messages a worker.
- Write into a coordinator's store, except new files in its `inbox/` through `ticket move` and `request`.
- Claim, renew, or release a lease, or start work. It only reserves contested paths for a ruling's winner.
### Admin recovery and retirement
The store outlives its thread. Recovery is a user request, run from one thread. Every `brigade.py` write checks the owner token under the store lock, and every `land.py` ruling write checks `--owner .admin/@<generation>` against the floor `set --thread` raises. So a command the old run started either finished before the claim or is refused after it. The stop steps are a courtesy.
1. Claim the store with `$B set --thread recovering:<this thread> --replace --expect <old thread>`. Of two racing recoveries, one wins and the other exits 1. The output names the recorded schedule ids and `previousThread`. Run `$B status` again for the new owner token.
2. Delete each schedule with `delete_scheduled_task`, and clear each name with `$B set --schedule <name>=`.
3. When the old thread still exists, call `t3_thread_interrupt` on it. `status: "interrupt_requested"` means its run has not stopped yet.
4. Call `t3_thread_wait` on the run id it returned, or on the thread when it returned none.
5. When the wait returns `timedOut: true`, launch nothing. The store stays `recovering:<this thread>`. Tell the user. Recovery answers the user's request, and this thread is not the admin, so the admin's level does not apply. Wait again on the next turn on the thread in `previousThread`.
6. When the wait reports a terminal state, archive the old thread. Launch the new thread with the message from opening step 5. Run `$B set --thread <new> --replace --expect recovering:<this thread> --stopped <run id>`, `--stopped idle` for an idle thread, or `--stopped gone` when the old thread no longer exists. Without `--stopped` it exits 1 with `brigade: the old run has not been confirmed stopped; wait for it with t3_thread_wait, then pass --stopped <run id>`. Then send the start message. The new thread's first service sends every coordinator the `reports-to` line.
When the recovering thread is itself gone, a later recovery claims with `--expect recovering:<that thread>` and runs the same steps.
To retire the admin, route or drop its waiting tickets, then run `$B set --intake ""`. Delete its schedules and clear each name. Send each coordinator `reports-to <coordinator> none`, and send a last update. Unpin its thread. Last, run `$B set --thread "" --replace --expect <this thread>`. A later wake then finds `thread not recorded` at service step 1 and ends. A write the old thread still runs exits with `brigade: owner <this thread>@<old generation> is stale; this store is owned by @<new generation>`. A write with `--owner @<new generation>`, the token `status` prints as `owner @<new generation>`, exits with `brigade: this store has no recorded thread; it was retired, and only set --thread --expect restarts it`. Each coordinator takes a shared source back with `set --intake <source>` where the user wants it. Restarting a retired admin is recovery with `--expect ""` and `--stopped gone`.
### What the user hears from the admin
The user hears from one thread. The admin's level uses the same three values as a coordinator's.
- `every-turn`. Reply after each wake. Each message is a wake. A coordinator's routine wake sends the admin no line, so it is not one.
- `milestones`. Reply when any coordinator's work merges, when a review sends work back or blocks it, when the user has a decision to make, when a failure arrives, when the user sends a message, and for the evening update. Every other wake ends the turn with no reply text at all. Those wakes include a `blocked` line, a `reply` line, a `contest` or `appeal` line, a ruling, a `drained` or `report` line, and a scheduled service with nothing new.
- `digest`. Reply for a decision, a failure no coordinator can fix itself, one summary when every coordinator has drained, and the evening update. Every coordinator has drained when no coordinator line in `walk` shows `in progress`, `in review`, `passed review`, or `waiting to land`.
A ruling is never a reply occasion on its own at `milestones` or `digest`. A message from the user gets at least a one-line acknowledgment at every level, and a direct question gets an answer. A coordinator's `reply` line answers something the user asked, so pass it on, including a decline and its reason. It arrives only as the message that woke the admin, and no store row keeps it, so this thread carries it until that reply. At `every-turn` it goes in that wake's reply. At `milestones` and `digest` it goes in the next reply that level sends, and in the evening update at the latest.
Every admin reply follows [Digest messages](#digest-messages) at every level. A few plain sentences say what changed for each purpose, what is next, and what the user must decide, each decision with its default. Name each coordinator by its purpose, not its store name. List the rulings made since the last update in plain words, each with the rule that decided it, and say the user can overrule any of them by describing it. End with one line naming the full update, which `$B close --to-file` writes. That file holds each ruling's id and each coordinator's newest report.