@nanocollective/roster 0.1.0-alpha.2 → 0.1.0-alpha.21

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 (96) hide show
  1. package/README.md +70 -84
  2. package/dist/cli.js +4775 -2495
  3. package/docs/README.md +19 -11
  4. package/docs/agents.md +328 -13
  5. package/docs/architecture.md +13 -5
  6. package/docs/charters/cmo.md +69 -0
  7. package/docs/charters/cto.md +71 -0
  8. package/docs/charters/support.md +60 -0
  9. package/docs/commands.md +95 -11
  10. package/docs/concepts.md +64 -12
  11. package/docs/cost.md +39 -3
  12. package/docs/developing.md +16 -21
  13. package/docs/doctor-codes.md +21 -6
  14. package/docs/export.md +2 -1
  15. package/docs/extending.md +13 -4
  16. package/docs/getting-started.md +121 -80
  17. package/docs/images/brain.jpg +0 -0
  18. package/docs/images/org.jpg +0 -0
  19. package/docs/images/prompt.jpg +0 -0
  20. package/docs/images/setup-org.jpg +0 -0
  21. package/docs/images/setup-plan.jpg +0 -0
  22. package/docs/images/staff.jpg +0 -0
  23. package/docs/manual-steps.md +94 -101
  24. package/docs/memory.md +29 -8
  25. package/docs/org-yaml.md +76 -11
  26. package/docs/portal.md +261 -47
  27. package/docs/prompts.md +77 -11
  28. package/docs/security.md +51 -7
  29. package/docs/session-workflow.md +51 -21
  30. package/docs/staff-yaml.md +16 -7
  31. package/docs/troubleshooting.md +23 -20
  32. package/docs/upgrading.md +9 -3
  33. package/docs/writing-a-charter.md +33 -17
  34. package/package.json +1 -1
  35. package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +7 -0
  36. package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +16 -4
  37. package/templates/brain/CHARTER.md +3 -3
  38. package/templates/brain/README.md +1 -0
  39. package/templates/brain/log/decisions.md +3 -0
  40. package/templates/brain/staff.yaml +0 -1
  41. package/templates/brain/strategy/ideas.md +7 -0
  42. package/templates/ops/.github/workflows/session.yaml +117 -40
  43. package/templates/ops/agents.mjs +127 -8
  44. package/templates/ops/compose.mjs +77 -7
  45. package/templates/ops/inflight.mjs +157 -0
  46. package/templates/ops/org/operating.md +21 -7
  47. package/templates/ops/org/voice.md +9 -0
  48. package/templates/ops/prompts/_identity.md +8 -1
  49. package/templates/ops/prompts/_inflight.md +14 -0
  50. package/templates/ops/prompts/_paths.md +2 -1
  51. package/templates/ops/prompts/daily.md +16 -7
  52. package/templates/ops/prompts/mention.md +18 -2
  53. package/templates/ops/run-record.mjs +144 -0
  54. package/templates/portal/css/base.css +238 -64
  55. package/templates/portal/css/brain.css +30 -20
  56. package/templates/portal/css/diff.css +15 -10
  57. package/templates/portal/css/graph.css +12 -7
  58. package/templates/portal/css/health.css +32 -11
  59. package/templates/portal/css/inbox.css +117 -14
  60. package/templates/portal/css/layout.css +93 -41
  61. package/templates/portal/css/markdown.css +57 -15
  62. package/templates/portal/css/runs.css +13 -0
  63. package/templates/portal/css/setup.css +83 -39
  64. package/templates/portal/index.html +24 -3
  65. package/templates/portal/js/api.js +65 -4
  66. package/templates/portal/js/app.js +112 -12
  67. package/templates/portal/js/dialog.js +94 -4
  68. package/templates/portal/js/dom.js +25 -0
  69. package/templates/portal/js/icons.js +8 -1
  70. package/templates/portal/js/lightbox.js +273 -0
  71. package/templates/portal/js/md.js +23 -6
  72. package/templates/portal/js/mdedit.js +84 -0
  73. package/templates/portal/js/mention.js +264 -0
  74. package/templates/portal/js/refresh.js +136 -6
  75. package/templates/portal/js/state.js +55 -8
  76. package/templates/portal/js/views/app.js +23 -5
  77. package/templates/portal/js/views/checklist.js +29 -10
  78. package/templates/portal/js/views/credential.js +98 -0
  79. package/templates/portal/js/views/docs.js +94 -4
  80. package/templates/portal/js/views/files.js +58 -14
  81. package/templates/portal/js/views/graph.js +1 -1
  82. package/templates/portal/js/views/health.js +178 -37
  83. package/templates/portal/js/views/inbox.js +938 -98
  84. package/templates/portal/js/views/memory.js +16 -1
  85. package/templates/portal/js/views/org.js +124 -104
  86. package/templates/portal/js/views/orgedit.js +213 -0
  87. package/templates/portal/js/views/paste.js +33 -7
  88. package/templates/portal/js/views/prompt.js +61 -67
  89. package/templates/portal/js/views/repos.js +20 -15
  90. package/templates/portal/js/views/runonce.js +94 -0
  91. package/templates/portal/js/views/runs.js +165 -0
  92. package/templates/portal/js/views/setup.js +311 -83
  93. package/templates/portal/js/views/staff.js +139 -22
  94. package/templates/portal/js/yaml.js +134 -0
  95. package/templates/brain/.github/workflows/%%STAFF%%-pr-mention.yaml +0 -50
  96. 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
- d.append(el("div", { className: "row", style: "margin-top:10px" }, [write, app, go, status]));
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. Everything else is copied from whoever is already " +
105
- "here, because app slugs carry a house naming scheme and the public identity is " +
106
- "genuinely shared.",
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 = field("schedule", "0 9 * * 1-5", "defaults to a slot clear of everyone else");
114
- for (const f of [handle, name, dir, schedule]) box.append(f.row);
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
- if (f.input.value.trim()) params.set(key, f.input.value.trim());
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,29 @@ 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.dir + "/staff.yaml gains a peer entry and a " + p.label + " label"));
204
+ box.append(line("ok", p.brain + " gains a " + p.label + " label"));
165
205
  }
166
- box.append(line("ok", "org.yaml gains a staff entry and a repo entry"));
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
- if (plan.secrets?.length) {
169
- box.append(el("div", { className: "planhead", textContent: "You will still have to" }));
170
- box.append(
171
- line("todo", "create the GitHub App: roster app " + s.handle),
172
- line("todo", "put " + plan.secrets.join(", ") + " on the new repo"),
173
- );
174
- }
175
- for (const w of plan.warnings ?? []) box.append(line("warn", w));
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("todo", "Create their GitHub App, then install it on the repos it opens with ticked"),
223
+ line("todo", "Write the charter"),
224
+ line("todo", "Run once now, to check everything works"),
225
+ );
226
+ for (const w of plan.warnings ?? []) box.append(line("warn", S.data.staff.length ? w : portalWords(w)));
176
227
 
177
228
  const status = el("span", { className: "meta" });
178
229
  const go = el("button", { className: "ghbtn primary", textContent: "Hire " + s.handle });
@@ -183,7 +234,7 @@ export function viewStaff(m) {
183
234
  handle: s.handle,
184
235
  flags: Object.fromEntries(params),
185
236
  },
186
- "Create " + s.brain + " and wire it up?\n\nThis creates a repository on GitHub.",
237
+ { title: "Create " + s.brain + " and wire it up?", hint: "This creates a repository on GitHub.", confirm: "Hire " + s.handle },
187
238
  go,
188
239
  status,
189
240
  box,
@@ -238,7 +289,7 @@ export function viewStaff(m) {
238
289
  go.onclick = () =>
239
290
  apply(
240
291
  { action: "retire", handle: plan.handle },
241
- "Retire " + plan.name + "?\n\n" + (plan.brain ?? plan.dir) + " is not touched.",
292
+ { title: "Retire " + plan.name + "?", hint: (plan.brain ?? plan.dir) + " is not touched.", confirm: "Retire " + plan.handle },
242
293
  go,
243
294
  status,
244
295
  box,
@@ -250,7 +301,7 @@ export function viewStaff(m) {
250
301
  /* -------------------------------- apply -------------------------------- */
251
302
 
252
303
  async function apply(payload, ask, btn, status, box) {
253
- if (!confirm(ask)) return;
304
+ if (!(await askYes(ask))) return;
254
305
  btn.disabled = true;
255
306
  status.textContent = "working…";
256
307
  status.className = "meta";
@@ -269,6 +320,14 @@ export function viewStaff(m) {
269
320
  }
270
321
  }
271
322
 
323
+ /* The plan's warnings are the CLI's, and name its flags. Here the same two options are fields
324
+ on the form above, and a flag the page has no box for reads as a dead end. */
325
+ function portalWords(warning) {
326
+ return warning
327
+ .replace(/pass --public-app\b/, "fill in public app above")
328
+ .replace(/pass --app\b/, "fill in app above");
329
+ }
330
+
272
331
  function field(label, placeholder, hint) {
273
332
  const input = el("input", { type: "search", placeholder, value: "", style: "width:100%" });
274
333
  input.dataset.field = label;
@@ -281,6 +340,64 @@ function field(label, placeholder, hint) {
281
340
  return { row, input };
282
341
  }
283
342
 
343
+ /**
344
+ * When the daily run happens, as a time and a set of days rather than as cron.
345
+ *
346
+ * The value on the wire is still a cron expression, because that is what goes in the workflow
347
+ * and what `roster hire` takes. But nobody hiring their first staff member knows that
348
+ * `0 9 * * 1-5` is nine in the morning on weekdays, and a field that demands it is a field
349
+ * that gets a wrong answer or an empty one.
350
+ *
351
+ * UTC, said out loud, with the local equivalent beside it: GitHub schedules in UTC, and an
352
+ * agent that starts an hour off twice a year is worse than one you had to think about once.
353
+ */
354
+ function scheduleField() {
355
+ const time = el("input", { type: "time", value: "", step: "60", style: "width:130px" });
356
+ const days = el("select");
357
+ days.append(
358
+ el("option", { value: "1-5", textContent: "Weekdays" }),
359
+ el("option", { value: "*", textContent: "Every day" }),
360
+ el("option", { value: "1", textContent: "Mondays" }),
361
+ el("option", { value: "1,3,5", textContent: "Mon, Wed, Fri" }),
362
+ );
363
+ // Said rather than relying on "the first option is selected", which is true of a rendered
364
+ // <select> and not of one that has only been built.
365
+ days.value = "1-5";
366
+
367
+ const said = el("small");
368
+ const value = () => {
369
+ if (!time.value) return "";
370
+ const [h, m] = time.value.split(":");
371
+ return `${Number(m)} ${Number(h)} * * ${days.value}`;
372
+ };
373
+ const say = () => {
374
+ const cron = value();
375
+ said.textContent = cron
376
+ ? cronText(cron) + local(time.value)
377
+ : "Leave it empty for a slot clear of everyone else.";
378
+ };
379
+ time.oninput = say;
380
+ days.onchange = say;
381
+ say();
382
+
383
+ const row = el("div", { className: "ffield" });
384
+ row.append(
385
+ el("label", { textContent: "schedule" }),
386
+ el("div", { className: "row" }, [time, days, el("span", { className: "meta", textContent: "UTC" })]),
387
+ said,
388
+ );
389
+ return { row, input: time, value };
390
+ }
391
+
392
+ /** What that UTC time is where the person reading it is, when the two differ. */
393
+ function local(hhmm) {
394
+ if (!hhmm) return "";
395
+ const [h, m] = hhmm.split(":").map(Number);
396
+ const at = new Date(Date.UTC(2026, 0, 5, h, m));
397
+ const here = at.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
398
+ return here === hhmm ? "" : " · " + here + " where you are";
399
+ }
400
+
284
401
  const MARK = { ok: "check", stop: "closed", keep: "issue-open", todo: "dash", warn: "label" };
285
402
 
286
403
  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(/&lt;/g, "<")
41
+ .replace(/&gt;/g, ">")
42
+ .replace(/&quot;/g, '"')
43
+ .replace(/&amp;/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.