@nanocollective/roster 0.1.0-alpha.4 → 0.1.0-alpha.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +70 -84
  2. package/dist/cli.js +4562 -2744
  3. package/docs/README.md +9 -6
  4. package/docs/agents.md +24 -20
  5. package/docs/architecture.md +13 -5
  6. package/docs/charters/analyst.md +65 -0
  7. package/docs/charters/cmo.md +69 -0
  8. package/docs/charters/community.md +63 -0
  9. package/docs/charters/cto.md +71 -0
  10. package/docs/charters/designer.md +65 -0
  11. package/docs/charters/devops.md +65 -0
  12. package/docs/charters/pm.md +70 -0
  13. package/docs/charters/qa.md +65 -0
  14. package/docs/charters/support.md +60 -0
  15. package/docs/charters/writer.md +63 -0
  16. package/docs/commands.md +90 -5
  17. package/docs/concepts.md +48 -14
  18. package/docs/cost.md +36 -1
  19. package/docs/developing.md +16 -21
  20. package/docs/doctor-codes.md +8 -2
  21. package/docs/export.md +2 -0
  22. package/docs/extending.md +2 -2
  23. package/docs/getting-started.md +126 -77
  24. package/docs/images/brain.jpg +0 -0
  25. package/docs/images/org.jpg +0 -0
  26. package/docs/images/prompt.jpg +0 -0
  27. package/docs/images/setup-org.jpg +0 -0
  28. package/docs/images/setup-plan.jpg +0 -0
  29. package/docs/images/staff.jpg +0 -0
  30. package/docs/manual-steps.md +94 -123
  31. package/docs/memory.md +21 -3
  32. package/docs/org-yaml.md +39 -2
  33. package/docs/portal.md +119 -44
  34. package/docs/prompts.md +31 -4
  35. package/docs/security.md +37 -5
  36. package/docs/session-workflow.md +49 -17
  37. package/docs/staff-yaml.md +14 -2
  38. package/docs/troubleshooting.md +8 -8
  39. package/docs/upgrading.md +9 -3
  40. package/docs/writing-a-charter.md +28 -0
  41. package/package.json +18 -20
  42. package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +6 -0
  43. package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +6 -0
  44. package/templates/brain/CHARTER.md +3 -3
  45. package/templates/brain/README.md +1 -0
  46. package/templates/brain/log/decisions.md +3 -0
  47. package/templates/brain/strategy/ideas.md +7 -0
  48. package/templates/briefs/priorities.md +46 -0
  49. package/templates/ops/.github/workflows/session.yaml +108 -14
  50. package/templates/ops/agents.mjs +7 -3
  51. package/templates/ops/compose.mjs +16 -3
  52. package/templates/ops/inflight.mjs +157 -0
  53. package/templates/ops/org/operating.md +21 -1
  54. package/templates/ops/org/voice.md +9 -0
  55. package/templates/ops/prompts/_inflight.md +14 -0
  56. package/templates/ops/prompts/_paths.md +2 -1
  57. package/templates/ops/prompts/daily.md +16 -7
  58. package/templates/ops/prompts/mention.md +2 -0
  59. package/templates/ops/run-record.mjs +144 -0
  60. package/templates/portal/css/base.css +146 -73
  61. package/templates/portal/css/brain.css +23 -20
  62. package/templates/portal/css/diff.css +10 -9
  63. package/templates/portal/css/graph.css +12 -7
  64. package/templates/portal/css/health.css +13 -11
  65. package/templates/portal/css/inbox.css +57 -25
  66. package/templates/portal/css/layout.css +96 -41
  67. package/templates/portal/css/markdown.css +36 -14
  68. package/templates/portal/css/runs.css +13 -0
  69. package/templates/portal/css/setup.css +116 -34
  70. package/templates/portal/index.html +18 -2
  71. package/templates/portal/js/api.js +41 -4
  72. package/templates/portal/js/app.js +132 -9
  73. package/templates/portal/js/dialog.js +82 -0
  74. package/templates/portal/js/icons.js +37 -0
  75. package/templates/portal/js/inflight.js +18 -0
  76. package/templates/portal/js/md.js +5 -2
  77. package/templates/portal/js/mdedit.js +84 -0
  78. package/templates/portal/js/readiness.js +35 -0
  79. package/templates/portal/js/refresh.js +2 -1
  80. package/templates/portal/js/state.js +17 -4
  81. package/templates/portal/js/views/app.js +24 -7
  82. package/templates/portal/js/views/checklist.js +10 -4
  83. package/templates/portal/js/views/credential.js +84 -0
  84. package/templates/portal/js/views/graph.js +1 -1
  85. package/templates/portal/js/views/health.js +17 -4
  86. package/templates/portal/js/views/hire.js +583 -0
  87. package/templates/portal/js/views/inbox.js +264 -84
  88. package/templates/portal/js/views/org.js +46 -106
  89. package/templates/portal/js/views/orgedit.js +234 -0
  90. package/templates/portal/js/views/paste.js +87 -21
  91. package/templates/portal/js/views/prompt.js +11 -4
  92. package/templates/portal/js/views/repos.js +20 -15
  93. package/templates/portal/js/views/runonce.js +94 -0
  94. package/templates/portal/js/views/runs.js +170 -0
  95. package/templates/portal/js/views/setup.js +257 -75
  96. package/templates/portal/js/views/staff.js +158 -243
  97. package/templates/portal/js/views/todo.js +62 -0
package/docs/portal.md CHANGED
@@ -18,10 +18,12 @@ Keep the repos checked out beside each other, in the same shape the runner uses.
18
18
 
19
19
  ## The sidebar
20
20
 
21
- **The counts are right on load.** The badges beside Inbox and Pending work used to fill in only
22
- once something caused a render with an inbox already loaded, which in practice meant "after you
23
- visit the Inbox". A sidebar that says nothing until you look at it is not a sidebar. The page
24
- now asks once at boot, and the Inbox screen shares that request rather than making a second one.
21
+ Anything that asks before it acts (a merge, a push, a hire, a retire, a paid run) asks in the
22
+ page's own dialog, never the browser's `confirm()`, which blocks the whole tab.
23
+
24
+ **The counts are right on load.** The badges beside Inbox and Pending work are fetched once at
25
+ boot, and the Inbox screen shares that request rather than making a second one. A sidebar that
26
+ says nothing until you look at it is not a sidebar.
25
27
 
26
28
  While the first answer is outstanding the badge is a placeholder rather than blank, because an
27
29
  empty badge reads as zero and zero is a different claim from "still counting". The screens
@@ -50,26 +52,44 @@ It asks GitHub which of two things this is:
50
52
  nothing is written until you apply. Same `initFiles` the CLI runs, so the browser and the
51
53
  terminal cannot disagree about what a new tenant contains.
52
54
 
53
- Then: the Actions setting, deep-linked to the exact page with the failure it causes if skipped;
54
- which repos the staff work in, as a picker over what your `gh` can see minus what `org.yaml`
55
- already has; and a prompt for writing `org/business.md`.
55
+ Then what is left. **The Actions setting**, read on arrival: if it is not set, *Set it for me*
56
+ asks GitHub to set it, and a refusal comes back with GitHub's reason and a link to the page to
57
+ click. **The agent credential**: a paste box, a line on where your agent's credential comes from,
58
+ and where it will be stored (one org secret shared with the brains, or each brain where an org
59
+ secret would not arrive, with the reason). The value goes to the local server in a POST and from
60
+ there to `gh` on standard input; it is never written to disk or echoed back. Until somebody is
61
+ hired the box says so, because nothing would read it. Then which repos the staff work in, as a
62
+ picker over what your `gh` can see minus what `org.yaml` already has, recorded with the
63
+ visibility GitHub reports. Then the two files only you can write: a prompt for writing
64
+ `org/business.md`, and `org/priorities.md` opened in place, stub and all, to replace with what
65
+ matters this month. Each says whether it is written yet.
56
66
 
57
67
  Nothing here stores which step you are on. Setup takes days rather than minutes: an App has to be
58
68
  installed, a credential set, a first run finished. So the page derives its state from `roster
59
- doctor` every time it is drawn. A stored step counter would disagree with the world within an hour.
69
+ doctor` every time it is drawn, and runs the check again after anything on it is saved. A stored
70
+ step counter would disagree with the world within an hour.
71
+
72
+ ### Getting started
73
+
74
+ **Once the org exists, the same steps stay in the sidebar as *Getting started*** for as long as
75
+ anything is left: nobody hired, or `org/business.md` or `org/priorities.md` still the stub. An
76
+ org nobody has been hired into opens on it rather than on an empty Inbox, with *Hire your first
77
+ staff member* first, then the Actions setting, the credential, the repo picker, both files, and
78
+ what doctor still says. A link to another screen still wins. When the list is empty the entry
79
+ goes away; the repo picker lives on under [Org](#org).
60
80
 
61
81
  ## Inbox
62
82
 
63
83
  Everything open across the org, from one GraphQL call per repo. Bodies and full timelines come
64
84
  down with the list, so opening a thread is a render rather than a request.
65
85
 
66
- - **Filter by staff member.** An item belongs to somebody if it is in their brain repo, their
67
- own App wrote it, a peer addressed it to them with a `from-<handle>` label, or it is assigned
68
- to them. The shared public identity cannot name one staff member, so an item it wrote counts
69
- for anyone who works in that repo. Items authored by humans belong to nobody, which is
70
- correct.
71
- - **Scope** to everything, what is assigned to you, decisions, or open PRs.
72
- - **Open, recently closed, or both.** Open by default: an inbox is what is waiting on
86
+ - **The links under Inbox in the sidebar filter it.** All; Unread; each staff member, which
87
+ lists the issues on their own tracker whoever filed them; and Issues, the product repos.
88
+ - **Unread comes from your GitHub notifications.** A thread with activity you have not read has
89
+ a bar on the left and a bold title, and Unread lists only those, with a count. Opening one
90
+ marks it read on GitHub too, and reading it on GitHub clears it here. GitHub only notifies you
91
+ about repos you watch and threads you are part of.
92
+ - **Open or closed.** Open by default: an inbox is what is waiting on
73
93
  somebody, and months of finished work mixed into that answers a different question. Closed
74
94
  work reaches back 45 days, up to 30 issues and 30 pull requests per repository, and carries
75
95
  a shorter timeline than open work because it is there to be read rather than triaged. A
@@ -80,9 +100,8 @@ down with the list, so opening a thread is a render rather than a request.
80
100
  silently doing nothing.
81
101
  - **A new issue asks one question: who is it for.** One dropdown, over the staff. It goes to
82
102
  that person's brain repo and their `@handle` is written into the body for you, because those
83
- are the two things that make an issue reach an agent rather than sit there. It used to ask
84
- for a staff member *and* a repo, which let you set the pair to a combination that woke
85
- nobody. To file in a product repo instead, use GitHub: this form is for asking the staff for
103
+ are the two things that make an issue reach an agent rather than sit there. Asking for a
104
+ repo as well would let you pick a pair that wakes nobody. To file in a product repo instead, use GitHub: this form is for asking the staff for
86
105
  something.
87
106
  - **Labels are the repo's own, as toggles.** They are the labels that exist on the recipient's
88
107
  repository, fetched from GitHub and cached. A text box was a spelling test: `from-cmo` and
@@ -149,8 +168,7 @@ of every repo to show one of them is the wrong trade.
149
168
  second decision and not this button's to make. It runs `gh pr merge` as you, so a protected
150
169
  branch, a failing required check or a merge queue behaves exactly as it would on the site.
151
170
 
152
- **It does not ask how.** There used to be a squash / merge commit / rebase dropdown beside it.
153
- That is a question about git rather than about the pull request in front of you, the repository
171
+ **It does not ask how.** Squash, merge commit or rebase is a question about git rather than about the pull request in front of you, the repository
154
172
  has already answered it in its own settings, and on any given repository most of the answers are
155
173
  wrong. So the repository is asked instead: squash where it is allowed, then a merge commit, then
156
174
  rebase.
@@ -160,8 +178,8 @@ rebase.
160
178
  **`@cto` on a pull request wakes nobody.** A staff member's caller workflow lives in their own
161
179
  brain repo and gates on their handle appearing *there*; on a product repo the same mention
162
180
  posts, renders as a chip, and does nothing. That is deliberate, for the reasons in
163
- [security](security.md#trust-in-a-prompt), and it used to be silent, which is worse than the
164
- restriction itself.
181
+ [security](security.md#trust-in-a-prompt). Saying so out loud is the portal's job, because a
182
+ silent no-op is worse than the restriction itself.
165
183
 
166
184
  **Reply is the one box, and it handles this.** Name somebody in a reply where a comment will
167
185
  not reach them and the offer appears under the box, ticked: *open it on their tracker too*. One
@@ -183,16 +201,30 @@ On the **Files** tab each file's heading has its own Reply, which opens the same
183
201
  one file. "This bit is wrong" is what you want to say while looking at a diff, and the
184
202
  alternative is describing in prose which of thirty files you meant.
185
203
 
186
- There was briefly a second button up here called *Ask a staff member*. It did almost the same
187
- thing as Reply, differing mainly in making you pick a name from a dropdown rather than typing
188
- it, and nothing on the page said which one you wanted. Two ways to do one thing is worse than
189
- either of them.
190
-
191
204
  Both writes go through your own `gh`, as you. Nothing is dispatched between repositories and no
192
205
  credential is put on a public repo. The tracker issue goes first, because it is the half that
193
206
  reaches anybody; if the copy on the pull request then fails you are told, rather than being
194
207
  shown an error that invites you to ask the same person the same thing twice.
195
208
 
209
+ ## Runs
210
+
211
+ What each staff member ran in the last 30 days: when, daily or mention, how it ended, how long
212
+ it took, turns and cost where known, and a link to the log. Above the tables, the 30-day total
213
+ for the org, and each staff member's own in their heading.
214
+
215
+ Runs are not in any repo, so this is the one screen that is only ever on GitHub. It reads the
216
+ run lists through your own `gh`, the same way the inbox does, and offline it says so rather
217
+ than drawing an empty table that reads as "nothing ran".
218
+
219
+ Cost comes from the record each run leaves behind (see [cost](cost.md#what-each-run-cost)). A
220
+ run from before records existed, or from an agent that does not report cost, shows a dash, and
221
+ a total says how many runs it could price. Each record is downloaded once and kept for as long
222
+ as the portal runs, so the first visit is the slow one.
223
+
224
+ A skipped mention is not a run and is not listed. A `setup-failure` is a run that failed before
225
+ the agent started, usually a token or a checkout. Past a [`budget`](org-yaml.md#budget), the
226
+ total turns amber.
227
+
196
228
  ## Org
197
229
 
198
230
  The layer every staff member inherits, in one place: `org.yaml`, every `org/*.md`, and the
@@ -200,9 +232,12 @@ prompt files in `prompts/`, each with a line saying what it is for, because a fi
200
232
  you nothing about which to open. Anything roster does not ship falls back to its own first
201
233
  heading.
202
234
 
203
- **The list comes off disk**, not out of the page. It used to be five paths written into the
204
- portal, so a tenant that added `org/pricing.md` could not open it at all and one that had not
205
- written `org/business.md` yet got "not found" with nothing to do about it. Only files the
235
+ **Add a product repo** opens the same picker setup uses, so a repo created later does not mean
236
+ typing `role: product` into `org.yaml` by hand. Adding one commits `org.yaml` and pushes. A new
237
+ hire picks it up; somebody already hired keeps the `works_in` in their own `staff.yaml`.
238
+
239
+ **The list comes off disk**, not out of the page, so a tenant that adds `org/pricing.md` can
240
+ open it like any other. Only files the
206
241
  portal may actually write are listed: an editor that offers a file it cannot save is a trap.
207
242
 
208
243
  Every one of them is editable from the screen it is read on: an **Edit** button on the file,
@@ -225,18 +260,50 @@ have more than one human, and a card that names one of two reads as the only one
225
260
 
226
261
  ## Staff
227
262
 
228
- Everyone on the roster, and the four things you could previously only do from a terminal.
229
-
230
- **Hiring** runs the same `buildPlan` and `applyPlan` that `roster hire` does, on the server.
231
- Only the handle is required; everything else is copied from whoever is already here. You see
232
- the plan first, listing every file, every label, the schedule it chose and why, and the manual
233
- steps it cannot do for you. Nothing happens until you apply. What the terminal would have
234
- printed is shown when it finishes.
263
+ Everyone on the roster, and the things you would otherwise do from a terminal.
264
+
265
+ **Hiring** starts from a role. With nobody hired the Staff screen opens on the role picker;
266
+ after that it is behind **Hire someone**. The cards are CTO, CMO and Support, each with a line
267
+ on what they do, and **Something else**, which asks for the role's name and a sentence about it.
268
+ A role that is already hired is not offered. Picking one fills in the handle, name and repo
269
+ (`cto`, `cmo`, `support`, or a handle made from the name) and leaves the schedule empty, so the
270
+ server picks a free slot. All four are under **Advanced**, still editable. The first hire has
271
+ nobody to copy App names from, so Advanced also holds the App's name and the shared public
272
+ App's, which are `--app` and `--public-app`, filled in as `<org>-<handle>` and `<org>-robot`.
273
+ The public one only matters when a product repo is public; on private ones the session uses the
274
+ staff member's own App.
275
+
276
+ The role opens a numbered list on the same screen:
277
+
278
+ 1. **Hire.** One sentence from the plan: the repo it creates and when it runs. **Show details**
279
+ has the full plan, from the same `buildPlan` and `applyPlan` that `roster hire` runs on the
280
+ server: every file, every label, the schedule and why, the commits it makes as you in repos
281
+ that already exist (each peer's `staff.yaml`, `org.yaml`), and whether the new brain joins
282
+ the credential's org secret. **Hire** asks before it acts.
283
+ 2. **Create their GitHub App.** The private App, and the shared public one only when a product
284
+ repo in `org.yaml` is public (or has no visibility written, which hire treats as public).
285
+ 3. **Write their charter.** Three tabs: an AI interview (the default), the matching worked
286
+ example with your business's name in place of Acme's, and the file itself. A role that
287
+ matches no example has no template tab.
288
+ 4. **Add your agent credential.** Only while none is stored.
289
+ 5. **Run once now.**
290
+
291
+ Steps 2 to 5 say *Hire first* until the hire is done. Each step's Done comes from real data:
292
+ the hire from the staff member appearing in `org.yaml`, the charter from `CHARTER.md` no longer
293
+ being the stub (the same test as doctor's `charter.stub`), the App from its `_APP_ID` secret on
294
+ the brain repo, the credential from the org secret, and the run from a successful daily run.
295
+ After **Hire** the screen reloads the org and stays on the same staff member, on step 2.
296
+
297
+ A card whose setup is not finished (a stub charter, no App secrets, or no successful run yet)
298
+ shows **Finish setting up**, which opens the same list for them. The App secrets and the run
299
+ are read from GitHub, so offline only the charter counts.
235
300
 
236
301
  **Writing the charter** is the copy-a-prompt loop below, aimed at `CHARTER.md`. `hire`
237
302
  deliberately does not write it, because a generated charter produces exactly the generic agent
238
- this whole arrangement exists to avoid. So this is the route that was previously `roster brief
239
- charter <handle>` and a terminal.
303
+ this whole arrangement exists to avoid. It is the same brief as `roster brief charter
304
+ <handle>`, with somewhere to put the answer, and a picker for the worked example it carries as a
305
+ model: matched to the role, or another, or none. For a role added with **Something else**, the
306
+ sentence you typed goes into the brief.
240
307
 
241
308
  **The GitHub App** is `roster app`, on this server rather than a second one. There is no API that
242
309
  creates an App: the only route is the manifest flow, where you post a manifest to a settings page,
@@ -246,9 +313,17 @@ one origin. The private key is still held in memory and written straight to a re
246
313
 
247
314
  GitHub redirects the tab *it* opened, not the one you clicked from, so the original polls for the
248
315
  result. What it cannot do is install the App: that is a grant of access to specific repositories
249
- and GitHub asks a human to choose them, which is correct and should not be worked around. The panel
250
- says so loudly, and says to grant every tracker the staff member writes to rather than only their
251
- own.
316
+ and GitHub asks a person to confirm it, which is correct and should not be worked around. So the
317
+ panel's **Install it** opens the install page with the org and the repos already selected: the
318
+ brain, every peer tracker it writes to, and the product repos. Any whose id could not be read are
319
+ listed for you to tick.
320
+
321
+ **Agent credential** is the setup screen's paste box, reachable from each card, because the
322
+ moment you look for it is while setting somebody up. It is once for the org.
323
+
324
+ **Run once now** starts the daily workflow, follows it, and shows how it ended with the log's
325
+ link and, on a failure, the step it failed at. It asks first, because it is a real run. A success
326
+ is what turns doctor's *unproven* into proven. Health has the same button.
252
327
 
253
328
  **Retiring** is `roster retire`, and it is deliberately not deletion. A brain repo is that
254
329
  agent's entire memory and there is no undo, so retiring disables the workflows, unwires them
@@ -284,8 +359,8 @@ rather than going nowhere.
284
359
 
285
360
  ## Prompt
286
361
 
287
- **What this staff member is actually sent**, which was previously only reachable through
288
- `roster prompt <handle> --kind daily` in a terminal. Composed on the server by the tenant's own
362
+ **What this staff member is actually sent**, the same as `roster prompt <handle> --kind daily`.
363
+ Composed on the server by the tenant's own
289
364
  `compose.mjs`, so there is no second implementation to drift.
290
365
 
291
366
  Pick the kind: `daily` or `mention`. A mention prompt is written for the comment
package/docs/prompts.md CHANGED
@@ -58,12 +58,12 @@ ROSTER_CONTEXT='{"issue_number":"1","comment_id":"1","repo":"o/r"}' \
58
58
  `comment_id`; a mention typed into the body of a *new* issue does not, and there is no comment
59
59
  for the agent to fetch. So `compose.mjs` derives `event.no_comment` from the absence, and
60
60
  `prompts/mention.md` branches on it: one side points at the comment, the other at the issue
61
- body. Without that, the first instruction in the prompt was a `gh api .../issues/comments/`
61
+ body. Without that, the first instruction in the prompt would be a `gh api .../issues/comments/`
62
62
  call with no id on the end, which 404s.
63
63
 
64
- The second route is the busier one now. It is what the portal's [Ask a staff
65
- member](portal.md#asking-for-a-change) produces, and those issues often carry a pull request
66
- that lives somewhere else and say to answer there instead.
64
+ The second route is the busier one. It is what the portal produces when you [ask for a
65
+ change](portal.md#asking-for-a-change), and those issues often carry a pull request that lives
66
+ somewhere else and say to answer there instead.
67
67
 
68
68
  ## Syntax
69
69
 
@@ -99,6 +99,12 @@ than a hang.
99
99
  | `ops.dir` | ops repo directory in the checkout |
100
100
  | `staff` | the whole of this staff member's `staff.yaml` |
101
101
  | `staff.dir` | where their brain lands in the checkout |
102
+ | `staff.handle`, `staff.name` | their handle (`cto`) and role name (`Chief Technology Officer`) |
103
+ | `staff.brain` | their brain repo, `owner/name` |
104
+ | `staff.bot` | the login they post as on private repos, e.g. `acme-cto[bot]` |
105
+ | `staff.public_bot` | the login they post as on public repos |
106
+ | `staff.public_token_env` | the environment variable holding the public repo token during a run |
107
+ | `staff.status_issue` | the number of their pinned status issue |
102
108
  | `staff.product` | **first entry of `works_in`, or null** |
103
109
  | `staff.product.repo` | that repo's `owner/name` |
104
110
  | `peers` | list of the other staff members |
@@ -108,10 +114,31 @@ than a hang.
108
114
  | `event` | trigger context, from `ROSTER_CONTEXT` |
109
115
  | `event.issue_number`, `event.comment_id`, `event.repo`, `event.actor` | |
110
116
  | `event.no_comment` | true when the ask is the issue body rather than a comment. There is no `{{#unless}}`, so the absence is a value |
117
+ | `inflight` | the pull requests people have open on the product repos, as a markdown list. **Empty outside a run**, and when there are none. See below |
111
118
 
112
119
  Anything else in a manifest is reachable under `staff.`, so `staff.status_issue` and
113
120
  `staff.public_token_env` work without being listed here.
114
121
 
122
+ ## Human work in flight
123
+
124
+ Before composing, a run looks at the staff member's product repos (their `works_in`, or every
125
+ `role: product` repo) for open pull requests opened by people rather than by any App. Each one
126
+ goes in as a line: title, author, age, branch, and the files it touches, capped at twenty with
127
+ the top directories named when there are more.
128
+
129
+ `prompts/_inflight.md` carries that list under a short instruction, inside `{{#if inflight}}`,
130
+ and both kinds include it: do not open competing work on files a human branch is changing, and
131
+ raise anything about it on that pull request instead.
132
+
133
+ It exists because nothing else tells them. Without it, a person's long-running branch is
134
+ invisible, and the staff open pull requests and issues chasing the same files, which the branch
135
+ then overtakes.
136
+
137
+ The list is a value, never a template. Titles are a person's words, and a `{{` in one must not
138
+ be able to break composition, so `compose.mjs` reads `.roster-run/inflight.md` and substitutes it
139
+ as it stands. Locally there is no such file, so `roster prompt` leaves the section out; pass
140
+ `--inflight` to fetch the real list through your own `gh`.
141
+
115
142
  ## Guarding
116
143
 
117
144
  `{{staff.product}}` is null for a staff member with an empty `works_in`, and an unresolved
package/docs/security.md CHANGED
@@ -45,6 +45,39 @@ whole organisation can reach anything in it.
45
45
  Install narrowly. The Staff card's **GitHub App** panel prints the list it actually needs, and
46
46
  so does `roster app`.
47
47
 
48
+ ## The review gate
49
+
50
+ "Nothing goes out unread" is enforced by GitHub, not by the prompt. An App with
51
+ `contents: write` on a product repo can push to its default branch, and with
52
+ `pull_requests: write` it can merge its own PR, unless a rule on the branch says otherwise.
53
+
54
+ The rule: **the default branch of every product repo requires a pull request with at least one
55
+ approving review, and no staff App is on the list of who may bypass it.** A ruleset or classic
56
+ branch protection both count.
57
+
58
+ **It is off unless you turn it on**, with `review_gate: true` in `org.yaml`. Without it, staff
59
+ are told to leave merging to you, and on Pip they always have, but GitHub does not stop them.
60
+ Most orgs keep product repos private on the Free plan, where GitHub cannot enforce the rule.
61
+
62
+ With it on, `roster doctor` reads it for every `role: product` repo in `org.yaml` and every repo a staff
63
+ member `works_in`, as `review-gate`. It fails when nothing requires a PR or a staff App can
64
+ bypass the rule, and warns when a PR is required with no approval, since then whoever opened
65
+ it can merge it.
66
+
67
+ With it on, `roster hire --apply` adds a ruleset named `roster: review before merge` to each product repo
68
+ that does not already require an approving review, and leaves anything stricter alone. Pass
69
+ `--no-review-gate` to skip it. The ruleset lets repository admins bypass it only by merging a
70
+ pull request, so your own merge is still the approval (GitHub will not let you approve your
71
+ own PR) and an App, which is never an admin, cannot merge at all.
72
+
73
+ **On GitHub Free, private repos can't have this rule.** GitHub only enforces rulesets and
74
+ branch protection on private repos for paid plans. To use the gate there, make the product repo
75
+ public or move the org to GitHub Team.
76
+
77
+ To set it by hand: repo **Settings -> Rules -> Rulesets -> New branch ruleset**, target the
78
+ default branch, tick **Require a pull request before merging** with one required approval, and
79
+ keep the staff Apps off the bypass list.
80
+
48
81
  ## Permissions a new App asks for
49
82
 
50
83
  ```
@@ -106,13 +139,12 @@ into a world-readable log.
106
139
 
107
140
  So a mention on a public pull request is decoration. It posts, it renders as a chip, and
108
141
  nothing happens, which is safe and also invisible. The portal is what makes it visible and what
109
- gets you out of it: replying with an `@handle` where nothing listens says so, and **Ask a staff
110
- member** opens the request on that person's own private tracker instead, carrying the pull
111
- request and the hunk. It writes through your own `gh`, as you, so no credential lives on the
142
+ gets you out of it: replying with an `@handle` where nothing listens says so, and offers to
143
+ open the request on that person's own private tracker as well, carrying the pull request and
144
+ the hunk. It writes through your own `gh`, as you, so no credential lives on the
112
145
  public repo and nothing is dispatched across a boundary.
113
146
 
114
- There was once a forwarder in the product repo that bridged this automatically. It was removed;
115
- see [concepts](concepts.md#kinds-of-run). If you reinstate one, its author gate is
147
+ If you add a workflow to a product repo that bridges this automatically, its author gate is
116
148
  load-bearing. Do not relax it.
117
149
 
118
150
  ## The loop guard
@@ -14,8 +14,8 @@ A caller is about forty lines and does nothing but pass arguments.
14
14
  ## Why it lives in the tenant
15
15
 
16
16
  A reusable workflow in a **private** repo can only be called from inside its own organisation.
17
- A tenant therefore cannot call the framework's copy. That constraint is what forced the whole
18
- design, and it turned out better: the framework is never a runtime dependency, so nothing
17
+ A tenant therefore cannot call the framework's copy. That constraint shapes the whole
18
+ design, and it is the better one anyway: the framework is never a runtime dependency, so nothing
19
19
  breaks if it moves, goes private, or is deleted.
20
20
 
21
21
  This is also why `roster-ops` needs **Settings -> Actions -> General -> accessible from
@@ -28,7 +28,7 @@ repositories in this organisation**. Without it, callers fail with "workflow not
28
28
  | `staff` | string | required | Handle, as in `org.yaml`. |
29
29
  | `kind` | string | `daily` | `daily` or `mention`. |
30
30
  | `ops_repo` | string | required | `owner/name` of the ops repo. |
31
- | `model` | string | `claude-opus-5` | Passed to the agent, unless the agent resolves its own. |
31
+ | `model` | string | `claude-opus-5-5` | Passed to the agent, unless the agent resolves its own. |
32
32
  | `timeout_minutes` | number | `90` | Job ceiling. |
33
33
  | `allowed_tools` | string | `Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch` | Tool permissions, for agents that take them. |
34
34
  | `issue_number` | string | `""` | Trigger context. |
@@ -51,23 +51,54 @@ authentication error forty lines into a log.
51
51
 
52
52
  ## What it does, in order
53
53
 
54
- 1. **Mint the private-tracker token** from the staff member's App.
55
- 2. **Mint the public-repo token**, if a public App was passed.
56
- 3. **React to the request** with eyes, on a `mention` only. Before any checkout, so it lands in
54
+ 1. **Start the clock**, for the run record.
55
+ 2. **Mint the private-tracker token** from the staff member's App.
56
+ 3. **Mint the public-repo token**, if a public App was passed.
57
+ 4. **React to the request** with eyes, on a `mention` only. Before any checkout, so it lands in
57
58
  seconds. `continue-on-error`: a missing reaction must never cost the answer.
58
- 4. **Check out the ops repo.** It is the only thing that can be cloned without having read a
59
+ 5. **Check out the ops repo.** It is the only thing that can be cloned without having read a
59
60
  manifest, so it goes first and then says what else to clone.
60
- 5. **Work out what to check out**, by running `runner-plan.mjs`.
61
- 6. **Check out the brain**, full history. The agent reads its own past.
62
- 7. **Check out peers and product repos**, per the plan.
63
- 8. **Set git identity** to the App.
64
- 9. **Set up Node and pnpm**, if the plan found a `package.json`.
65
- 10. **Compose the prompt**, to a step output and to `.roster-prompt.txt`.
66
- 11. **Check the agent has a credential.**
67
- 12. **Work out which agent runs this**, by running `agents.mjs`.
68
- 13. **Run the session**, by one of two steps: the Action-based reference runner, or the generic
61
+ 6. **Work out what to check out**, by running `runner-plan.mjs`.
62
+ 7. **Check out the brain**, full history. The agent reads its own past.
63
+ 8. **Check out peers and product repos**, per the plan.
64
+ 9. **Gather human work in flight**: open pull requests people have on the product repos, via
65
+ `inflight.mjs`, for the prompt. Never fatal. See [prompts](prompts.md#human-work-in-flight).
66
+ 10. **Set git identity** to the App.
67
+ 11. **Set up Node and pnpm**, if the plan found a `package.json`.
68
+ 12. **Compose the prompt**, to a step output and to `.roster-prompt.txt`.
69
+ 13. **Check the agent has a credential.**
70
+ 14. **Work out which agent runs this**, by running `agents.mjs`.
71
+ 15. **Run the session**, by one of two steps: the Action-based reference runner, or the generic
69
72
  CLI one. See [choosing a coding agent](agents.md).
70
- 14. **Say so if the run did not finish.**
73
+ 16. **Write down the run**, whatever happened: staff, kind, outcome, duration, and turns, cost
74
+ and tokens where the agent reports them. Into the job summary, and kept as an artifact
75
+ called `roster-run`. Never fatal. See [cost](cost.md#what-each-run-cost).
76
+ 17. **Say so if the run did not finish.** A comment on the status issue, or on the issue that
77
+ woke a mention, linking the run.
78
+
79
+ ## When the failure is the token
80
+
81
+ The failure notice cannot rely on anything that might be what failed. A renamed repo, a rotated
82
+ key or an uninstalled App breaks the App token first, and an alert that posts with that token
83
+ says nothing at exactly the moment it is needed. So the notice uses the App token when there is
84
+ one, and falls back to the job's own `github.token` when there is not or it is refused. It then
85
+ posts as `github-actions`, and says to check the App.
86
+
87
+ That needs `issues: write` on the job token. `session.yaml` asks for it, but a called workflow
88
+ can only narrow what its caller grants, so both callers grant it too:
89
+
90
+ ```yaml
91
+ jobs:
92
+ session:
93
+ permissions:
94
+ contents: read
95
+ issues: write
96
+ uses: acme/roster-ops/.github/workflows/session.yaml@main
97
+ ```
98
+
99
+ Callers generated before this lack it, and their fallback cannot post. `roster upgrade --apply`
100
+ regenerates them. `roster doctor` separately reports any other workflow in the ops or brain
101
+ repos that has failed run after run, as `workflows.failing`.
71
102
 
72
103
  ## What `runner-plan.mjs` emits
73
104
 
@@ -92,6 +123,7 @@ Consumed by later steps as `steps.plan.outputs.*`.
92
123
  | `GH_TOKEN` | private-tracker token, already authenticated |
93
124
  | `PUBLIC_TOKEN` | public product repo token |
94
125
  | `AGENT_PROMPT_FILE` | absolute path to the composed prompt |
126
+ | `AGENT_RESULT_FILE` | where to write the agent's own result JSON, if it has one. Optional; it is how cost gets into the run record |
95
127
  | `AGENT_MODEL` | resolved model |
96
128
  | `AGENT_TOOLS` | the `allowed_tools` string |
97
129
  | *the agent's own* | its credential, under whatever name it declares |
@@ -22,13 +22,13 @@ brain: acme/technology
22
22
  status_issue: 15
23
23
 
24
24
  schedule: "0 7 * * 1-5"
25
- model: claude-opus-5
25
+ model: claude-opus-5-5
26
26
  timeout_minutes: 90
27
27
  mention_timeout_minutes: 90
28
28
 
29
29
  bot: acme-cto[bot]
30
30
  public_bot: acme-robot[bot]
31
- public_token_env: PIPWEB_TOKEN
31
+ public_token_env: PUBLIC_TOKEN
32
32
  agent_secret: CLAUDE_CODE_OAUTH_TOKEN
33
33
 
34
34
  identities:
@@ -59,6 +59,7 @@ labels:
59
59
  | `handle` | yes | Must match `org.yaml`. The composer looks them up by the `org.yaml` one, so a mismatch composes the wrong brain. |
60
60
  | `name` | yes | Role name in prose. |
61
61
  | `mention` | yes | What wakes them, as in `@cto`. The caller's condition tests for this string. |
62
+ | `icon` | no | The icon the portal shows for them: `code`, `megaphone`, `life-buoy`, `compass`, `brush`, `bug`, `server`, `book-open`, `bar-chart`, `users` or `person`. Unset, it is picked from the role. |
62
63
  | `brain` | yes | `owner/name` of this repo. Without it nothing can check secrets, labels or runs. |
63
64
  | `status_issue` | yes in practice | Number of the pinned status issue. The prompts reference it, so a run cannot compose without it. `roster hire --apply` writes it. |
64
65
 
@@ -154,6 +155,17 @@ Labels this staff member expects to exist on its own tracker, grouped for readab
154
155
  value across every group is checked by `roster doctor`. An agent applying a label that does not
155
156
  exist gets an API error mid-run.
156
157
 
158
+ ## `memory`
159
+
160
+ This staff member's own memory budgets, overriding the org's. Same three fields as
161
+ [`memory` in org.yaml](org-yaml.md#memory); any left out fall back to the org, then to the
162
+ defaults.
163
+
164
+ ```yaml
165
+ memory:
166
+ max_index_kb: 32
167
+ ```
168
+
157
169
  ## What `roster upgrade` does to this file
158
170
 
159
171
  Nothing. It is `scaffold` class: written once by `roster hire`, and yours from that moment.
@@ -22,8 +22,10 @@ a coding agent, split into what an agent can fix and what only a person can. `ro
22
22
 
23
23
  **Is:** the ops repo's Actions access is not set to organisation-wide.
24
24
 
25
- Settings -> Actions -> General on `roster-ops`. The setup screen deep-links that exact page,
26
- which is the fastest way to fix it; Health and `roster doctor` both check it explicitly.
25
+ `roster init --apply` sets it, and so does *Set it for me* on the setup screen. If GitHub
26
+ refused (it needs admin on the ops repo), set it by hand: Settings -> Actions -> General ->
27
+ Access on `roster-ops`, "accessible from repositories in the organisation". Health and `roster
28
+ doctor` both check it explicitly.
27
29
 
28
30
  ---
29
31
 
@@ -90,11 +92,9 @@ constrains it is not a thing anybody wants.
90
92
  **Is:** you edited a framework-owned file. `compose.mjs`, `agents.mjs`, `runner-plan.mjs` and
91
93
  `session.yaml` are generated. The next `roster upgrade` reconciles them against the template.
92
94
 
93
- This happened here: a fix went into `roster-ops/.github/workflows/session.yaml` instead of
94
- `templates/ops/...`, and nothing noticed because the framework had not touched that file *yet*.
95
-
96
- `roster upgrade` now reports an edit to a framework-owned file whether or not anything has
97
- collided, and `roster upgrade --check` fails on it. Move the change upstream.
95
+ Nothing notices until the framework next touches that file. `roster upgrade` reports an edit
96
+ to a framework-owned file whether or not anything has collided, and `roster upgrade --check`
97
+ fails on it. Move the change upstream.
98
98
 
99
99
  ---
100
100
 
@@ -181,7 +181,7 @@ landed on GitHub thirty seconds ago and the checkout is behind, that is what you
181
181
 
182
182
  ## `roster upgrade` says a file has no base
183
183
 
184
- A tenant created before the merge base was recorded has nothing to merge against. Reconstruct
184
+ A tenant with no recorded merge base has nothing to merge against. Reconstruct
185
185
  one from the framework's history:
186
186
 
187
187
  ```bash
package/docs/upgrading.md CHANGED
@@ -10,9 +10,9 @@ The framework writes templates out. A tenant runs its own copies. So the two dri
10
10
  `roster upgrade` is what reconciles them without eating your edits.
11
11
 
12
12
  ```bash
13
- roster upgrade # what would change
14
- roster upgrade --apply # do it
15
- roster upgrade --check # exit non-zero if anything is pending (for CI)
13
+ npx @nanocollective/roster@latest upgrade # what would change
14
+ npx @nanocollective/roster@latest upgrade --apply # do it
15
+ npx @nanocollective/roster@latest upgrade --check # exit non-zero if anything is pending (for CI)
16
16
  ```
17
17
 
18
18
  ## How it decides
@@ -53,6 +53,12 @@ The diff is printed either way, so nothing goes quietly.
53
53
  If you want a caller to differ, change the thing it is generated from. Timeouts, schedule,
54
54
  model and identities all live in `staff.yaml`.
55
55
 
56
+ **A caller the framework no longer generates is removed.** It would still dispatch into
57
+ `session.yaml` with a kind that no longer composes, and fail at run time. The plan lists it.
58
+ The one that has gone so far is `<handle>-pr-mention.yaml`; if your tenant is old enough to
59
+ have one, a forwarding workflow in the product repo went with it, and that one is yours to
60
+ delete, because `roster upgrade` never writes into product repos.
61
+
56
62
  ## Conflicts
57
63
 
58
64
  A conflict is never written into a live file. Agents read `org/voice.md` at every boot, and
@@ -28,6 +28,13 @@ layer. That is the part that matters most, because without them the model writes
28
28
  of whoever it was shown. The brief then interviews you, drafts from your answers, and tells you
29
29
  what it cut and why.
30
30
 
31
+ **So is a worked example, when one fits.** A staff member whose handle or role reads as one of
32
+ the ten roles below gets the matching [example](#worked-examples) inside the brief, labelled as a
33
+ model for the shape and not content to copy. The copy-a-prompt panel has a picker to choose
34
+ another or none; in a terminal it is `--example <handle>` or `--example none`. It is still a brief you
35
+ answer: the interview comes first, and nothing in the charter should come from the example
36
+ rather than from you.
37
+
31
38
  From a terminal, the same brief:
32
39
 
33
40
  ```bash
@@ -63,6 +70,27 @@ touches. Be specific. A vague boundary is one that gets crossed at 07:00 with no
63
70
  **Where the rest of it lives.** Point at `memory/INDEX.md`, `log/decisions.md`, the pinned
64
71
  status issue, and the surfaces the manifest declares.
65
72
 
73
+ ## Worked examples
74
+
75
+ Ten, for an invented company called Acme. The handle in brackets is the one `--example` takes.
76
+
77
+ | Example | What they do |
78
+ |---|---|
79
+ | [CTO](charters/cto.md) (`cto`) | Builds the product: fixes, features and tests, as pull requests. |
80
+ | [CMO](charters/cmo.md) (`cmo`) | Posts, SEO, copy and launch plans, as drafts to approve. |
81
+ | [Head of Support](charters/support.md) (`support`) | Answers issues, writes help docs, and turns user reports into bugs. |
82
+ | [Product Manager](charters/pm.md) (`pm`) | Turns ideas and user feedback into clear specs and a ranked backlog. |
83
+ | [Designer](charters/designer.md) (`designer`) | Improves the product's look and usability, with accessibility fixes, as pull requests. |
84
+ | [QA Engineer](charters/qa.md) (`qa`) | Tests the product, finds bugs, and writes clear reproductions and tests. |
85
+ | [DevOps Engineer](charters/devops.md) (`devops`) | Keeps CI, deploys and dependencies healthy, and patches security updates. |
86
+ | [Technical Writer](charters/writer.md) (`writer`) | Writes and keeps up the docs, guides and changelog. |
87
+ | [Data Analyst](charters/analyst.md) (`analyst`) | Reads the numbers and writes a short weekly report on what changed. |
88
+ | [Community Manager](charters/community.md) (`community`) | Answers discussions, welcomes contributors, and drafts release announcements. |
89
+
90
+ They are examples to adapt, not templates to fill in. Read them for what a finished charter covers and how specific it gets, then write your own
91
+ about your business. A charter copied from one of these describes Acme. The brief carries the matching one for
92
+ you; these links are for reading them first.
93
+
66
94
  ## Things worth being concrete about
67
95
 
68
96
  - **Escalation.** Name the label and the mechanism, not the sentiment. "Open an issue labelled