@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.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/cli.js +5679 -0
- package/docs/README.md +99 -0
- package/docs/agents.md +163 -0
- package/docs/architecture.md +121 -0
- package/docs/commands.md +223 -0
- package/docs/concepts.md +112 -0
- package/docs/cost.md +61 -0
- package/docs/developing.md +147 -0
- package/docs/doctor-codes.md +74 -0
- package/docs/export.md +113 -0
- package/docs/extending.md +97 -0
- package/docs/getting-started.md +134 -0
- package/docs/hosting.md +72 -0
- package/docs/manual-steps.md +163 -0
- package/docs/memory.md +71 -0
- package/docs/org-yaml.md +143 -0
- package/docs/portal.md +342 -0
- package/docs/prompts.md +133 -0
- package/docs/security.md +122 -0
- package/docs/session-workflow.md +112 -0
- package/docs/staff-yaml.md +163 -0
- package/docs/troubleshooting.md +189 -0
- package/docs/upgrading.md +83 -0
- package/docs/writing-a-charter.md +83 -0
- package/package.json +60 -0
- package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +33 -0
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +65 -0
- package/templates/brain/.github/workflows/%%STAFF%%-pr-mention.yaml +50 -0
- package/templates/brain/CHARTER.md +49 -0
- package/templates/brain/README.md +18 -0
- package/templates/brain/drafts/README.md +7 -0
- package/templates/brain/log/decisions.md +6 -0
- package/templates/brain/memory/INDEX.md +28 -0
- package/templates/brain/staff.yaml +44 -0
- package/templates/brain/strategy/README.md +7 -0
- package/templates/briefs/amend.md +60 -0
- package/templates/briefs/charter.md +47 -0
- package/templates/briefs/discover.md +61 -0
- package/templates/briefs/voice.md +53 -0
- package/templates/ops/.github/workflows/session.yaml +333 -0
- package/templates/ops/agents.mjs +143 -0
- package/templates/ops/compose.mjs +333 -0
- package/templates/ops/org/guardrails.md +14 -0
- package/templates/ops/org/operating.md +82 -0
- package/templates/ops/org/voice.md +40 -0
- package/templates/ops/prompts/_identity.md +14 -0
- package/templates/ops/prompts/_paths.md +15 -0
- package/templates/ops/prompts/daily.md +82 -0
- package/templates/ops/prompts/mention.md +53 -0
- package/templates/ops/prompts/pr-mention.md +57 -0
- package/templates/ops/runner-plan.mjs +65 -0
- package/templates/portal/css/base.css +104 -0
- package/templates/portal/css/brain.css +106 -0
- package/templates/portal/css/diff.css +28 -0
- package/templates/portal/css/graph.css +34 -0
- package/templates/portal/css/health.css +41 -0
- package/templates/portal/css/inbox.css +79 -0
- package/templates/portal/css/layout.css +98 -0
- package/templates/portal/css/markdown.css +54 -0
- package/templates/portal/css/setup.css +106 -0
- package/templates/portal/index.html +55 -0
- package/templates/portal/js/api.js +74 -0
- package/templates/portal/js/app.js +282 -0
- package/templates/portal/js/dialog.js +70 -0
- package/templates/portal/js/dom.js +106 -0
- package/templates/portal/js/icons.js +94 -0
- package/templates/portal/js/md.js +386 -0
- package/templates/portal/js/refresh.js +59 -0
- package/templates/portal/js/router.js +20 -0
- package/templates/portal/js/state.js +160 -0
- package/templates/portal/js/textdiff.js +96 -0
- package/templates/portal/js/views/app.js +128 -0
- package/templates/portal/js/views/brain.js +260 -0
- package/templates/portal/js/views/changed.js +157 -0
- package/templates/portal/js/views/checklist.js +87 -0
- package/templates/portal/js/views/docs.js +84 -0
- package/templates/portal/js/views/files.js +95 -0
- package/templates/portal/js/views/graph.js +436 -0
- package/templates/portal/js/views/health.js +158 -0
- package/templates/portal/js/views/inbox.js +549 -0
- package/templates/portal/js/views/memory.js +135 -0
- package/templates/portal/js/views/org.js +175 -0
- package/templates/portal/js/views/paste.js +142 -0
- package/templates/portal/js/views/prompt.js +412 -0
- package/templates/portal/js/views/repos.js +92 -0
- package/templates/portal/js/views/setup.js +344 -0
- 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
|
+
```
|
package/docs/prompts.md
ADDED
|
@@ -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.
|
package/docs/security.md
ADDED
|
@@ -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.
|