Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
eaa0614
tests: expect the Playbooks section to stop, a missing file from chec…
creedants Oct 11, 2026
7ed7c95
check: bound Playbooks, report a bad bullet, and name drift as review…
creedants Oct 11, 2026
44ef6f7
tests: an indented Playbooks note is not a finding
creedants Oct 11, 2026
9d0a095
check: skip an indented list line inside Playbooks
creedants Oct 11, 2026
3ec0147
brigade: add a kind cell and a goal cell to each ticket and work item…
creedants Oct 11, 2026
0101ffe
brigade: add the goal add, goal set, and goal list commands
creedants Oct 11, 2026
8302915
brigade: record a kind and an optional goal on a ticket
creedants Oct 11, 2026
0d69a60
brigade: give each new work item its lead ticket's kind and goal at fire
creedants Oct 11, 2026
6b1a107
brigade: print kinds and goals in ticket list, startable, status, and…
creedants Oct 11, 2026
cb862bb
changes: add the note for kinds and goals in the coordinator's store
creedants Oct 11, 2026
ae43765
brigade: say how to give a ticket no goal and how to give a taken tic…
creedants Oct 11, 2026
adab50d
brigade: correct eight statements about kinds and goals that said mor…
creedants Oct 11, 2026
ef5ec3e
brigade: refuse a kind or goal change on a waiting ticket that an ite…
creedants Oct 11, 2026
13bb3b5
brigade: correct four statements that the refusal on a ticket an item…
creedants Oct 11, 2026
fd198dc
brigade: pass the kind and goal reader the end of its warning, not a …
creedants Oct 11, 2026
61e6a28
brigade: keep a hand-edited kind, goal, and goal id across a rewrite
creedants Oct 11, 2026
991342c
brigade: print kind and goal warnings on every command that acts on them
creedants Oct 11, 2026
cf91994
brigade: write the lead ticket kind before the item row
creedants Oct 11, 2026
ba2de93
brigade: say an item in flight with no kind is absent from kinds
creedants Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ Start from the upstream file and change only what T3 changes. Upstream's rules,
- After opening or driving a PR, add the `link_pull_request` step.
3. Link the runtime by relative path (`../pstack-runtime/SKILL.md` from a SKILL.md, `../../pstack-runtime/SKILL.md` from a playbook) when the step needs it. Do not paste runtime content.
4. Keep tool names unprefixed (`delegate_task`, not `mcp__t3-code__delegate_task`).
5. Run `python3 scripts/build.py`. The build fails on Cursor leftovers (see `scripts/check.py`), broken links, bad frontmatter, or override drift.
5. Run `python3 scripts/build.py`. The build fails on Cursor leftovers (see `scripts/check.py`), broken links, bad frontmatter, a poteto-mode playbook bullet whose file does not exist, or override drift.
6. Run `python3 scripts/build.py --update-lock` only after reviewing that the override matches the current upstream file.

`python3 scripts/sync_upstream.py --merge` syncs and, for each override whose upstream file changed, either writes a clean three-way merge of it or leaves the override as it is and names it. A clean merge can carry a new Cursor mechanism in upstream's lines, so a merged override still needs the review in step 6 and the build's Cursor-leftover check, which `python3 scripts/build.py --skip-lock` reaches while the lock is stale.
`python3 scripts/sync_upstream.py --merge` syncs and, for each override whose upstream file changed, either writes a clean three-way merge of it or leaves the override as it is and names it. Add `--conflicts <dir>` to also write git's conflict-marked merge of each override whose merge conflicts under `<dir>`, a path outside the checkout that does not exist yet. A clean merge can carry a new Cursor mechanism in upstream's lines, so a merged override still needs the review in step 6 and the build's Cursor-leftover check, which `python3 scripts/build.py --skip-lock` reaches while the lock is stale.

## Writing style

Expand Down
2 changes: 2 additions & 0 deletions changes/pstack-t3%2Fd160.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- `brigade.py` records a kind and an optional goal on each ticket and work item, and keeps the goals in a new table, `goals.tsv`. `ticket add --summary` needs `--kind`, one of `feature`, `fix`, `polish`, `upkeep`, and `study`, and takes `--goal G<n>` with the id of an open or paused goal. `ticket set --kind`, `--goal`, and `--no-goal` change a waiting ticket and are refused on a ticket in another state and on a waiting ticket that an item in flight names. `goal add` records an open goal with a name, a purpose, and a done test. `goal set` changes a goal, and a move to `done` or to `dropped` needs `--evidence` and is refused while the goal has a waiting ticket, an item in flight, or an assigned ticket in an item that is not under that goal. `goal list` prints each open or paused goal with its counts of waiting tickets, items in flight, merged items, and send-backs, then the counts of unassigned work. `fire` gives the new work item the goal of the first ticket in `--tickets` and that ticket's kind. When that ticket has no kind, the kind is `--kind`, or without it `fix` on station `bug-fix` or `perf-issue` and `study` on station `investigation`, and `fire` is refused on any other station. The first ticket records the kind the item took. A ticket after the first must have the first ticket's goal or no goal unless `--ride-along` names it, and a ticket named there must have priority low and must not be a feature. `ticket add --from-report` gives each ticket the goal of the report's item and, when that item has a kind, the kind `polish`, and it refuses `--kind` and `--goal`. `ticket move` hands the kind to the other coordinator and leaves the goal behind. `ticket list` and `startable` print a ticket's kind and goal after its priority. `status` prints a `kinds:` line and a `goals:` line with the counts of waiting tickets and items in flight under each, and an `untagged:` line with the number of waiting tickets that have no kind. A line with nothing to count is absent. `close` prints the kind on the lines of Merged, In progress, and New tickets, not started, and prints the lines of such a section under one heading per goal when one of them has a goal. A kind cell that holds a value other than one of the five kinds and a goal cell that holds an id with no row in `goals.tsv` read as empty, and `status`, `goal list`, `ticket list`, `startable`, and `close` print a warning for the cell on standard error. `fire` prints it for a ticket it names when it reaches its kind and goal check. `open` writes `goals.tsv` into a new store. A store written before this change reads with every ticket and item unassigned and with no kind. A command that appends a row to `rail.tsv` or `dishes.tsv` there writes the row with the two new cells, and a command that rewrites the table writes every row with them. A `brigade.py` from before this change then calls that table malformed. `activity.py` reads a work item row with or without the two cells and prints the same page for both.
- The prefixes `--no` and `--r` stop working, because `--no-goal` and `--ride-along` made them ambiguous, and the full flag names `--no-decision` and `--reason` work as before.
2 changes: 2 additions & 0 deletions changes/pstack-t3%2Fd169.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- `scripts/build.py` reports override drift as `<rel>: upstream changed since this override was written; review or re-port it, then --update-lock`. `scripts/check.py` reports a missing poteto-mode playbook file when `poteto-mode/SKILL.md` is a file, using `poteto-mode/SKILL.md: playbook "<title>" resolves to <file>, which does not exist`. `scripts/build.py` still fails on that finding before it swaps the tree, and it no longer calls `missing_playbooks` itself. `scripts/catalog.py` reads only the Playbooks section, from the line after the level-two heading `Playbooks` until the next level-two heading. A line in that section that starts with `- `, `* `, or `+ ` in column one, with no leading whitespace, and does not match `- **Title.** summary`, is a finding `poteto-mode/SKILL.md:<line>: playbook bullet does not match "- **Title.** summary": <line text>`. A line that matches `- **Title.** summary` still resolves to the backticked `playbooks/<name>.md` at the end of the bullet, with or without a `File: ` lead-in, or to `playbooks/<kebab title>.md`. `AGENTS.md` step 5 names a poteto-mode playbook bullet whose file does not exist. The `--merge` paragraph names `--conflicts <dir>`.
- An indented list line inside the Playbooks section is skipped. It is not a playbook entry and it is not a playbook finding.
78 changes: 71 additions & 7 deletions docs/cli/brigade.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Bookkeeping for brigade restaurants.
| `--at AT` | | | | restaurant directory (default $BRIGADE_DIR) |
| `--owner OWNER` | | | | \<thread>@\<generation> from status; every write in a store with a generation needs it |

`<command>` is one of `open`, `set`, `ticket`, `fire`, `brief`, `contract`, `watch`, `startable`, `hang`, `dish`, `pass`, `edits`, `86`, `inbox`, `request`, `rule`, `sync`, `status`, `close`, `walk`.
`<command>` is one of `open`, `set`, `ticket`, `goal`, `fire`, `brief`, `contract`, `watch`, `startable`, `hang`, `dish`, `pass`, `edits`, `86`, `inbox`, `request`, `rule`, `sync`, `status`, `close`, `walk`.

## brigade.py open

Expand Down Expand Up @@ -75,13 +75,13 @@ add, list, update, move, or take tickets on the rail
## brigade.py ticket add

```text
brigade.py [flags] ticket add (--summary SUMMARY | --from-report FILE) [--dry-run] [--source SOURCE] [--ref REF] [--request REQUEST] [--again] [--priority PRIORITY] [--paths PATHS] [--decision]
brigade.py [flags] ticket add (--summary SUMMARY | --from-report FILE) [--dry-run] [--source SOURCE] [--ref REF] [--request REQUEST] [--again] [--priority PRIORITY] [--paths PATHS] [--decision] [--kind KIND] [--goal G<n>]
```

| Argument | Required | Choices | Default | Help |
| --- | --- | --- | --- | --- |
| `--summary SUMMARY` | | | | |
| `--from-report FILE` | | | | an item report under reports/, such as reports/D2.md; files one waiting ticket per follow-up in it and changes only the ticket table, the log, and lastActivityAt |
| `--from-report FILE` | | | | an item report under reports/, such as reports/D2.md; files one waiting ticket per follow-up in it and changes only the ticket table, the log, and lastActivityAt; each ticket takes the goal of the report's item, and when that item has a kind the ticket takes fix at priority urgent or normal and polish at low; a follow-up is filed with no --priority, so it is low |
| `--dry-run` | | | | with --from-report: print what it would add; it changes no table and no log |
| `--source SOURCE` | | | `user` | |
| `--ref REF` | | | | |
Expand All @@ -90,6 +90,8 @@ brigade.py [flags] ticket add (--summary SUMMARY | --from-report FILE) [--dry-ru
| `--priority PRIORITY` | | `urgent`, `normal`, `low` | | missing reads as urgent from upstream, low from report, and normal from every other source |
| `--paths PATHS` | | | | comma-separated files and directories the work will touch |
| `--decision` | | | | the ticket asks the owner to decide; the startable command never lists it as startable |
| `--kind KIND` | | `feature`, `fix`, `polish`, `upkeep`, `study` | | the kind of work; a ticket added with --summary is refused without it |
| `--goal G<n>` | | | | with --summary: the goal this ticket belongs to, which must be open or paused; without it the ticket is unassigned |

Give exactly one of `--summary`, `--from-report`.

Expand All @@ -106,7 +108,7 @@ brigade.py [flags] ticket list [--state STATE]
## brigade.py ticket set

```text
brigade.py [flags] ticket set [--state STATE] [--priority PRIORITY] [--paths PATHS] [--decision] id
brigade.py [flags] ticket set [--state STATE] [--priority PRIORITY] [--paths PATHS] [--decision] [--kind KIND] [--goal G<n> | --no-goal] id
```

| Argument | Required | Choices | Default | Help |
Expand All @@ -116,6 +118,11 @@ brigade.py [flags] ticket set [--state STATE] [--priority PRIORITY] [--paths PAT
| `--priority PRIORITY` | | `urgent`, `normal`, `low` | | |
| `--paths PATHS` | | | | replaces the recorded paths; "" clears them |
| `--decision, --no-decision` | | | | mark or unmark the ticket as one that asks the owner to decide |
| `--kind KIND` | | `feature`, `fix`, `polish`, `upkeep`, `study` | | replaces the kind of a waiting ticket; refused on a ticket in another state, and on a waiting ticket that an item in flight names |
| `--goal G<n>` | | | | replaces the goal of a waiting ticket with one that is open or paused; refused on a ticket in another state, and on a waiting ticket that an item in flight names |
| `--no-goal` | | | | clears the goal of a waiting ticket, so it is unassigned; refused on a ticket in another state, and on a waiting ticket that an item in flight names |

Give at most one of `--goal`, `--no-goal`.

## brigade.py ticket move

Expand Down Expand Up @@ -150,17 +157,72 @@ record the tracked paths each waiting ticket's summary quotes, on tickets that r
| --- | --- | --- | --- | --- |
| `--dry-run` | | | | print what it would record; it changes no table |

## brigade.py goal

```text
brigade.py [flags] goal <action>
```

add, change, or list goals; a goal is a named line of work

`<action>` is one of `add`, `set`, `list`.

## brigade.py goal add

```text
brigade.py [flags] goal add --name NAME --purpose PURPOSE --done DONE [--share SHARE]
```

record a new open goal and print its id; refused when an open or paused goal has the same name, where case, leading and trailing whitespace, and the length of a whitespace run do not count

| Argument | Required | Choices | Default | Help |
| --- | --- | --- | --- | --- |
| `--name NAME` | yes | | | what the goal is called, such as "activity view" |
| `--purpose PURPOSE` | yes | | | one sentence that says why this line of work exists |
| `--done DONE` | yes | | | one sentence that states the test that shows the goal is reached |
| `--share SHARE` | | | | a whole number of 1 or more, recorded as the goal's share of the worker slots; goal list prints it, and it does not affect which tickets start |

## brigade.py goal set

```text
brigade.py [flags] goal set [--state STATE] [--evidence EVIDENCE] [--name NAME] [--purpose PURPOSE] [--done DONE] [--share SHARE] id
```

change a goal's state, name, purpose, done test, or share

| Argument | Required | Choices | Default | Help |
| --- | --- | --- | --- | --- |
| `id` | yes | | | |
| `--state STATE` | | `open`, `paused`, `done`, `dropped` | | paused is recorded and printed. It does not affect which tickets start. A move to done or to dropped needs --evidence, and is refused while a ticket of the goal is waiting, an item of the goal is in progress, in review, passed, sent back, blocked, or waiting to land, or an assigned ticket of the goal is not in an item of the goal. A done or dropped goal moves only to open. |
| `--evidence EVIDENCE` | | | | one line, stored with the time in the goal's closed cell when the goal moves to done or dropped; with --state done, what shows the done test is met; with --state dropped, why the goal is dropped; refused without --state done or --state dropped |
| `--name NAME` | | | | replaces the goal's name; refused when it is empty or only whitespace |
| `--purpose PURPOSE` | | | | replaces the goal's purpose; refused when it is empty or only whitespace |
| `--done DONE` | | | | replaces the goal's done test; refused when it is empty or only whitespace |
| `--share SHARE` | | | | a whole number of 1 or more, recorded as the goal's share of the worker slots; "" clears it; goal list prints it, and it does not affect which tickets start |

## brigade.py goal list

```text
brigade.py [flags] goal list [--all]
```

the state, name, counts, purpose, and done test of each open or paused goal and of each done or dropped goal that has a waiting ticket, an item in flight, or a ticket riding along, then the counts of unassigned work; waiting counts waiting tickets; in flight counts items in progress, in review, passed, sent back, blocked, or waiting to land; merged counts merged items; send-backs counts the send-back verdicts on the goal's items, in any state, that are not a panel member's row; riding along counts assigned tickets of the goal that are not in an item of the goal

| Argument | Required | Choices | Default | Help |
| --- | --- | --- | --- | --- |
| `--all` | | | | also print each other done or dropped goal |

## brigade.py fire

```text
brigade.py [flags] fire --tickets TICKETS --station STATION --summary SUMMARY [--task TASK] [--thread THREAD] [--branch BRANCH] [--timebox TIMEBOX] [--paths PATHS] [--mode MODE] [--reason REASON]
brigade.py [flags] fire --tickets TICKETS --station STATION --summary SUMMARY [--task TASK] [--thread THREAD] [--branch BRANCH] [--timebox TIMEBOX] [--paths PATHS] [--mode MODE] [--reason REASON] [--kind KIND] [--ride-along RIDE_ALONG]
```

group tickets into one dish and assign it to a station

| Argument | Required | Choices | Default | Help |
| --- | --- | --- | --- | --- |
| `--tickets TICKETS` | yes | | | comma-separated ticket ids |
| `--tickets TICKETS` | yes | | | comma-separated ticket ids; the first is the lead ticket; the item takes the lead ticket's goal, and its kind when it has one |
| `--station STATION` | yes | | | the pstack playbook the worker runs |
| `--summary SUMMARY` | yes | | | |
| `--task TASK` | | | | |
Expand All @@ -170,6 +232,8 @@ group tickets into one dish and assign it to a station
| `--paths PATHS` | | | | paths the dish will change; fire claims a landing lease on them first |
| `--mode MODE` | | | | full, to start the dish in full mode; needs --reason |
| `--reason REASON` | | | | one line: why the dish runs in full mode |
| `--kind KIND` | | `feature`, `fix`, `polish`, `upkeep`, `study` | | the item's kind when the lead ticket has none or --tickets names no ticket; a lead ticket with no kind records it too; refused when the lead ticket has another kind; with no --kind and no kind from the lead ticket, station bug-fix or perf-issue gives fix, station investigation gives study, and fire is refused on any other station |
| `--ride-along RIDE_ALONG` | | | | comma-separated ids from --tickets, not the first, that may be under another goal than the first ticket's; each must have priority low and must not be a feature; every other ticket after the first must have the first ticket's goal or no goal |

## brigade.py brief

Expand Down Expand Up @@ -489,7 +553,7 @@ executive admin: copy each coordinator's new log rows into this log
brigade.py [flags] status
```

the thread line first, then counts, then workers outside the executive admin's store, then reports to, mode, and owner when present
the thread line first, then counts, then workers outside the executive admin's store, then a kinds line, a goals line, and an untagged line when each has something to count, then reports to, mode, and owner when present

## brigade.py close

Expand Down
8 changes: 4 additions & 4 deletions scripts/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
t3/overrides.lock.json sha256 of each upstream file an override replaces

An override whose upstream file changed since it was written fails the build
until it is re-ported and the lock refreshed with --update-lock.
until it is reviewed or re-ported and the lock refreshed with --update-lock.
"""

import argparse
Expand Down Expand Up @@ -81,7 +81,7 @@ def check_lock(update):
if rel not in locked:
problems.append(f"{rel}: override not in overrides.lock.json (run build.py --update-lock after reviewing)")
elif locked[rel] != digest:
problems.append(f"{rel}: upstream changed since this override was written; re-port it, then --update-lock")
problems.append(f"{rel}: upstream changed since this override was written; review or re-port it, then --update-lock")
for rel in locked:
if rel not in current:
problems.append(f"{rel}: locked but no override exists (run --update-lock)")
Expand Down Expand Up @@ -125,9 +125,9 @@ def build(destination, update_lock=False, skip_lock=False):
render(staged)
sys.path.insert(0, str(ROOT / "scripts"))
from check import check_tree
from catalog import missing_playbooks, write
from catalog import write

findings = check_tree(staged) + missing_playbooks(staged)
findings = check_tree(staged)
if findings:
raise SystemExit("check failed:\n " + "\n ".join(findings))
replace_tree(staged, destination)
Expand Down
Loading
Loading