@nanocollective/roster 0.1.0-alpha.6 → 0.1.0-alpha.8

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 (63) hide show
  1. package/README.md +65 -84
  2. package/dist/cli.js +4187 -2709
  3. package/docs/README.md +9 -6
  4. package/docs/agents.md +24 -20
  5. package/docs/architecture.md +13 -5
  6. package/docs/charters/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 +81 -5
  10. package/docs/concepts.md +48 -14
  11. package/docs/cost.md +36 -1
  12. package/docs/developing.md +16 -21
  13. package/docs/doctor-codes.md +8 -2
  14. package/docs/extending.md +2 -2
  15. package/docs/getting-started.md +110 -77
  16. package/docs/manual-steps.md +93 -123
  17. package/docs/memory.md +21 -3
  18. package/docs/org-yaml.md +37 -2
  19. package/docs/portal.md +83 -35
  20. package/docs/prompts.md +25 -4
  21. package/docs/security.md +29 -5
  22. package/docs/session-workflow.md +49 -17
  23. package/docs/staff-yaml.md +13 -2
  24. package/docs/troubleshooting.md +8 -8
  25. package/docs/upgrading.md +6 -0
  26. package/docs/writing-a-charter.md +15 -0
  27. package/package.json +1 -1
  28. package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +6 -0
  29. package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +6 -0
  30. package/templates/brain/CHARTER.md +3 -3
  31. package/templates/brain/README.md +1 -0
  32. package/templates/brain/log/decisions.md +3 -0
  33. package/templates/brain/strategy/ideas.md +7 -0
  34. package/templates/ops/.github/workflows/session.yaml +108 -14
  35. package/templates/ops/agents.mjs +7 -3
  36. package/templates/ops/compose.mjs +16 -3
  37. package/templates/ops/inflight.mjs +157 -0
  38. package/templates/ops/org/operating.md +21 -1
  39. package/templates/ops/org/voice.md +9 -0
  40. package/templates/ops/prompts/_inflight.md +14 -0
  41. package/templates/ops/prompts/_paths.md +2 -1
  42. package/templates/ops/prompts/daily.md +16 -7
  43. package/templates/ops/prompts/mention.md +2 -0
  44. package/templates/ops/run-record.mjs +144 -0
  45. package/templates/portal/css/runs.css +13 -0
  46. package/templates/portal/css/setup.css +2 -0
  47. package/templates/portal/index.html +12 -1
  48. package/templates/portal/js/api.js +32 -4
  49. package/templates/portal/js/app.js +42 -8
  50. package/templates/portal/js/dialog.js +47 -0
  51. package/templates/portal/js/state.js +3 -1
  52. package/templates/portal/js/views/app.js +23 -5
  53. package/templates/portal/js/views/credential.js +93 -0
  54. package/templates/portal/js/views/health.js +17 -4
  55. package/templates/portal/js/views/inbox.js +6 -2
  56. package/templates/portal/js/views/org.js +23 -3
  57. package/templates/portal/js/views/paste.js +29 -0
  58. package/templates/portal/js/views/prompt.js +7 -3
  59. package/templates/portal/js/views/repos.js +12 -7
  60. package/templates/portal/js/views/runonce.js +94 -0
  61. package/templates/portal/js/views/runs.js +165 -0
  62. package/templates/portal/js/views/setup.js +267 -45
  63. package/templates/portal/js/views/staff.js +81 -20
@@ -10,13 +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";
18
20
  import { cronText } from "./health.js";
19
21
  import { paste } from "./paste.js";
22
+ import { runOnce } from "./runonce.js";
20
23
 
21
24
  export function viewStaff(m) {
22
25
  m.append(el("h1", { textContent: "Staff" }));
@@ -88,23 +91,48 @@ export function viewStaff(m) {
88
91
  pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
89
92
  };
90
93
 
91
- 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
+ );
92
115
  return d;
93
116
  }
94
117
 
95
118
  /* ------------------------------- hiring -------------------------------- */
96
119
 
97
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;
98
124
  const box = el("div", { className: "card" });
99
125
  box.append(
100
126
  el("h3", { textContent: "Hire someone" }),
101
127
  el("p", {
102
128
  className: "sub",
103
129
  style: "margin-bottom:14px",
104
- textContent:
105
- "Only the handle is required. Everything else is copied from whoever is already " +
106
- "here, because app slugs carry a house naming scheme and the public identity is " +
107
- "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.",
108
136
  }),
109
137
  );
110
138
 
@@ -112,7 +140,17 @@ export function viewStaff(m) {
112
140
  const name = field("name", "Chief Financial Officer", "defaults to the handle, uppercased");
113
141
  const dir = field("dir", "finance", "directory and repo name; defaults to the handle");
114
142
  const schedule = scheduleField();
115
- for (const f of [handle, name, dir, schedule]) box.append(f.row);
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);
116
154
 
117
155
  const status = el("span", { className: "meta" });
118
156
  const plan = el("button", { className: "ghbtn primary", textContent: "Show the plan" });
@@ -134,7 +172,7 @@ export function viewStaff(m) {
134
172
  status.className = "meta";
135
173
  out.replaceChildren();
136
174
  const params = new URLSearchParams({ handle: handle.input.value.trim() });
137
- for (const [key, f] of [["name", name], ["dir", dir], ["schedule", schedule]]) {
175
+ for (const [key, f] of [["name", name], ["dir", dir], ["schedule", schedule], ...apps]) {
138
176
  const value = (f.value ? f.value() : f.input.value).trim();
139
177
  if (value) params.set(key, value);
140
178
  }
@@ -163,18 +201,33 @@ export function viewStaff(m) {
163
201
  line("ok", "runs at " + s.schedule + ", on " + s.model),
164
202
  );
165
203
  for (const p of plan.peers) {
166
- 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"));
167
205
  }
168
- 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));
169
210
 
170
- if (plan.secrets?.length) {
171
- box.append(el("div", { className: "planhead", textContent: "You will still have to" }));
172
- box.append(
173
- line("todo", "create the GitHub App: roster app " + s.handle),
174
- line("todo", "put " + plan.secrets.join(", ") + " on the new repo"),
175
- );
176
- }
177
- 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(
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)));
178
231
 
179
232
  const status = el("span", { className: "meta" });
180
233
  const go = el("button", { className: "ghbtn primary", textContent: "Hire " + s.handle });
@@ -185,7 +238,7 @@ export function viewStaff(m) {
185
238
  handle: s.handle,
186
239
  flags: Object.fromEntries(params),
187
240
  },
188
- "Create " + s.brain + " and wire it up?\n\nThis creates a repository on GitHub.",
241
+ { title: "Create " + s.brain + " and wire it up?", hint: "This creates a repository on GitHub.", confirm: "Hire " + s.handle },
189
242
  go,
190
243
  status,
191
244
  box,
@@ -240,7 +293,7 @@ export function viewStaff(m) {
240
293
  go.onclick = () =>
241
294
  apply(
242
295
  { action: "retire", handle: plan.handle },
243
- "Retire " + plan.name + "?\n\n" + (plan.brain ?? plan.dir) + " is not touched.",
296
+ { title: "Retire " + plan.name + "?", hint: (plan.brain ?? plan.dir) + " is not touched.", confirm: "Retire " + plan.handle },
244
297
  go,
245
298
  status,
246
299
  box,
@@ -252,7 +305,7 @@ export function viewStaff(m) {
252
305
  /* -------------------------------- apply -------------------------------- */
253
306
 
254
307
  async function apply(payload, ask, btn, status, box) {
255
- if (!confirm(ask)) return;
308
+ if (!(await askYes(ask))) return;
256
309
  btn.disabled = true;
257
310
  status.textContent = "working…";
258
311
  status.className = "meta";
@@ -271,6 +324,14 @@ export function viewStaff(m) {
271
324
  }
272
325
  }
273
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
+
274
335
  function field(label, placeholder, hint) {
275
336
  const input = el("input", { type: "search", placeholder, value: "", style: "width:100%" });
276
337
  input.dataset.field = label;