@nanocollective/roster 0.1.0-alpha.4 → 0.1.0-alpha.41

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 (97) hide show
  1. package/README.md +70 -84
  2. package/dist/cli.js +4562 -2744
  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/analyst.md +65 -0
  7. package/docs/charters/cmo.md +69 -0
  8. package/docs/charters/community.md +63 -0
  9. package/docs/charters/cto.md +71 -0
  10. package/docs/charters/designer.md +65 -0
  11. package/docs/charters/devops.md +65 -0
  12. package/docs/charters/pm.md +70 -0
  13. package/docs/charters/qa.md +65 -0
  14. package/docs/charters/support.md +60 -0
  15. package/docs/charters/writer.md +63 -0
  16. package/docs/commands.md +90 -5
  17. package/docs/concepts.md +48 -14
  18. package/docs/cost.md +36 -1
  19. package/docs/developing.md +16 -21
  20. package/docs/doctor-codes.md +8 -2
  21. package/docs/export.md +2 -0
  22. package/docs/extending.md +2 -2
  23. package/docs/getting-started.md +126 -77
  24. package/docs/images/brain.jpg +0 -0
  25. package/docs/images/org.jpg +0 -0
  26. package/docs/images/prompt.jpg +0 -0
  27. package/docs/images/setup-org.jpg +0 -0
  28. package/docs/images/setup-plan.jpg +0 -0
  29. package/docs/images/staff.jpg +0 -0
  30. package/docs/manual-steps.md +94 -123
  31. package/docs/memory.md +21 -3
  32. package/docs/org-yaml.md +39 -2
  33. package/docs/portal.md +119 -44
  34. package/docs/prompts.md +31 -4
  35. package/docs/security.md +37 -5
  36. package/docs/session-workflow.md +49 -17
  37. package/docs/staff-yaml.md +14 -2
  38. package/docs/troubleshooting.md +8 -8
  39. package/docs/upgrading.md +9 -3
  40. package/docs/writing-a-charter.md +28 -0
  41. package/package.json +18 -20
  42. package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +6 -0
  43. package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +6 -0
  44. package/templates/brain/CHARTER.md +3 -3
  45. package/templates/brain/README.md +1 -0
  46. package/templates/brain/log/decisions.md +3 -0
  47. package/templates/brain/strategy/ideas.md +7 -0
  48. package/templates/briefs/priorities.md +46 -0
  49. package/templates/ops/.github/workflows/session.yaml +108 -14
  50. package/templates/ops/agents.mjs +7 -3
  51. package/templates/ops/compose.mjs +16 -3
  52. package/templates/ops/inflight.mjs +157 -0
  53. package/templates/ops/org/operating.md +21 -1
  54. package/templates/ops/org/voice.md +9 -0
  55. package/templates/ops/prompts/_inflight.md +14 -0
  56. package/templates/ops/prompts/_paths.md +2 -1
  57. package/templates/ops/prompts/daily.md +16 -7
  58. package/templates/ops/prompts/mention.md +2 -0
  59. package/templates/ops/run-record.mjs +144 -0
  60. package/templates/portal/css/base.css +146 -73
  61. package/templates/portal/css/brain.css +23 -20
  62. package/templates/portal/css/diff.css +10 -9
  63. package/templates/portal/css/graph.css +12 -7
  64. package/templates/portal/css/health.css +13 -11
  65. package/templates/portal/css/inbox.css +57 -25
  66. package/templates/portal/css/layout.css +96 -41
  67. package/templates/portal/css/markdown.css +36 -14
  68. package/templates/portal/css/runs.css +13 -0
  69. package/templates/portal/css/setup.css +116 -34
  70. package/templates/portal/index.html +18 -2
  71. package/templates/portal/js/api.js +41 -4
  72. package/templates/portal/js/app.js +132 -9
  73. package/templates/portal/js/dialog.js +82 -0
  74. package/templates/portal/js/icons.js +37 -0
  75. package/templates/portal/js/inflight.js +18 -0
  76. package/templates/portal/js/md.js +5 -2
  77. package/templates/portal/js/mdedit.js +84 -0
  78. package/templates/portal/js/readiness.js +35 -0
  79. package/templates/portal/js/refresh.js +2 -1
  80. package/templates/portal/js/state.js +17 -4
  81. package/templates/portal/js/views/app.js +24 -7
  82. package/templates/portal/js/views/checklist.js +10 -4
  83. package/templates/portal/js/views/credential.js +84 -0
  84. package/templates/portal/js/views/graph.js +1 -1
  85. package/templates/portal/js/views/health.js +17 -4
  86. package/templates/portal/js/views/hire.js +583 -0
  87. package/templates/portal/js/views/inbox.js +264 -84
  88. package/templates/portal/js/views/org.js +46 -106
  89. package/templates/portal/js/views/orgedit.js +234 -0
  90. package/templates/portal/js/views/paste.js +87 -21
  91. package/templates/portal/js/views/prompt.js +11 -4
  92. package/templates/portal/js/views/repos.js +20 -15
  93. package/templates/portal/js/views/runonce.js +94 -0
  94. package/templates/portal/js/views/runs.js +170 -0
  95. package/templates/portal/js/views/setup.js +257 -75
  96. package/templates/portal/js/views/staff.js +158 -243
  97. package/templates/portal/js/views/todo.js +62 -0
@@ -5,31 +5,36 @@
5
5
  * spelling a repo name exactly right in a YAML file, which is the kind of thing that fails
6
6
  * quietly and reads as the tool being broken. */
7
7
 
8
- import { addRepo, listRepos } from "../api.js";
8
+ import { addRepo, getRepos, listRepos } from "../api.js";
9
9
  import { el } from "../dom.js";
10
10
 
11
11
  /**
12
- * @param {{org: string, known: string[], onAdded?: () => void}} opts
12
+ * @param {{org: string, known?: string[], onAdded?: () => void}} opts without `known`, what
13
+ * org.yaml already lists is read off disk, which is how the Org screen uses it long after setup.
13
14
  */
14
15
  export function repoPicker(opts) {
15
- const box = el("div", { className: "manual" });
16
- box.append(
17
- el("b", { textContent: "Which repos the staff work in" }),
18
- el("p", {
19
- textContent:
20
- "Repos marked as products are the ones staff contribute to without owning. A new hire " +
21
- "picks them up automatically.",
22
- }),
23
- );
16
+ const box = el("div", { className: opts.bare ? "" : "manual" });
17
+ if (!opts.bare) {
18
+ box.append(
19
+ el("b", { textContent: "Which repos the staff work in" }),
20
+ el("p", {
21
+ textContent:
22
+ "Add the repos your staff should work on. New hires get access to them automatically.",
23
+ }),
24
+ );
25
+ }
24
26
 
25
27
  const out = el("div", { style: "margin-top:9px" });
26
28
  box.append(out);
27
29
  out.replaceChildren(el("p", { className: "sub", textContent: "Reading your repos…" }));
28
30
 
29
- listRepos(opts.org)
30
- .then((data) => {
31
+ const known = opts.known
32
+ ? Promise.resolve(opts.known)
33
+ : getRepos().then((d) => (d.repos ?? []).map((r) => r.name));
34
+ Promise.all([listRepos(opts.org), known])
35
+ .then(([data, listed]) => {
31
36
  if (data.error) throw new Error(data.error);
32
- const already = new Set(opts.known ?? []);
37
+ const already = new Set(listed);
33
38
  const candidates = (data.repos ?? []).filter((r) => !already.has(r.name));
34
39
 
35
40
  if (!candidates.length) {
@@ -52,7 +57,7 @@ export function repoPicker(opts) {
52
57
  add.disabled = true;
53
58
  add.textContent = "Adding…";
54
59
  try {
55
- await addRepo(repo.name, "product");
60
+ await addRepo(repo.name, "product", repo.visibility);
56
61
  add.textContent = "Added";
57
62
  row.classList.add("added");
58
63
  opts.onAdded?.();
@@ -0,0 +1,94 @@
1
+ /* One daily run, now, followed to the end.
2
+ *
3
+ * The last step of setting anybody up. Doctor calls a workflow that has never run unproven,
4
+ * because nothing short of a finished run shows the App, its grant, the secrets and the
5
+ * callers all work together. This starts one, and says how it ended. */
6
+
7
+ import { runStatus, startRun } from "../api.js";
8
+ import { askYes } from "../dialog.js";
9
+ import { el } from "../dom.js";
10
+
11
+ /**
12
+ * @param {{staff: string, name: string, onDone?: (ok: boolean) => void}} opts
13
+ */
14
+ export function runOnce(opts) {
15
+ const box = el("div", { className: "manual" });
16
+ box.append(
17
+ el("b", { textContent: "Run " + opts.name + " once, now" }),
18
+ el("p", {
19
+ className: "sub",
20
+ textContent:
21
+ "A real daily run: it does a day's work and spends what a scheduled one would. " +
22
+ "When it finishes you get the outcome and the log.",
23
+ }),
24
+ );
25
+ const go = el("button", { className: "btn primary", textContent: "Run once now" });
26
+ const out = el("div", { style: "margin-top:9px" });
27
+ box.append(el("div", { className: "row" }, [go]), out);
28
+
29
+ go.onclick = async () => {
30
+ const yes = await askYes({
31
+ title: "Start a daily run for " + opts.name + " now?",
32
+ hint: "It is a real run, and it costs what one does.",
33
+ confirm: "Start the run",
34
+ });
35
+ if (!yes) return;
36
+ go.disabled = true;
37
+ const say = (text, cls) => out.replaceChildren(el("p", { className: cls ?? "sub", textContent: text }));
38
+ say("Starting…");
39
+ let run;
40
+ try {
41
+ run = await startRun(opts.staff);
42
+ } catch (err) {
43
+ say(String(err.message || err), "err");
44
+ go.disabled = false;
45
+ return;
46
+ }
47
+ const link = el("a", { href: run.url, target: "_blank", rel: "noopener", textContent: "the log" });
48
+ if (!run.id) {
49
+ out.replaceChildren(el("p", { className: "sub" }, ["Started. It has not shown up yet; follow it at ", link, "."]));
50
+ go.disabled = false;
51
+ return;
52
+ }
53
+
54
+ /* Polled rather than streamed: a run takes minutes, the status changes a handful of times,
55
+ and a clock beside it is what shows the page has not hung. */
56
+ const began = Date.now();
57
+ const tick = async () => {
58
+ let now;
59
+ try {
60
+ now = await runStatus(opts.staff, run.id);
61
+ if (now.error) throw new Error(now.error);
62
+ } catch (err) {
63
+ say(String(err.message || err), "err");
64
+ go.disabled = false;
65
+ return;
66
+ }
67
+ const mins = Math.floor((Date.now() - began) / 60000);
68
+ if (now.status !== "completed") {
69
+ out.replaceChildren(
70
+ el("p", { className: "sub" }, [now.status.replace("_", " ") + ", " + mins + " min so far. ", link]),
71
+ );
72
+ setTimeout(tick, 15000);
73
+ return;
74
+ }
75
+ go.disabled = false;
76
+ const ok = now.conclusion === "success";
77
+ const done = el("div");
78
+ done.append(
79
+ el("p", { className: ok ? "" : "err" }, [
80
+ ok
81
+ ? "It worked. The App, its grant, the secrets and the callers are proven, and Health now says so. "
82
+ : "It ended " + (now.conclusion || "without a conclusion") + ". ",
83
+ el("a", { href: now.url, target: "_blank", rel: "noopener", textContent: "The log" }),
84
+ ]),
85
+ );
86
+ for (const f of now.failedAt ?? []) done.append(el("p", { className: "sub", textContent: "Failed at " + f }));
87
+ if (!ok) done.append(el("p", { className: "sub", textContent: "Troubleshooting, in the docs, has what each failure usually means." }));
88
+ out.replaceChildren(done);
89
+ opts.onDone?.(ok);
90
+ };
91
+ tick();
92
+ };
93
+ return box;
94
+ }
@@ -0,0 +1,170 @@
1
+ /* Every staff member's recent runs, and what the last 30 days cost.
2
+ *
3
+ * The one screen that is only ever on GitHub: runs are not in any repo, so offline it says so
4
+ * rather than drawing an empty table that reads as "nothing ran". */
5
+
6
+ import { getRuns } from "../api.js";
7
+ import { ago, el, esc, skeleton } from "../dom.js";
8
+ import { render } from "../router.js";
9
+ import { S } from "../state.js";
10
+
11
+ /** How many runs a staff member's table shows. The spend covers all of the last 30 days. */
12
+ const SHOWN = 15;
13
+
14
+ export function viewRuns(m) {
15
+ m.append(el("h1", { textContent: "Runs" }));
16
+ m.append(
17
+ el("p", {
18
+ className: "sub",
19
+ textContent:
20
+ "What each staff member ran in the last 30 days, how it ended, and what it cost where " +
21
+ "the agent said. Read from GitHub through your own gh.",
22
+ }),
23
+ );
24
+
25
+ if (!S.runs) {
26
+ const wait = el("div", { className: "runs" }, skeleton("row", 4));
27
+ m.append(wait);
28
+ load(false);
29
+ return;
30
+ }
31
+ paint(m, S.runs);
32
+ /* Kept for the session, it could be days old: a tab opened during a rate limit went on
33
+ showing a two-week-old list. Anything older than a few minutes is asked for again. */
34
+ // Only a read with a time on it can be old, and only one re-read is asked for at a time.
35
+ const at = S.runs.fetchedAt ? new Date(S.runs.fetchedAt).getTime() : 0;
36
+ if (at && Date.now() - at > 5 * 60_000 && !asking) load(true);
37
+ }
38
+
39
+ let asking = false;
40
+
41
+ async function load(force) {
42
+ if (asking) return;
43
+ asking = true;
44
+ try {
45
+ S.runs = await getRuns(force);
46
+ } catch (e) {
47
+ S.runs = { online: false, error: e.message, staff: [] };
48
+ } finally {
49
+ asking = false;
50
+ }
51
+ if (S.view === "runs") render();
52
+ }
53
+
54
+ function paint(m, data) {
55
+ const again = el("button", { className: "ghbtn", textContent: "Ask GitHub again" });
56
+ again.onclick = () => {
57
+ S.runs = null;
58
+ load(true);
59
+ render();
60
+ };
61
+
62
+ if (!data.online) {
63
+ const box = el("div", { className: "notice" });
64
+ box.innerHTML =
65
+ "<b>Runs live on GitHub, and it could not be reached.</b><br>" +
66
+ esc(data.error ?? "gh is unavailable") +
67
+ "<br>Everything else in the portal reads from disk and still works.";
68
+ m.append(box, el("div", { className: "row hacts" }, [again]));
69
+ return;
70
+ }
71
+
72
+ const g = el("div", { className: "grid runsum" });
73
+ g.append(tile("Last 30 days", money(data.spend), priced(data.spend), over(data.spend, data.budget)));
74
+ if (data.budget) {
75
+ g.append(tile("Budget", "$" + data.budget, "org.yaml · 30 days, all staff", over(data.spend, data.budget)));
76
+ }
77
+ g.append(tile("Runs", String(data.spend?.runs ?? 0), "skipped mentions are not counted"));
78
+ m.append(g);
79
+
80
+ for (const s of data.staff ?? []) m.append(...staffSection(s));
81
+
82
+ m.append(
83
+ el("div", { className: "row hacts" }, [
84
+ again,
85
+ el("span", { className: "meta", textContent: data.fetchedAt ? "read " + ago(data.fetchedAt) : "" }),
86
+ ]),
87
+ );
88
+ }
89
+
90
+ function staffSection(s) {
91
+ const head = el("div", { className: "hsect" });
92
+ head.append(el("span", { textContent: s.name }));
93
+ const total = el("i", {
94
+ textContent: money(s.spend) + (s.budget ? " of $" + s.budget : "") + " · 30 days",
95
+ });
96
+ if (over(s.spend, s.budget)) total.className = "warn";
97
+ head.append(total);
98
+
99
+ const body = el("div", { className: "hbody runs" });
100
+ for (const e of s.errors ?? []) body.append(el("p", { className: "hnote err", textContent: e }));
101
+ if (!(s.runs ?? []).length) {
102
+ body.append(el("p", { className: "hnote", textContent: "No runs in the last 30 days." }));
103
+ return [head, body];
104
+ }
105
+
106
+ const table = el("table", { className: "runtable" });
107
+ table.innerHTML =
108
+ "<thead><tr><th>When</th><th>Kind</th><th>Outcome</th><th>Took</th><th>Turns</th>" +
109
+ "<th>Cost</th><th></th></tr></thead>";
110
+ const rows = el("tbody");
111
+ for (const r of s.runs.slice(0, SHOWN)) rows.append(row(r));
112
+ table.append(rows);
113
+ body.append(table);
114
+ if (s.runs.length > SHOWN) {
115
+ body.append(
116
+ el("p", {
117
+ className: "hnote",
118
+ textContent: s.runs.length - SHOWN + " older runs count towards the total and are not listed.",
119
+ }),
120
+ );
121
+ }
122
+ return [head, body];
123
+ }
124
+
125
+ function row(r) {
126
+ const tr = el("tr");
127
+ const out = outcome(r);
128
+ const rec = r.record ?? {};
129
+ const mins = rec.duration_s != null ? rec.duration_s / 60 : r.minutes;
130
+ tr.innerHTML =
131
+ '<td title="' + esc(r.createdAt) + '">' + esc(ago(r.createdAt)) + "</td>" +
132
+ "<td>" + esc(r.kind) + "</td>" +
133
+ '<td><span class="chip ' + out.tone + '">' + esc(out.word) + "</span></td>" +
134
+ "<td>" + (mins != null ? Math.round(mins) + "m" : "–") + "</td>" +
135
+ "<td>" + (rec.turns ?? "–") + "</td>" +
136
+ "<td>" + (typeof rec.cost_usd === "number" ? "$" + rec.cost_usd.toFixed(2) : "–") + "</td>" +
137
+ '<td><a class="ref" href="' + esc(r.url) + '" target="_blank" rel="noopener">log</a></td>';
138
+ return tr;
139
+ }
140
+
141
+ /** The record's word when there is one, which knows a setup failure from an agent failure. */
142
+ function outcome(r) {
143
+ if (r.status !== "completed") return { word: r.status.replace("_", " "), tone: "" };
144
+ const word = r.record?.outcome ?? r.conclusion ?? "unknown";
145
+ const tone = word === "success" ? "ok" : word === "cancelled" ? "warm" : "hot";
146
+ return { word, tone };
147
+ }
148
+
149
+ function money(spend) {
150
+ return "$" + (spend?.usd ?? 0).toFixed(2);
151
+ }
152
+
153
+ /** A total is only as good as the runs it could price, so it says how many that was. */
154
+ function priced(spend) {
155
+ if (!spend?.runs) return "no runs";
156
+ if (spend.known === spend.runs) return "every run priced";
157
+ return "cost known for " + spend.known + " of " + spend.runs + " runs";
158
+ }
159
+
160
+ function over(spend, budget) {
161
+ return !!budget && (spend?.usd ?? 0) > budget;
162
+ }
163
+
164
+ function tile(k, v, sub, bad) {
165
+ const d = el("div", { className: "kv" + (bad ? " bad" : "") });
166
+ d.innerHTML =
167
+ '<div class="k">' + esc(k) + '</div><div class="v">' + esc(v) +
168
+ (sub ? "<small>" + esc(sub) + "</small>" : "") + "</div>";
169
+ return d;
170
+ }