@nanocollective/roster 0.1.0-alpha.1

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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/cli.js +5679 -0
  4. package/docs/README.md +99 -0
  5. package/docs/agents.md +163 -0
  6. package/docs/architecture.md +121 -0
  7. package/docs/commands.md +223 -0
  8. package/docs/concepts.md +112 -0
  9. package/docs/cost.md +61 -0
  10. package/docs/developing.md +147 -0
  11. package/docs/doctor-codes.md +74 -0
  12. package/docs/export.md +113 -0
  13. package/docs/extending.md +97 -0
  14. package/docs/getting-started.md +134 -0
  15. package/docs/hosting.md +72 -0
  16. package/docs/manual-steps.md +163 -0
  17. package/docs/memory.md +71 -0
  18. package/docs/org-yaml.md +143 -0
  19. package/docs/portal.md +342 -0
  20. package/docs/prompts.md +133 -0
  21. package/docs/security.md +122 -0
  22. package/docs/session-workflow.md +112 -0
  23. package/docs/staff-yaml.md +163 -0
  24. package/docs/troubleshooting.md +189 -0
  25. package/docs/upgrading.md +83 -0
  26. package/docs/writing-a-charter.md +83 -0
  27. package/package.json +60 -0
  28. package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +33 -0
  29. package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +65 -0
  30. package/templates/brain/.github/workflows/%%STAFF%%-pr-mention.yaml +50 -0
  31. package/templates/brain/CHARTER.md +49 -0
  32. package/templates/brain/README.md +18 -0
  33. package/templates/brain/drafts/README.md +7 -0
  34. package/templates/brain/log/decisions.md +6 -0
  35. package/templates/brain/memory/INDEX.md +28 -0
  36. package/templates/brain/staff.yaml +44 -0
  37. package/templates/brain/strategy/README.md +7 -0
  38. package/templates/briefs/amend.md +60 -0
  39. package/templates/briefs/charter.md +47 -0
  40. package/templates/briefs/discover.md +61 -0
  41. package/templates/briefs/voice.md +53 -0
  42. package/templates/ops/.github/workflows/session.yaml +333 -0
  43. package/templates/ops/agents.mjs +143 -0
  44. package/templates/ops/compose.mjs +333 -0
  45. package/templates/ops/org/guardrails.md +14 -0
  46. package/templates/ops/org/operating.md +82 -0
  47. package/templates/ops/org/voice.md +40 -0
  48. package/templates/ops/prompts/_identity.md +14 -0
  49. package/templates/ops/prompts/_paths.md +15 -0
  50. package/templates/ops/prompts/daily.md +82 -0
  51. package/templates/ops/prompts/mention.md +53 -0
  52. package/templates/ops/prompts/pr-mention.md +57 -0
  53. package/templates/ops/runner-plan.mjs +65 -0
  54. package/templates/portal/css/base.css +104 -0
  55. package/templates/portal/css/brain.css +106 -0
  56. package/templates/portal/css/diff.css +28 -0
  57. package/templates/portal/css/graph.css +34 -0
  58. package/templates/portal/css/health.css +41 -0
  59. package/templates/portal/css/inbox.css +79 -0
  60. package/templates/portal/css/layout.css +98 -0
  61. package/templates/portal/css/markdown.css +54 -0
  62. package/templates/portal/css/setup.css +106 -0
  63. package/templates/portal/index.html +55 -0
  64. package/templates/portal/js/api.js +74 -0
  65. package/templates/portal/js/app.js +282 -0
  66. package/templates/portal/js/dialog.js +70 -0
  67. package/templates/portal/js/dom.js +106 -0
  68. package/templates/portal/js/icons.js +94 -0
  69. package/templates/portal/js/md.js +386 -0
  70. package/templates/portal/js/refresh.js +59 -0
  71. package/templates/portal/js/router.js +20 -0
  72. package/templates/portal/js/state.js +160 -0
  73. package/templates/portal/js/textdiff.js +96 -0
  74. package/templates/portal/js/views/app.js +128 -0
  75. package/templates/portal/js/views/brain.js +260 -0
  76. package/templates/portal/js/views/changed.js +157 -0
  77. package/templates/portal/js/views/checklist.js +87 -0
  78. package/templates/portal/js/views/docs.js +84 -0
  79. package/templates/portal/js/views/files.js +95 -0
  80. package/templates/portal/js/views/graph.js +436 -0
  81. package/templates/portal/js/views/health.js +158 -0
  82. package/templates/portal/js/views/inbox.js +549 -0
  83. package/templates/portal/js/views/memory.js +135 -0
  84. package/templates/portal/js/views/org.js +175 -0
  85. package/templates/portal/js/views/paste.js +142 -0
  86. package/templates/portal/js/views/prompt.js +412 -0
  87. package/templates/portal/js/views/repos.js +92 -0
  88. package/templates/portal/js/views/setup.js +344 -0
  89. package/templates/portal/js/views/staff.js +290 -0
package/docs/portal.md ADDED
@@ -0,0 +1,342 @@
1
+ ---
2
+ title: "The portal"
3
+ description: "Every view and every action."
4
+ sidebar_order: 17
5
+ ---
6
+
7
+ # The portal
8
+
9
+ ```bash
10
+ roster # or `roster portal`, or `npx @nanocollective/roster`
11
+ ```
12
+
13
+ A local web UI over the checked-out repositories. Reads them from disk, so it needs no
14
+ authentication and no API quota, and works offline. It can write to GitHub through your own
15
+ `gh`. See [hosting](hosting.md) for why it stays local.
16
+
17
+ Keep the repos checked out beside each other, in the same shape the runner uses.
18
+
19
+ ## Setup
20
+
21
+ **With no tenant where you started it, the portal is the setup screen instead.** That is not an
22
+ error state: it is the first thing anybody sees, and the page becomes the thing that fixes it.
23
+
24
+ The server starts without a workspace, borrows the framework's own `compose.mjs` until a tenant
25
+ has vendored its copy, and mounts only the setup routes. Everything else answers `409` with
26
+ `mode: setup` rather than dereferencing a workspace that was never found. The moment `org.yaml`
27
+ lands on disk it re-resolves, switches to the tenant's composer, and the rest of the portal
28
+ appears **without a restart**.
29
+
30
+ It asks GitHub which of two things this is:
31
+
32
+ - **The organisation already runs roster.** Then nothing needs creating; it needs checking out.
33
+ The button becomes *Check it out here*, and it clones the ops repo and every brain side by
34
+ side, which is the shape the CI runner uses. This is how a second person joins an org somebody
35
+ else set up. *Create* is hidden, because offering both is how an org ends up with two ops repos.
36
+ - **It does not.** Then the plan is shown first, listing every file and the repo it would create, and
37
+ nothing is written until you apply. Same `initFiles` the CLI runs, so the browser and the
38
+ terminal cannot disagree about what a new tenant contains.
39
+
40
+ Then: the Actions setting, deep-linked to the exact page with the failure it causes if skipped;
41
+ which repos the staff work in, as a picker over what your `gh` can see minus what `org.yaml`
42
+ already has; and a prompt for writing `org/business.md`.
43
+
44
+ Nothing here stores which step you are on. Setup takes days rather than minutes: an App has to be
45
+ installed, a credential set, a first run finished. So the page derives its state from `roster
46
+ doctor` every time it is drawn. A stored step counter would disagree with the world within an hour.
47
+
48
+ ## Inbox
49
+
50
+ Everything open across the org, from one GraphQL call per repo. Bodies and full timelines come
51
+ down with the list, so opening a thread is a render rather than a request.
52
+
53
+ - **Filter by staff member.** An item belongs to somebody if it is in their brain repo, their
54
+ own App wrote it, a peer addressed it to them with a `from-<handle>` label, or it is assigned
55
+ to them. The shared public identity cannot name one staff member, so an item it wrote counts
56
+ for anyone who works in that repo. Items authored by humans belong to nobody, which is
57
+ correct.
58
+ - **Scope** to everything, what is assigned to you, decisions, or open PRs.
59
+ - **Open, recently closed, or both.** Open by default: an inbox is what is waiting on
60
+ somebody, and months of finished work mixed into that answers a different question. Closed
61
+ work reaches back 45 days, up to 30 issues and 30 pull requests per repository, and carries
62
+ a shorter timeline than open work because it is there to be read rather than triaged. A
63
+ closed row is dimmed and marked; the sidebar badge keeps counting only what is open.
64
+ - **Reply, close, reopen, open an issue.** All as you, through your own `gh`, so they are
65
+ indistinguishable from doing it on the site. Closing asks for confirmation.
66
+ - Issue and PR references in a body become chips you can click through, and `@handle`
67
+ mentions become chips too. A mention of somebody on this roster goes to their repository
68
+ rather than to a GitHub profile of that name, which for `@cto` is a stranger.
69
+ - **Not every comment is markdown.** Deploy bots post raw HTML. A `<table>` renders as a
70
+ table, and inline `<a>`, `<img>`, `<strong>`, `<em>`, `<code>` and `<br>` are reduced to
71
+ what they stand for. Nothing relaxes the escaping: the HTML is taken apart and its pieces go
72
+ back through the same escape-first renderer as everything else, so no markup from a comment
73
+ ever reaches the page. Code spans and fences are left alone, so a comment discussing
74
+ `<meta name="robots">` still says so, and a tag outside the handful above stays visible as
75
+ text rather than being silently deleted.
76
+ - **The whole thread, not just the comments.** Cross-references ("mentioned this in #55"),
77
+ commits that reference the issue, and close, reopen and merge events sit inline in GitHub's
78
+ own order. Labels, assignees and renames are bookkeeping, so a run of them folds behind one
79
+ disclosure. An item a cross-reference points at opens in the portal when the inbox already
80
+ holds it, and on GitHub when it does not.
81
+
82
+ ## Org
83
+
84
+ The layer every staff member inherits, in one place: `org.yaml` and the four org documents,
85
+ each with a line saying what it is for, because five filenames tell you nothing about which to
86
+ open. All five are editable, and saving commits and pushes.
87
+
88
+ `org.yaml` is the exception to the write allowlist. It belongs to the person rather than the
89
+ agent, so it is writable. But it is the one file here that stops every prompt composing when
90
+ it is wrong, so the server parses it with the tenant's own `compose.mjs` first and refuses
91
+ anything that is not YAML it can read, or that has lost `org`, `name`, or a handle on a staff
92
+ entry.
93
+
94
+ ## Staff
95
+
96
+ Everyone on the roster, and the four things you could previously only do from a terminal.
97
+
98
+ **Hiring** runs the same `buildPlan` and `applyPlan` that `roster hire` does, on the server.
99
+ Only the handle is required; everything else is copied from whoever is already here. You see
100
+ the plan first, listing every file, every label, the schedule it chose and why, and the manual
101
+ steps it cannot do for you. Nothing happens until you apply. What the terminal would have
102
+ printed is shown when it finishes.
103
+
104
+ **Writing the charter** is the copy-a-prompt loop below, aimed at `CHARTER.md`. `hire`
105
+ deliberately does not write it, because a generated charter produces exactly the generic agent
106
+ this whole arrangement exists to avoid. So this is the route that was previously `roster brief
107
+ charter <handle>` and a terminal.
108
+
109
+ **The GitHub App** is `roster app`, on this server rather than a second one. There is no API that
110
+ creates an App: the only route is the manifest flow, where you post a manifest to a settings page,
111
+ a human confirms, and GitHub hands back a one-time code. `roster app` stands up its own listener on
112
+ 4310 to catch that; in the portal it runs on the port you are already on, so it is one browser and
113
+ one origin. The private key is still held in memory and written straight to a repo secret.
114
+
115
+ GitHub redirects the tab *it* opened, not the one you clicked from, so the original polls for the
116
+ result. What it cannot do is install the App: that is a grant of access to specific repositories
117
+ and GitHub asks a human to choose them, which is correct and should not be worked around. The panel
118
+ says so loudly, and says to grant every tracker the staff member writes to rather than only their
119
+ own.
120
+
121
+ **Retiring** is `roster retire`, and it is deliberately not deletion. A brain repo is that
122
+ agent's entire memory and there is no undo, so retiring disables the workflows, unwires them
123
+ from `org.yaml` and from every peer, and deletes the dead `from-<handle>` labels. The plan says
124
+ what it keeps as prominently as what it stops, because that is the thing you have to believe
125
+ before clicking it.
126
+
127
+ ## Brain
128
+
129
+ Memory and the file tree, merged, because they were always the same thing: both manifests
130
+ already declared `memory/` as a surface.
131
+
132
+ The navigator has three boxes, because a parsed memory section and a file on disk are
133
+ different kinds of thing.
134
+
135
+ **Memory** is the fact sections, the notes behind them, and `INDEX.md` itself. A note is the
136
+ argument behind one fact, read only when that fact is in play, which is what keeps the index
137
+ cheap enough to read at every boot. Both live here rather than among the files: `INDEX.md` is
138
+ literally what "All facts" renders.
139
+
140
+ **Identity** is `CHARTER.md` and `staff.yaml`. Neither is in a declared surface, and they are
141
+ the two files that decide what this staff member is.
142
+
143
+ **Files** is every other declared surface, folded, with anything over a dozen files closed.
144
+
145
+ One search box searches facts and files together: type a slug and the matching facts are
146
+ offered directly. Clicking a fact's name narrows the pane to its section with that fact lit;
147
+ a crumb says so, and Escape or either crumb widens it again.
148
+
149
+ Renderers follow the surface's `render` field: markdown as documents, images, CSV as tables,
150
+ code with its line breaks. A relative link inside a brain document opens that file in the pane
151
+ rather than going nowhere.
152
+
153
+ ## Prompt
154
+
155
+ **What this staff member is actually sent**, which was previously only reachable through
156
+ `roster prompt <handle> --kind daily` in a terminal. Composed on the server by the tenant's own
157
+ `compose.mjs`, so there is no second implementation to drift.
158
+
159
+ Pick the kind: `daily`, `mention` or `pr-mention`. A mention prompt is written for the comment
160
+ that woke it, so a preview fills in obviously-fake context.
161
+
162
+ Beneath the composed text, the files it was made of, in two groups.
163
+
164
+ **Inlined, in order** is walked out of the `{{> …}}` includes rather than written down, so it
165
+ stays true when somebody adds a fragment. Each row says which repo it came from, because a
166
+ change to `roster-ops/org/voice.md` reaches every staff member and a change to a brain's own
167
+ `prompts/work.md` reaches one. An optional fragment (`{{>? …}}`) that a role does not have is
168
+ shown as absent rather than hidden.
169
+
170
+ **Named, not inlined** is `CHARTER.md`, `memory/INDEX.md` and `org/business.md`. The prompt
171
+ tells the agent to open these; it does not contain them. Editing a charter changes what an
172
+ agent does without changing a byte of the composed prompt, and that distinction is easy to
173
+ miss.
174
+
175
+ ### Problems
176
+
177
+ The audit, beside the layers. Nothing here judges prose: every check is something a machine
178
+ can be sure about, because a linter you stop believing is worse than no linter.
179
+
180
+ | Check | Why it matters |
181
+ |---|---|
182
+ | a file is still the scaffold | `org/business.md` is composed into every run. Leaving it as questions is invisible: the run works and the output is just generic |
183
+ | a placeholder never resolved | `{{staff.product.repo}}` is null for a staff member who contributes to no other repo, and the agent reads the braces literally |
184
+ | the same line in two layers | the layers are inherited, so a rule a charter repeats from `org/` is duplication nobody reading either file can see |
185
+ | the prompt is long | it is read in full on every run, forever |
186
+ | it names a file that is not there | an instruction to read something absent is a quiet no-op inside a run nobody watches |
187
+ | an included layer is empty | it contributes nothing and costs a line of includes |
188
+
189
+ **Every finding carries the fix.** "Copy a prompt to fix this" builds a brief containing the
190
+ finding, the composed prompt, and every layer, and puts it on your clipboard. Paste it into
191
+ whatever agent you use. Knowing there is a problem is the hard part; writing the paragraph is
192
+ not. The box comes pre-filled with what the finding worked out, so you can add to it rather
193
+ than retype it.
194
+
195
+ ### Getting help changing it
196
+
197
+ **Copy a brief for changing this** asks what you want changed, in a box big enough to say it
198
+ in, and copies the same thing: a
199
+ self-contained prompt carrying the composed text, every layer with its path and blast radius,
200
+ and what may and may not be edited. `roster brief amend <handle> --want "…"` prints the same
201
+ from the terminal.
202
+
203
+ It carries the state rather than asking for it, because working out which of eight files to
204
+ open is the difficulty being solved. A brief that says "read your layers first" has handed
205
+ that straight back.
206
+
207
+ ### Editing
208
+
209
+ Layers are editable in place. Saving writes the file, commits **only that file**, and pushes,
210
+ as you, through your own git. The confirmation names the repository and says who picks it up:
211
+ every staff member for anything under `org/` or `prompts/`, one staff member for a brain file.
212
+
213
+ The writable set is an allowlist, because this commits to repositories the agents run from:
214
+
215
+ | Writable | Not |
216
+ |---|---|
217
+ | `<ops>/org/*.md` | `org.yaml`, `compose.mjs`, `agents.mjs` |
218
+ | `<ops>/prompts/*.md` | any `.github/workflows/` |
219
+ | `<brain>/CHARTER.md` | `staff.yaml` |
220
+ | `<brain>/prompts/*.md` | `memory/INDEX.md` |
221
+
222
+ `staff.yaml` and the workflows break composition when they are wrong, and `memory/INDEX.md` is
223
+ the agent's own working memory: a hand edit there is writing over what the next run is about
224
+ to rewrite. All four are readable, none is writable.
225
+
226
+ An unrelated edit sitting in the working tree is left alone. A push that fails is reported with
227
+ the commit it did make, rather than as a failure, because the edit is committed and that is the
228
+ part that is awkward to redo.
229
+
230
+ **Saving shows what the edit did to the composed prompt**, not to the file. Those are not the
231
+ same thing: a line added to one fragment can land three times or not at all, and the file diff
232
+ answers a question you did not ask.
233
+
234
+ ## Copy a prompt, paste the answer back
235
+
236
+ `org/business.md` and every `CHARTER.md` are the two files nothing can generate. roster holds no
237
+ model credential and is agent-agnostic on purpose, so the portal cannot write them for you and
238
+ should not pretend to.
239
+
240
+ What it does instead is both halves of a round trip.
241
+
242
+ **Copy the prompt** builds a brief that carries its own state: every file it refers to is inlined,
243
+ so a chat window with no filesystem is as useful here as an agent standing in the repo. A charter
244
+ brief carries the org layer, `business.md` and **the peers' charters**, because without those the
245
+ model writes a second copy of whoever it was shown. It runs about 19,000 characters, on purpose:
246
+ one paste into a large-context model beats six rounds of it asking for files it will never get.
247
+
248
+ **Paste the answer back** turns a chat reply into a file. The brief asks for the finished file
249
+ inside sentinels:
250
+
251
+ ```
252
+ <<<ROSTER FILE roster-ops/org/business.md>>>
253
+ ...the whole file...
254
+ <<<ROSTER END>>>
255
+ ```
256
+
257
+ Sentinels rather than code fences, because fences cannot survive the content: a charter and a
258
+ `business.md` both legitimately contain fenced examples. Text outside the block is ignored, because
259
+ the model will chat; one wrapping fence is stripped, because it will fence things anyway.
260
+
261
+ **Nothing is saved automatically.** You get a diff, then a button. And the failures come back as
262
+ next steps rather than errors, each with a line you can copy straight back:
263
+
264
+ | | |
265
+ |---|---|
266
+ | no envelope | *"Your AI answered in prose"*, plus the re-prompt |
267
+ | a file the brief did not ask for | refused and named; never offered as a save |
268
+ | the template handed straight back | caught; some models restate a long prompt before working |
269
+ | four lines | *"a failed answer, not a short one"* |
270
+
271
+ `roster brief <kind>` prints the same brief in a terminal.
272
+
273
+ ## Graph
274
+
275
+ Two levels. Memory sections are big nodes on a fixed ring; clicking one fans its facts outwards
276
+ into its own angular slice, so a fan never lands among its neighbours. Clicking a fact reads
277
+ it. Groups keep their angle for the life of the view, so the picture is something you can build
278
+ a mental map of.
279
+
280
+ ## What changed
281
+
282
+ What this agent learned and forgot, from git. Facts added and removed, grouped by day, each
283
+ opening the diff that did it: one block per file, coloured, with line numbers.
284
+
285
+ ## Health
286
+
287
+ Three parts.
288
+
289
+ **The org, from `roster doctor`.** Every finding that is not `ok`, with what to do about it, and
290
+ a button that turns the lot into one brief for a coding agent. That is `roster fix`: `doctor`,
291
+ the prompt audit and `lint` each already carry the sentence that fixes their own finding, and
292
+ this collects them.
293
+
294
+ Two piles come out, and the split matters. What an agent editing files here can do, and what only
295
+ a person can: an org permission on a settings page, an App a human has to install, a credential
296
+ roster cannot obtain. The second pile is listed but explicitly not asked for, because an agent
297
+ handed one of those does not fail cleanly: it invents a workaround, and every workaround is worse
298
+ than the finding. The brief also names the framework-owned files **before** any of the work, since
299
+ an agent that has started editing has stopped reading.
300
+
301
+ Paste it into whatever edits files here, then press *Check again*. The ids should be gone.
302
+
303
+ **Memory problems**, and **the rig**.
304
+
305
+ Memory problems are the same checks `roster lint` runs. Each one has a button that opens an
306
+ issue in that staff member's own repo asking them to fix it, which is usually right, because
307
+ they wrote it.
308
+
309
+ **The rig**: schedule in words, workflows present, last commit, last commit touching `memory/`,
310
+ index and notes size, charter, status issue, missing surfaces.
311
+
312
+ ## Docs
313
+
314
+ The framework's own documentation, rendered where you already are. Links between pages navigate
315
+ the portal.
316
+
317
+ ## After upgrading roster
318
+
319
+ **Restart the portal.** Its stylesheets and modules are read per request, so editing one and
320
+ reloading works. Its server is not: it is loaded when `roster portal` starts. A portal left
321
+ running across an upgrade serves new modules against an old API.
322
+
323
+ The page notices and says so rather than rendering half of itself against values that are not
324
+ there, naming the fields the old server is not sending.
325
+
326
+ ## Keeping it current
327
+
328
+ `/api/sync` fetches and fast-forwards every repository on each refresh. It refuses to pull one
329
+ that is dirty or has diverged, and says which in a banner rather than guessing. If a run landed
330
+ thirty seconds ago and your checkout is behind, that is what you are looking at.
331
+
332
+ Press `r` to refresh. The URL carries the state, so a refresh lands where you were and a link
333
+ is shareable.
334
+
335
+ ## Flags
336
+
337
+ ```
338
+ --port <n> default 4300
339
+ --host <a> default 127.0.0.1. Anything else exposes write actions to the network.
340
+ --ops <dir> ops repo directory
341
+ --dir <path> where a tenant would be created or checked out (default: here)
342
+ ```
@@ -0,0 +1,133 @@
1
+ ---
2
+ title: "Prompt reference"
3
+ description: "The template syntax, the context, and what has to be guarded."
4
+ sidebar_order: 15
5
+ ---
6
+
7
+ # Prompt reference
8
+
9
+ What a staff member is actually sent, and how to change it.
10
+
11
+ See it for yourself before changing anything:
12
+
13
+ ```bash
14
+ roster prompt cto --kind daily
15
+ ```
16
+
17
+ ## What gets assembled
18
+
19
+ ```
20
+ org/operating.md the autonomy contract: boot ritual, hand-off, decision rights
21
+ org/guardrails.md non-negotiables, binding on everyone
22
+ org/voice.md house style
23
+ org/business.md what the business is
24
+ <staff>/CHARTER.md the personality
25
+ prompts/<kind>.md what this kind of run is for
26
+ ```
27
+
28
+ Assembled at run time by `compose.mjs`, in the tenant's own repo. It is vendored rather than
29
+ imported so a run at 07:00 depends on no network fetch, no npm, and no organisation the tenant
30
+ does not control.
31
+
32
+ ## Kinds
33
+
34
+ | Kind | Woken by | Needs trigger context |
35
+ |---|---|---|
36
+ | `daily` | cron | no |
37
+ | `mention` | `@handle` in a comment, or in a new issue body | yes |
38
+ | `pr-mention` | a review comment on the product repo, forwarded in | yes |
39
+
40
+ `mention` and `pr-mention` refuse to compose without context, because they are written for the
41
+ comment that woke them. That is correct behaviour. To see one locally:
42
+
43
+ ```bash
44
+ ROSTER_CONTEXT='{"issue_number":"1","comment_id":"1","pr_number":"1","repo":"o/r"}' \
45
+ roster prompt cto --kind mention
46
+ ```
47
+
48
+ ## Syntax
49
+
50
+ Four forms, and nothing else.
51
+
52
+ | Form | Does |
53
+ |---|---|
54
+ | `{{ path.to.value }}` | Substitutes a value. **Unresolved is a hard error**, not a blank. |
55
+ | `{{> partial.md }}` | Includes a fragment. Required: a missing one throws. |
56
+ | `{{>? partial.md }}` | Includes a fragment if it exists, nothing if it does not. |
57
+ | `{{#if path}}...{{/if}}` | Includes a block when the value is truthy. |
58
+
59
+ **Conditionals do not nest.** The matcher is non-greedy, so an inner `{{/if}}` closes the outer
60
+ block. If you need two conditions, guard on the one that can actually be absent.
61
+
62
+ Conditionals are resolved first, so a partial inside a false block is never read. That matters
63
+ when the partial is expensive or may not exist.
64
+
65
+ Include depth is capped at 8, which turns a partial that includes itself into an error rather
66
+ than a hang.
67
+
68
+ ## The context
69
+
70
+ | Path | Is |
71
+ |---|---|
72
+ | `org` | the whole of `org.yaml` |
73
+ | `org.name`, `org.org` | the business name, the GitHub org |
74
+ | `human` | the `human` block from `org.yaml` |
75
+ | `human.name`, `human.github`, `human.marker` | |
76
+ | `ops.dir` | ops repo directory in the checkout |
77
+ | `staff` | the whole of this staff member's `staff.yaml` |
78
+ | `staff.dir` | where their brain lands in the checkout |
79
+ | `staff.product` | **first entry of `works_in`, or null** |
80
+ | `staff.product.repo` | that repo's `owner/name` |
81
+ | `peers` | list of the other staff members |
82
+ | `peer` | the first peer, or null |
83
+ | `peer_list` | peers pre-rendered as a markdown list |
84
+ | `kind` | `daily`, `mention` or `pr-mention` |
85
+ | `event` | trigger context, from `ROSTER_CONTEXT` |
86
+ | `event.issue_number`, `event.comment_id`, `event.pr_number`, `event.repo`, `event.actor` | |
87
+
88
+ Anything else in a manifest is reachable under `staff.`, so `staff.status_issue` and
89
+ `staff.public_token_env` work without being listed here.
90
+
91
+ ## Guarding
92
+
93
+ `{{staff.product}}` is null for a staff member with an empty `works_in`, and an unresolved
94
+ placeholder is a hard error. Anything referring to the product repo must be wrapped:
95
+
96
+ ```
97
+ {{#if staff.product}}
98
+ Open the PR on `{{staff.product.repo}}` from a branch.
99
+ {{/if}}
100
+ ```
101
+
102
+ The same applies to `{{peers}}`, which is empty for the only staff member in an org.
103
+
104
+ This is not hypothetical. The shipped prompts referred to `{{staff.product.repo}}` unguarded,
105
+ which nobody noticed because every existing staff member had one. The first staff member of any
106
+ new org could not compose a prompt at all.
107
+
108
+ ## Overriding a fragment for one staff member
109
+
110
+ ```
111
+ {{>? staff:prompts/work.md}}
112
+ ```
113
+
114
+ Renders `prompts/work.md` from that staff member's own brain repo if it exists, and nothing if
115
+ it does not. This is how one role gets a different working ritual without changing anybody
116
+ else's. `daily.md` already carries this hook.
117
+
118
+ ## Changing them
119
+
120
+ `prompts/` is `seeded` class: yours to edit, and `roster upgrade` gives you a real three-way
121
+ merge. Changes reach every staff member on their next run.
122
+
123
+ Before pushing a prompt change to a live org, diff the composed result:
124
+
125
+ ```bash
126
+ roster prompt cto --kind daily > before.txt
127
+ # edit
128
+ roster prompt cto --kind daily > after.txt
129
+ diff before.txt after.txt
130
+ ```
131
+
132
+ That is the only test there is for a prompt change. A change that composes fine and reads
133
+ differently is not caught by anything else.
@@ -0,0 +1,122 @@
1
+ ---
2
+ title: "Security model"
3
+ description: "What can reach what, what stops it, and what is not defended against."
4
+ sidebar_order: 21
5
+ ---
6
+
7
+ # Security model
8
+
9
+ What can reach what, and what stops it.
10
+
11
+ ## Credentials
12
+
13
+ | Credential | Lives | Can |
14
+ |---|---|---|
15
+ | Staff App private key | a repo secret | mint a token for the trackers it is installed on |
16
+ | Public App private key | a repo secret | mint a token for the public product repo |
17
+ | Agent credential | a repo secret | spend money with your model provider |
18
+ | Your `gh` token | your machine | whatever you can |
19
+
20
+ **No credential is held by roster.** The CLI and the portal shell out to your own `gh`. The
21
+ framework has no service, no server it talks to, and nothing to store.
22
+
23
+ ## The App private key
24
+
25
+ Created by GitHub during the manifest flow and returned exactly once. `roster app` holds it in
26
+ memory and passes it to `gh secret set` on standard input.
27
+
28
+ It is never written to a file, never passed as a command-line argument, and never appears in
29
+ the process table. If the secret write fails after the App is created, the key is unrecoverable
30
+ and roster says so: generate a new one from the App's settings page.
31
+
32
+ ## What a run can reach
33
+
34
+ A session runs with two tokens and a shell. It can do anything those tokens can do, which is
35
+ the point and also the boundary worth understanding.
36
+
37
+ **It cannot push a change under `.github/workflows/` in any repository.** That is a GitHub
38
+ restriction on App tokens, not a roster policy, and it is why agents cannot modify the
39
+ workflows that constrain them.
40
+
41
+ **It can only reach repos the App was installed on.** Under-granting is the common
42
+ misconfiguration and it fails at run time. Over-granting is the risk: an App installed on the
43
+ whole organisation can reach anything in it.
44
+
45
+ Install narrowly. `roster app` prints the list it actually needs.
46
+
47
+ ## Permissions a new App asks for
48
+
49
+ ```
50
+ contents: write commit and push
51
+ issues: write open, comment, close, label, pin
52
+ pull_requests: write
53
+ metadata: read always required
54
+ ```
55
+
56
+ Deliberately **not** `workflows: write`. One of the pre-existing Apps here declares it, which is
57
+ a good illustration of the trap: **a declaration is not a grant**. `GET /apps/<slug>` reports
58
+ what an App asked for, which says nothing about what any installation gave it.
59
+
60
+ Never verify an installation by reading the API. The only proof is a run that finished.
61
+
62
+ ## Setting up from the portal
63
+
64
+ The setup screen creates repositories, creates GitHub Apps and writes repository secrets. It is
65
+ the most privileged surface roster has, and it is **local only**: bound to `127.0.0.1`, behind the
66
+ same write guard as everything else, and never something to put behind a tunnel. There is no
67
+ hosted setup and there should not be.
68
+
69
+ The App manifest hand-off runs on the portal's own port rather than a second one, which changes
70
+ where the callback lands and nothing else: the one-time code is still exchanged server-side, and
71
+ the private key is still held in memory and written straight to a repo secret without touching
72
+ disk. A hand-off is keyed by a random state that GitHub echoes back, held in memory only, and a
73
+ callback whose state is unknown is refused.
74
+
75
+ ## The portal
76
+
77
+ It can write to GitHub, so it is worth being precise.
78
+
79
+ It binds to `127.0.0.1`. A write needs a `POST`, an `x-roster` header (which forces a CORS
80
+ preflight that fails from any other origin), and an `Origin` that is either absent or
81
+ localhost. Reads are served to anything that can reach the port.
82
+
83
+ Two paths take attacker-controlled input to the disk or a command line, and both are
84
+ constrained rather than sanitised:
85
+
86
+ - `/api/file` resolves the path and refuses anything outside the workspace root.
87
+ - `/api/diff` requires the directory to be a known staff repo and the sha to look like a sha.
88
+ - `/api/doc` requires the page to be one the listing offered.
89
+
90
+ `--host` overrides the bind address and prints a warning. It exists for people who know what
91
+ they are doing on a network they control. See [hosting the portal](hosting.md).
92
+
93
+ ## Trust in a prompt
94
+
95
+ Everything composed into a prompt is content you or your agents wrote: `org/`, the charter, the
96
+ memory index. A `mention` run additionally carries the text of a comment.
97
+
98
+ **On a private tracker that is you.** On the public product repo it is not, which is exactly
99
+ why the product lane is split: a mention on a public PR is caught by a forwarder in that repo
100
+ which does no work of its own, and dispatches to the private lane. The run that prints a
101
+ charter and a chain of reasoning happens where the logs are not world-readable.
102
+
103
+ The forwarder has an author gate, and it is load-bearing: anyone can comment on a public PR,
104
+ and a run started from a comment executes with repository secrets. Do not relax it.
105
+
106
+ ## The loop guard
107
+
108
+ The mention callers gate on `github.event.sender.login`, the account that **performed** the
109
+ action, not the author of the thing acted on.
110
+
111
+ That distinction is load-bearing. The pinned status issue is opened by the human and rewritten
112
+ by the agent on every run. An author check would let the agent's own edit wake another run,
113
+ which would edit it again.
114
+
115
+ ## What is not defended against
116
+
117
+ - **A compromised model provider account.** The agent credential can spend money and the agent
118
+ can write to your repos.
119
+ - **A malicious prompt injection from a public comment**, beyond the split above. A staff
120
+ member reading a public PR comment is reading untrusted text.
121
+ - **Someone with write access to the ops repo.** They can change what every staff member is
122
+ told to do, on the next run. Treat `roster-ops` as production.