@nanocollective/roster 0.1.0-alpha.1 → 0.1.0-alpha.10
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/README.md +65 -84
- package/dist/cli.js +4687 -2495
- package/docs/README.md +19 -11
- package/docs/agents.md +328 -13
- package/docs/architecture.md +13 -5
- package/docs/charters/cmo.md +69 -0
- package/docs/charters/cto.md +71 -0
- package/docs/charters/support.md +60 -0
- package/docs/commands.md +90 -11
- package/docs/concepts.md +64 -12
- package/docs/cost.md +39 -3
- package/docs/developing.md +16 -21
- package/docs/doctor-codes.md +21 -6
- package/docs/export.md +2 -1
- package/docs/extending.md +13 -4
- package/docs/getting-started.md +118 -80
- package/docs/images/brain.jpg +0 -0
- package/docs/images/org.jpg +0 -0
- package/docs/images/prompt.jpg +0 -0
- package/docs/images/setup-org.jpg +0 -0
- package/docs/images/setup-plan.jpg +0 -0
- package/docs/images/staff.jpg +0 -0
- package/docs/manual-steps.md +94 -101
- package/docs/memory.md +29 -8
- package/docs/org-yaml.md +74 -11
- package/docs/portal.md +261 -47
- package/docs/prompts.md +71 -11
- package/docs/security.md +43 -7
- package/docs/session-workflow.md +51 -21
- package/docs/staff-yaml.md +16 -7
- package/docs/troubleshooting.md +23 -20
- package/docs/upgrading.md +6 -0
- package/docs/writing-a-charter.md +33 -17
- package/package.json +1 -1
- package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +7 -0
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +16 -4
- package/templates/brain/CHARTER.md +3 -3
- package/templates/brain/README.md +1 -0
- package/templates/brain/log/decisions.md +3 -0
- package/templates/brain/staff.yaml +0 -1
- package/templates/brain/strategy/ideas.md +7 -0
- package/templates/ops/.github/workflows/session.yaml +117 -40
- package/templates/ops/agents.mjs +127 -8
- package/templates/ops/compose.mjs +77 -7
- package/templates/ops/inflight.mjs +157 -0
- package/templates/ops/org/operating.md +21 -7
- package/templates/ops/org/voice.md +9 -0
- package/templates/ops/prompts/_identity.md +8 -1
- package/templates/ops/prompts/_inflight.md +14 -0
- package/templates/ops/prompts/_paths.md +2 -1
- package/templates/ops/prompts/daily.md +16 -7
- package/templates/ops/prompts/mention.md +18 -2
- package/templates/ops/run-record.mjs +144 -0
- package/templates/portal/css/base.css +238 -64
- package/templates/portal/css/brain.css +30 -20
- package/templates/portal/css/diff.css +15 -10
- package/templates/portal/css/graph.css +12 -7
- package/templates/portal/css/health.css +31 -10
- package/templates/portal/css/inbox.css +117 -14
- package/templates/portal/css/layout.css +92 -41
- package/templates/portal/css/markdown.css +38 -15
- package/templates/portal/css/runs.css +13 -0
- package/templates/portal/css/setup.css +50 -37
- package/templates/portal/index.html +24 -3
- package/templates/portal/js/api.js +65 -4
- package/templates/portal/js/app.js +112 -12
- package/templates/portal/js/dialog.js +94 -4
- package/templates/portal/js/dom.js +25 -0
- package/templates/portal/js/icons.js +8 -1
- package/templates/portal/js/lightbox.js +273 -0
- package/templates/portal/js/md.js +23 -6
- package/templates/portal/js/mention.js +264 -0
- package/templates/portal/js/refresh.js +136 -6
- package/templates/portal/js/state.js +55 -8
- package/templates/portal/js/views/app.js +23 -5
- package/templates/portal/js/views/checklist.js +20 -7
- package/templates/portal/js/views/credential.js +93 -0
- package/templates/portal/js/views/docs.js +94 -4
- package/templates/portal/js/views/files.js +58 -14
- package/templates/portal/js/views/graph.js +1 -1
- package/templates/portal/js/views/health.js +178 -37
- package/templates/portal/js/views/inbox.js +938 -98
- package/templates/portal/js/views/memory.js +16 -1
- package/templates/portal/js/views/org.js +161 -61
- package/templates/portal/js/views/paste.js +29 -0
- package/templates/portal/js/views/prompt.js +57 -66
- package/templates/portal/js/views/repos.js +12 -7
- package/templates/portal/js/views/runonce.js +94 -0
- package/templates/portal/js/views/runs.js +165 -0
- package/templates/portal/js/views/setup.js +302 -56
- package/templates/portal/js/views/staff.js +143 -22
- package/templates/portal/js/yaml.js +134 -0
- package/templates/brain/.github/workflows/%%STAFF%%-pr-mention.yaml +0 -50
- package/templates/ops/prompts/pr-mention.md +0 -57
|
@@ -10,12 +10,16 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { post } from "../api.js";
|
|
13
|
+
import { askYes } from "../dialog.js";
|
|
13
14
|
import { ago, el, esc } from "../dom.js";
|
|
14
15
|
import { icon } from "../icons.js";
|
|
15
16
|
import { refreshAll } from "../refresh.js";
|
|
16
17
|
import { S } from "../state.js";
|
|
17
18
|
import { appPanel } from "./app.js";
|
|
19
|
+
import { credentialPanel } from "./credential.js";
|
|
20
|
+
import { cronText } from "./health.js";
|
|
18
21
|
import { paste } from "./paste.js";
|
|
22
|
+
import { runOnce } from "./runonce.js";
|
|
19
23
|
|
|
20
24
|
export function viewStaff(m) {
|
|
21
25
|
m.append(el("h1", { textContent: "Staff" }));
|
|
@@ -87,31 +91,66 @@ export function viewStaff(m) {
|
|
|
87
91
|
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
88
92
|
};
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
/* The credential is once for the org, but the moment somebody looks for it is while setting
|
|
95
|
+
up one staff member, so it is reachable from each card. */
|
|
96
|
+
const cred = el("button", { className: "ghbtn", textContent: "Agent credential" });
|
|
97
|
+
cred.onclick = () => {
|
|
98
|
+
pane.replaceChildren(el("div", { className: "card" }, [credentialPanel()]));
|
|
99
|
+
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const run = el("button", { className: "ghbtn", textContent: "Run once now" });
|
|
103
|
+
run.onclick = () => {
|
|
104
|
+
pane.replaceChildren(
|
|
105
|
+
el("div", { className: "card" }, [
|
|
106
|
+
runOnce({ staff: s.handle, name: s.name, onDone: (ok) => ok && refreshAll(true) }),
|
|
107
|
+
]),
|
|
108
|
+
);
|
|
109
|
+
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
d.append(
|
|
113
|
+
el("div", { className: "row", style: "margin-top:10px" }, [write, app, cred, run, go, status]),
|
|
114
|
+
);
|
|
91
115
|
return d;
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
/* ------------------------------- hiring -------------------------------- */
|
|
95
119
|
|
|
96
120
|
function hireForm(host) {
|
|
121
|
+
/* App names are copied from whoever is already here. The first hire has nobody to copy
|
|
122
|
+
from, and the CLI asks for --app and --public-app at that point; these are those two. */
|
|
123
|
+
const first = !S.data.staff.length;
|
|
97
124
|
const box = el("div", { className: "card" });
|
|
98
125
|
box.append(
|
|
99
126
|
el("h3", { textContent: "Hire someone" }),
|
|
100
127
|
el("p", {
|
|
101
128
|
className: "sub",
|
|
102
129
|
style: "margin-bottom:14px",
|
|
103
|
-
textContent:
|
|
104
|
-
"Only the handle is required.
|
|
105
|
-
|
|
106
|
-
"
|
|
130
|
+
textContent: first
|
|
131
|
+
? "Only the handle is required. This is the first hire, so there is nobody to copy " +
|
|
132
|
+
"App names from: name them here, and later hires follow the pattern."
|
|
133
|
+
: "Only the handle is required. Everything else is copied from whoever is already " +
|
|
134
|
+
"here, because app slugs carry a house naming scheme and the public identity is " +
|
|
135
|
+
"genuinely shared.",
|
|
107
136
|
}),
|
|
108
137
|
);
|
|
109
138
|
|
|
110
139
|
const handle = field("handle", "cfo", "lowercase, digits and dashes");
|
|
111
140
|
const name = field("name", "Chief Financial Officer", "defaults to the handle, uppercased");
|
|
112
141
|
const dir = field("dir", "finance", "directory and repo name; defaults to the handle");
|
|
113
|
-
const schedule =
|
|
114
|
-
|
|
142
|
+
const schedule = scheduleField();
|
|
143
|
+
const fields = [handle, name, dir, schedule];
|
|
144
|
+
const apps = [];
|
|
145
|
+
if (first) {
|
|
146
|
+
const org = S.data.org;
|
|
147
|
+
apps.push(
|
|
148
|
+
["app", field("app", org + "-cfo", "this staff member's GitHub App. Names are unique across GitHub, so prefix it")],
|
|
149
|
+
["publicApp", field("public app", org + "-robot", "the shared identity for public product repos. Optional when they are all private")],
|
|
150
|
+
);
|
|
151
|
+
fields.push(...apps.map(([, f]) => f));
|
|
152
|
+
}
|
|
153
|
+
for (const f of fields) box.append(f.row);
|
|
115
154
|
|
|
116
155
|
const status = el("span", { className: "meta" });
|
|
117
156
|
const plan = el("button", { className: "ghbtn primary", textContent: "Show the plan" });
|
|
@@ -133,8 +172,9 @@ export function viewStaff(m) {
|
|
|
133
172
|
status.className = "meta";
|
|
134
173
|
out.replaceChildren();
|
|
135
174
|
const params = new URLSearchParams({ handle: handle.input.value.trim() });
|
|
136
|
-
for (const [key, f] of [["name", name], ["dir", dir], ["schedule", schedule]]) {
|
|
137
|
-
|
|
175
|
+
for (const [key, f] of [["name", name], ["dir", dir], ["schedule", schedule], ...apps]) {
|
|
176
|
+
const value = (f.value ? f.value() : f.input.value).trim();
|
|
177
|
+
if (value) params.set(key, value);
|
|
138
178
|
}
|
|
139
179
|
try {
|
|
140
180
|
const data = await (await fetch("/api/staff/plan?" + params, { cache: "no-store" })).json();
|
|
@@ -161,18 +201,33 @@ export function viewStaff(m) {
|
|
|
161
201
|
line("ok", "runs at " + s.schedule + ", on " + s.model),
|
|
162
202
|
);
|
|
163
203
|
for (const p of plan.peers) {
|
|
164
|
-
box.append(line("ok", p.
|
|
204
|
+
box.append(line("ok", p.brain + " gains a " + p.label + " label"));
|
|
165
205
|
}
|
|
166
|
-
|
|
206
|
+
/* Commits into repos that already exist, as you. Listed because they are writes to
|
|
207
|
+
somebody else's repo, and a plan that hid them would be the silent kind. */
|
|
208
|
+
box.append(el("div", { className: "planhead", textContent: "Commits and pushes, as you" }));
|
|
209
|
+
for (const c of plan.commits ?? []) box.append(line("ok", c.repo + ": " + c.file + ", " + c.why));
|
|
167
210
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
line("
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
211
|
+
const cred = s.agentSecret ?? "the agent credential";
|
|
212
|
+
box.append(
|
|
213
|
+
plan.orgSecret?.visibility === "selected"
|
|
214
|
+
? line("ok", s.brain + " is added to the repos that can read the org secret " + cred)
|
|
215
|
+
: plan.orgSecret
|
|
216
|
+
? line("ok", "the org secret " + cred + " already reaches " + plan.orgSecret.visibility + " repos")
|
|
217
|
+
: line("warn", "no org secret " + cred + " yet: store it once with Agent credential, after this"),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
box.append(el("div", { className: "planhead", textContent: "Then, on the new card" }));
|
|
221
|
+
box.append(
|
|
222
|
+
line(
|
|
223
|
+
"todo",
|
|
224
|
+
"GitHub App, or roster app " + s.handle + " --apply: creates it, sets its secrets, and " +
|
|
225
|
+
"opens the install page with the repos already ticked",
|
|
226
|
+
),
|
|
227
|
+
line("todo", "Write the charter"),
|
|
228
|
+
line("todo", "Run once now, or roster run " + s.handle + " --apply: the run that proves the wiring"),
|
|
229
|
+
);
|
|
230
|
+
for (const w of plan.warnings ?? []) box.append(line("warn", S.data.staff.length ? w : portalWords(w)));
|
|
176
231
|
|
|
177
232
|
const status = el("span", { className: "meta" });
|
|
178
233
|
const go = el("button", { className: "ghbtn primary", textContent: "Hire " + s.handle });
|
|
@@ -183,7 +238,7 @@ export function viewStaff(m) {
|
|
|
183
238
|
handle: s.handle,
|
|
184
239
|
flags: Object.fromEntries(params),
|
|
185
240
|
},
|
|
186
|
-
"Create " + s.brain + " and wire it up
|
|
241
|
+
{ title: "Create " + s.brain + " and wire it up?", hint: "This creates a repository on GitHub.", confirm: "Hire " + s.handle },
|
|
187
242
|
go,
|
|
188
243
|
status,
|
|
189
244
|
box,
|
|
@@ -238,7 +293,7 @@ export function viewStaff(m) {
|
|
|
238
293
|
go.onclick = () =>
|
|
239
294
|
apply(
|
|
240
295
|
{ action: "retire", handle: plan.handle },
|
|
241
|
-
"Retire " + plan.name + "
|
|
296
|
+
{ title: "Retire " + plan.name + "?", hint: (plan.brain ?? plan.dir) + " is not touched.", confirm: "Retire " + plan.handle },
|
|
242
297
|
go,
|
|
243
298
|
status,
|
|
244
299
|
box,
|
|
@@ -250,7 +305,7 @@ export function viewStaff(m) {
|
|
|
250
305
|
/* -------------------------------- apply -------------------------------- */
|
|
251
306
|
|
|
252
307
|
async function apply(payload, ask, btn, status, box) {
|
|
253
|
-
if (!
|
|
308
|
+
if (!(await askYes(ask))) return;
|
|
254
309
|
btn.disabled = true;
|
|
255
310
|
status.textContent = "working…";
|
|
256
311
|
status.className = "meta";
|
|
@@ -269,6 +324,14 @@ export function viewStaff(m) {
|
|
|
269
324
|
}
|
|
270
325
|
}
|
|
271
326
|
|
|
327
|
+
/* The plan's warnings are the CLI's, and name its flags. Here the same two options are fields
|
|
328
|
+
on the form above, and a flag the page has no box for reads as a dead end. */
|
|
329
|
+
function portalWords(warning) {
|
|
330
|
+
return warning
|
|
331
|
+
.replace(/pass --public-app\b/, "fill in public app above")
|
|
332
|
+
.replace(/pass --app\b/, "fill in app above");
|
|
333
|
+
}
|
|
334
|
+
|
|
272
335
|
function field(label, placeholder, hint) {
|
|
273
336
|
const input = el("input", { type: "search", placeholder, value: "", style: "width:100%" });
|
|
274
337
|
input.dataset.field = label;
|
|
@@ -281,6 +344,64 @@ function field(label, placeholder, hint) {
|
|
|
281
344
|
return { row, input };
|
|
282
345
|
}
|
|
283
346
|
|
|
347
|
+
/**
|
|
348
|
+
* When the daily run happens, as a time and a set of days rather than as cron.
|
|
349
|
+
*
|
|
350
|
+
* The value on the wire is still a cron expression, because that is what goes in the workflow
|
|
351
|
+
* and what `roster hire` takes. But nobody hiring their first staff member knows that
|
|
352
|
+
* `0 9 * * 1-5` is nine in the morning on weekdays, and a field that demands it is a field
|
|
353
|
+
* that gets a wrong answer or an empty one.
|
|
354
|
+
*
|
|
355
|
+
* UTC, said out loud, with the local equivalent beside it: GitHub schedules in UTC, and an
|
|
356
|
+
* agent that starts an hour off twice a year is worse than one you had to think about once.
|
|
357
|
+
*/
|
|
358
|
+
function scheduleField() {
|
|
359
|
+
const time = el("input", { type: "time", value: "", step: "60", style: "width:130px" });
|
|
360
|
+
const days = el("select");
|
|
361
|
+
days.append(
|
|
362
|
+
el("option", { value: "1-5", textContent: "Weekdays" }),
|
|
363
|
+
el("option", { value: "*", textContent: "Every day" }),
|
|
364
|
+
el("option", { value: "1", textContent: "Mondays" }),
|
|
365
|
+
el("option", { value: "1,3,5", textContent: "Mon, Wed, Fri" }),
|
|
366
|
+
);
|
|
367
|
+
// Said rather than relying on "the first option is selected", which is true of a rendered
|
|
368
|
+
// <select> and not of one that has only been built.
|
|
369
|
+
days.value = "1-5";
|
|
370
|
+
|
|
371
|
+
const said = el("small");
|
|
372
|
+
const value = () => {
|
|
373
|
+
if (!time.value) return "";
|
|
374
|
+
const [h, m] = time.value.split(":");
|
|
375
|
+
return `${Number(m)} ${Number(h)} * * ${days.value}`;
|
|
376
|
+
};
|
|
377
|
+
const say = () => {
|
|
378
|
+
const cron = value();
|
|
379
|
+
said.textContent = cron
|
|
380
|
+
? cronText(cron) + local(time.value)
|
|
381
|
+
: "Leave it empty for a slot clear of everyone else.";
|
|
382
|
+
};
|
|
383
|
+
time.oninput = say;
|
|
384
|
+
days.onchange = say;
|
|
385
|
+
say();
|
|
386
|
+
|
|
387
|
+
const row = el("div", { className: "ffield" });
|
|
388
|
+
row.append(
|
|
389
|
+
el("label", { textContent: "schedule" }),
|
|
390
|
+
el("div", { className: "row" }, [time, days, el("span", { className: "meta", textContent: "UTC" })]),
|
|
391
|
+
said,
|
|
392
|
+
);
|
|
393
|
+
return { row, input: time, value };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** What that UTC time is where the person reading it is, when the two differ. */
|
|
397
|
+
function local(hhmm) {
|
|
398
|
+
if (!hhmm) return "";
|
|
399
|
+
const [h, m] = hhmm.split(":").map(Number);
|
|
400
|
+
const at = new Date(Date.UTC(2026, 0, 5, h, m));
|
|
401
|
+
const here = at.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
|
402
|
+
return here === hhmm ? "" : " · " + here + " where you are";
|
|
403
|
+
}
|
|
404
|
+
|
|
284
405
|
const MARK = { ok: "check", stop: "closed", keep: "issue-open", todo: "dash", warn: "label" };
|
|
285
406
|
|
|
286
407
|
function line(kind, text) {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/* Colour for YAML.
|
|
2
|
+
*
|
|
3
|
+
* `org.yaml` and `staff.yaml` are the two files here a person actually reads closely, and they
|
|
4
|
+
* were served as one flat grey wall: the comments, the keys and the values all the same weight,
|
|
5
|
+
* so finding `mention_timeout_minutes` meant reading every line. Structure is the thing YAML has
|
|
6
|
+
* and prose does not, and showing it costs about eighty lines.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately not a parser. It is a per-line tokeniser that knows comments, keys, quoted
|
|
9
|
+
* strings, numbers and the three keywords — enough to read by, and it cannot fail on a file it
|
|
10
|
+
* does not understand, because anything it does not recognise is left as plain text.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { el, esc } from "./dom.js";
|
|
14
|
+
|
|
15
|
+
/** A `<pre>` holding the highlighted source. */
|
|
16
|
+
export function yamlPre(text, className = "code yaml") {
|
|
17
|
+
const pre = el("pre", { className });
|
|
18
|
+
pre.innerHTML = yamlHTML(text);
|
|
19
|
+
return pre;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Escaped, highlighted HTML. Takes raw text: it does its own escaping. */
|
|
23
|
+
export function yamlHTML(text) {
|
|
24
|
+
return String(text ?? "")
|
|
25
|
+
.split("\n")
|
|
26
|
+
.map(colourLine)
|
|
27
|
+
.join("\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The same, for text that is already HTML-escaped.
|
|
32
|
+
*
|
|
33
|
+
* The markdown renderer escapes a fenced block before it knows what language it is, and
|
|
34
|
+
* un-escaping four entities to escape them again is exact and cheaper than restructuring
|
|
35
|
+
* the renderer around one case.
|
|
36
|
+
*/
|
|
37
|
+
export function yamlHTMLFromEscaped(escaped) {
|
|
38
|
+
return yamlHTML(
|
|
39
|
+
String(escaped ?? "")
|
|
40
|
+
.replace(/</g, "<")
|
|
41
|
+
.replace(/>/g, ">")
|
|
42
|
+
.replace(/"/g, '"')
|
|
43
|
+
.replace(/&/g, "&"),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const span = (cls, text) => '<span class="' + cls + '">' + esc(text) + "</span>";
|
|
48
|
+
|
|
49
|
+
function colourLine(raw) {
|
|
50
|
+
const [code, comment] = splitComment(raw);
|
|
51
|
+
return (code ? colourCode(code) : "") + (comment ? span("yc", comment) : "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Split a trailing comment off, without cutting a `#` that is inside a string.
|
|
56
|
+
*
|
|
57
|
+
* `marker: will # the provenance tag` is a comment; `name: "a # b"` is not. YAML also only
|
|
58
|
+
* starts a comment at the beginning of a line or after whitespace, which is what keeps a
|
|
59
|
+
* `#hashtag` in a value intact.
|
|
60
|
+
*/
|
|
61
|
+
function splitComment(raw) {
|
|
62
|
+
let quote = null;
|
|
63
|
+
for (let i = 0; i < raw.length; i++) {
|
|
64
|
+
const c = raw[i];
|
|
65
|
+
if (quote) {
|
|
66
|
+
if (c === quote && raw[i - 1] !== "\\") quote = null;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === '"' || c === "'") {
|
|
70
|
+
quote = c;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (c === "#" && (i === 0 || /\s/.test(raw[i - 1]))) return [raw.slice(0, i), raw.slice(i)];
|
|
74
|
+
}
|
|
75
|
+
return [raw, ""];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const DOC = /^\s*(---|\.\.\.)\s*$/;
|
|
79
|
+
/** Indent, then any run of list dashes: ` - - name: x` is two levels of list. */
|
|
80
|
+
const LEAD = /^(\s*)((?:-\s+)*)/;
|
|
81
|
+
/** A key is everything up to the first colon that is followed by a space or the end of line. */
|
|
82
|
+
const KEY = /^([^\s"'#][^:]*?)(:)(\s|$)/;
|
|
83
|
+
|
|
84
|
+
function colourCode(code) {
|
|
85
|
+
if (DOC.test(code)) return span("yp", code);
|
|
86
|
+
|
|
87
|
+
const lead = LEAD.exec(code);
|
|
88
|
+
const indent = lead[1];
|
|
89
|
+
const dashes = lead[2];
|
|
90
|
+
let rest = code.slice(indent.length + dashes.length);
|
|
91
|
+
let out = indent + (dashes ? span("yd", dashes) : "");
|
|
92
|
+
|
|
93
|
+
/* A line that opens a flow collection has no key of its own: `- { handle: cto }` is a list
|
|
94
|
+
item whose keys are all inside the braces, and taking "{ handle" for a key marked the
|
|
95
|
+
brace as part of the name. */
|
|
96
|
+
const key = /^[{[]/.test(rest) ? null : KEY.exec(rest);
|
|
97
|
+
if (key) {
|
|
98
|
+
out += span("yk", key[1]) + span("yp", key[2]) + key[3];
|
|
99
|
+
rest = rest.slice(key[1].length + 1 + key[3].length);
|
|
100
|
+
}
|
|
101
|
+
return out + colourValue(rest);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Strings, inline-map keys, the three keywords, numbers, and the punctuation that holds a flow
|
|
105
|
+
collection together. Ordered: a quoted string wins over everything inside it. */
|
|
106
|
+
const TOKEN = new RegExp(
|
|
107
|
+
[
|
|
108
|
+
/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/, // 1 quoted string
|
|
109
|
+
/([A-Za-z_][\w.-]*)(\s*:)(?=\s|$)/, // 2,3 a key inside an inline map
|
|
110
|
+
/(\b(?:true|false|null|yes|no|on|off)\b|~)/, // 4 keywords
|
|
111
|
+
/(-?\d+(?:\.\d+)?\b)/, // 5 numbers
|
|
112
|
+
/([{}[\],])/, // 6 flow punctuation
|
|
113
|
+
]
|
|
114
|
+
.map((r) => r.source)
|
|
115
|
+
.join("|"),
|
|
116
|
+
"g",
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
function colourValue(value) {
|
|
120
|
+
if (!value) return "";
|
|
121
|
+
let out = "";
|
|
122
|
+
let at = 0;
|
|
123
|
+
TOKEN.lastIndex = 0;
|
|
124
|
+
for (let m = TOKEN.exec(value); m; m = TOKEN.exec(value)) {
|
|
125
|
+
out += esc(value.slice(at, m.index));
|
|
126
|
+
if (m[1]) out += span("ys", m[1]);
|
|
127
|
+
else if (m[2]) out += span("yk", m[2]) + span("yp", m[3]);
|
|
128
|
+
else if (m[4]) out += span("yb", m[4]);
|
|
129
|
+
else if (m[5]) out += span("yn", m[5]);
|
|
130
|
+
else out += span("yp", m[6]);
|
|
131
|
+
at = m.index + m[0].length;
|
|
132
|
+
}
|
|
133
|
+
return out + esc(value.slice(at));
|
|
134
|
+
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
name: %%STAFF_UPPER%% on a product PR
|
|
2
|
-
|
|
3
|
-
# Generated by roster. The body lives in %%OPS_REPO%%/.github/workflows/session.yaml.
|
|
4
|
-
#
|
|
5
|
-
# Amends an open PR on the product repo in response to a review comment, so the human can ask for
|
|
6
|
-
# changes where they are already reading the diff instead of opening a ticket and cross-referencing.
|
|
7
|
-
#
|
|
8
|
-
# Not triggered directly. A forwarder in the PUBLIC product repo catches the mention and dispatches
|
|
9
|
-
# here. The split exists because that repo is public: its Actions logs are world readable, so the
|
|
10
|
-
# run that prints a charter and a chain of reasoning has to happen in a repo only we can see.
|
|
11
|
-
#
|
|
12
|
-
# NOTE: %%TOKENS%% are filled by `roster hire`. GitHub's own ${{ }} expressions are left alone.
|
|
13
|
-
|
|
14
|
-
on:
|
|
15
|
-
repository_dispatch:
|
|
16
|
-
types: [pr-mention]
|
|
17
|
-
|
|
18
|
-
# Manual handle, for testing without leaving a comment on a public PR, and for re-running a
|
|
19
|
-
# request the dispatcher already consumed.
|
|
20
|
-
workflow_dispatch:
|
|
21
|
-
inputs:
|
|
22
|
-
pr:
|
|
23
|
-
description: "PR number on the product repo"
|
|
24
|
-
required: true
|
|
25
|
-
comment_id:
|
|
26
|
-
description: "id of the comment that triggered this"
|
|
27
|
-
required: true
|
|
28
|
-
|
|
29
|
-
# Per PR. Several review comments in a row are the normal case; different PRs never contend.
|
|
30
|
-
concurrency:
|
|
31
|
-
group: %%STAFF%%-pr-${{ github.event.inputs.pr || github.event.client_payload.pr }}
|
|
32
|
-
cancel-in-progress: false
|
|
33
|
-
|
|
34
|
-
jobs:
|
|
35
|
-
amend:
|
|
36
|
-
uses: %%OPS_REPO%%/.github/workflows/session.yaml@main
|
|
37
|
-
with:
|
|
38
|
-
staff: %%STAFF%%
|
|
39
|
-
kind: pr-mention
|
|
40
|
-
ops_repo: %%OPS_REPO%%
|
|
41
|
-
model: %%MODEL%%
|
|
42
|
-
timeout_minutes: %%PR_MENTION_TIMEOUT%%
|
|
43
|
-
pr_number: ${{ github.event.inputs.pr || github.event.client_payload.pr }}
|
|
44
|
-
comment_id: ${{ github.event.inputs.comment_id || github.event.client_payload.comment_id }}
|
|
45
|
-
secrets:
|
|
46
|
-
APP_ID: ${{ secrets.%%SECRET_PREFIX%%_APP_ID }}
|
|
47
|
-
APP_PRIVATE_KEY: ${{ secrets.%%SECRET_PREFIX%%_APP_PRIVATE_KEY }}
|
|
48
|
-
PUBLIC_APP_ID: ${{ secrets.%%PUBLIC_SECRET_PREFIX%%_APP_ID }}
|
|
49
|
-
PUBLIC_APP_PRIVATE_KEY: ${{ secrets.%%PUBLIC_SECRET_PREFIX%%_APP_PRIVATE_KEY }}
|
|
50
|
-
AGENT_TOKEN: ${{ secrets.%%AGENT_SECRET%% }}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
You are **{{staff.name}}** at {{org.name}}. {{human.name}} has asked for a change to an open pull
|
|
2
|
-
request, in a comment on the PR itself. They are not watching a terminal; **your reply on that PR is
|
|
3
|
-
the only thing they will see.**
|
|
4
|
-
|
|
5
|
-
**This is not a session.** No boot ritual, no handoff, no rewriting #{{staff.status_issue}}. Make the
|
|
6
|
-
change asked for, push it, reply, stop.
|
|
7
|
-
|
|
8
|
-
{{#if staff.public_bot}}
|
|
9
|
-
## You are anonymous over there
|
|
10
|
-
|
|
11
|
-
The PR is on a **public** repo. You post as `{{staff.public_bot}}`, an ordinary automation account.
|
|
12
|
-
**Never sign as {{staff.name}}, never mention a charter, a tracker, a scheduled run, or that there is
|
|
13
|
-
a role behind the account.** Write like a maintainer's bot: short, factual, no personality.
|
|
14
|
-
|
|
15
|
-
Every `gh` call against that repo must carry the public identity: `GH_TOKEN=$PUBLIC_TOKEN gh ...`.
|
|
16
|
-
The bare `GH_TOKEN` is your private-tracker identity and using it there would sign your name to a
|
|
17
|
-
public repo.
|
|
18
|
-
{{/if}}
|
|
19
|
-
|
|
20
|
-
{{> prompts/_paths.md}}
|
|
21
|
-
|
|
22
|
-
## The request
|
|
23
|
-
|
|
24
|
-
**PR #{{event.pr_number}}, comment `{{event.comment_id}}`.** Read it before touching anything: an
|
|
25
|
-
inline review comment carries `path`, `line` and `diff_hunk`, which say exactly which code is meant.
|
|
26
|
-
Do not guess from the prose.
|
|
27
|
-
|
|
28
|
-
## Do the work
|
|
29
|
-
|
|
30
|
-
1. **You are already on the PR branch** - the runner checked it out. Do not start a new one.
|
|
31
|
-
2. **If the PR is from a fork you cannot push to**, do not try. Reply with the exact diff to apply,
|
|
32
|
-
and say why you could not push it.
|
|
33
|
-
3. **Make only the change asked for.** A review comment is a narrow request. Fixing something else
|
|
34
|
-
makes the diff unreviewable and costs a second review. If you spot something real and separate,
|
|
35
|
-
mention it in the reply and leave the code alone.
|
|
36
|
-
4. **Run the full gate before pushing.** If it goes red on the requested change, push nothing and
|
|
37
|
-
reply with what broke.
|
|
38
|
-
5. **Pull with rebase before pushing** - there may be several comments, and another run may be on
|
|
39
|
-
the same branch. If the push is still rejected, pull and retry rather than forcing: a force push
|
|
40
|
-
on a branch being reviewed throws away their place in the diff.
|
|
41
|
-
6. **You cannot verify anything UI-shaped** - there is no browser and no device here. Say so plainly
|
|
42
|
-
rather than implying a green gate covered it.
|
|
43
|
-
|
|
44
|
-
**Do not merge, and do not push to the default branch.**
|
|
45
|
-
|
|
46
|
-
{{> org/guardrails.md}}
|
|
47
|
-
|
|
48
|
-
## Reply where they asked
|
|
49
|
-
|
|
50
|
-
Answer in the same place the request came from. **Do not @-mention them** - they are subscribed to
|
|
51
|
-
their own PR.
|
|
52
|
-
|
|
53
|
-
{{> org/voice.md}}
|
|
54
|
-
|
|
55
|
-
**For this reply specifically:** what changed, the SHA, gate status, what it did not cover.
|
|
56
|
-
**Nothing else.** No preamble, no restating the request, no headers, no sign-off, no narration of
|
|
57
|
-
the steps you took. If nothing changed, one line saying so and why.
|