@nanocollective/roster 0.1.0-alpha.2 → 0.1.0-alpha.20
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 +4733 -2490
- 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 +76 -11
- package/docs/portal.md +261 -47
- package/docs/prompts.md +77 -11
- package/docs/security.md +51 -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 +32 -11
- package/templates/portal/css/inbox.css +117 -14
- package/templates/portal/css/layout.css +93 -41
- package/templates/portal/css/markdown.css +57 -15
- package/templates/portal/css/runs.css +13 -0
- package/templates/portal/css/setup.css +83 -39
- 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/mdedit.js +84 -0
- 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 +29 -10
- package/templates/portal/js/views/credential.js +98 -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 +124 -104
- package/templates/portal/js/views/orgedit.js +213 -0
- package/templates/portal/js/views/paste.js +33 -7
- package/templates/portal/js/views/prompt.js +61 -67
- package/templates/portal/js/views/repos.js +20 -15
- 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 +311 -83
- 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
|
@@ -5,28 +5,109 @@
|
|
|
5
5
|
* is rather than as it was left. Setup cannot be finished in one sitting, so it must survive
|
|
6
6
|
* being abandoned halfway. */
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
checkOrg,
|
|
10
|
+
createTenant,
|
|
11
|
+
getAccess,
|
|
12
|
+
getOrg,
|
|
13
|
+
getSetup,
|
|
14
|
+
joinOrg,
|
|
15
|
+
planTenant,
|
|
16
|
+
setAccess,
|
|
17
|
+
} from "../api.js";
|
|
9
18
|
import { $, el, esc, toClipboard } from "../dom.js";
|
|
19
|
+
import { go } from "../router.js";
|
|
20
|
+
import { S as App } from "../state.js";
|
|
10
21
|
import { checklist } from "./checklist.js";
|
|
22
|
+
import { businessForm, prioritiesForm } from "./orgedit.js";
|
|
23
|
+
import { credentialPanel } from "./credential.js";
|
|
11
24
|
import { repoPicker } from "./repos.js";
|
|
12
|
-
import { paste } from "./paste.js";
|
|
13
25
|
|
|
14
26
|
let S = null; // /api/setup/status, refreshed after anything that changes the world
|
|
15
27
|
|
|
28
|
+
/* Where the doctor checklist is drawn on this screen, so a save can re-run it. Left stale, it
|
|
29
|
+
went on saying business.stub after business.md had been saved, until "Check again". */
|
|
30
|
+
let healthHost = null;
|
|
31
|
+
let healthStep = null;
|
|
32
|
+
|
|
33
|
+
/* What the "What is left" cards already ask for. Doctor reporting them as well, right after a
|
|
34
|
+
successful create, read as the create having failed. */
|
|
35
|
+
const COVERED = ["business", "business.stub", "priorities", "priorities.stub", "actions-access", "secrets", "agent"];
|
|
36
|
+
const HEALTH = { skip: COVERED, hideWhenClean: true };
|
|
37
|
+
|
|
38
|
+
/* Each "what is left" step that is a file, by id, so a save can mark it done in place rather
|
|
39
|
+
than repainting the page and throwing away the diff somebody is still reading. */
|
|
40
|
+
const fileSteps = new Map();
|
|
41
|
+
|
|
16
42
|
export async function viewSetup(main) {
|
|
17
43
|
S = await getSetup();
|
|
18
44
|
render(main);
|
|
19
45
|
}
|
|
20
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The same steps once a tenant exists, as their own screen in the portal.
|
|
49
|
+
*
|
|
50
|
+
* Setup used to be reachable only before there was a tenant, so the reload that followed
|
|
51
|
+
* creating one landed on an empty Inbox with the repo picker, the credential and both files
|
|
52
|
+
* gone. The sidebar offers this while anything is unfinished, and an org nobody has been hired
|
|
53
|
+
* into opens on it.
|
|
54
|
+
*/
|
|
55
|
+
export async function viewGettingStarted(main) {
|
|
56
|
+
main.append(el("h1", { textContent: "Getting started" }));
|
|
57
|
+
const sub = el("p", { className: "sub", textContent: "Reading what is left…" });
|
|
58
|
+
main.append(sub);
|
|
59
|
+
S = await getSetup();
|
|
60
|
+
// Somebody may have clicked elsewhere while GitHub was answering.
|
|
61
|
+
if (App.view !== "setup") return;
|
|
62
|
+
sub.textContent = "Work through these in order.";
|
|
63
|
+
main.append(afterCreate());
|
|
64
|
+
|
|
65
|
+
const health = step("!", "Other problems", false);
|
|
66
|
+
healthHost = el("div", { style: "margin-top:12px" });
|
|
67
|
+
health.append(healthHost);
|
|
68
|
+
main.append(health);
|
|
69
|
+
healthStep = health;
|
|
70
|
+
checklist(healthHost, { ...HEALTH, onEmpty: () => health.remove() });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether the sidebar offers Getting started: while anything is left, or while it is open. */
|
|
74
|
+
export function paintSetupNav() {
|
|
75
|
+
const nav = $("#setupnav");
|
|
76
|
+
if (!nav) return;
|
|
77
|
+
nav.hidden = !App.data?.unfinished?.length && App.view !== "setup";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* After a save: the checklist and the step list are both derived from disk, so both are asked
|
|
81
|
+
again rather than updated by hand. The sidebar's list comes from /api/org, which is the one
|
|
82
|
+
the portal already reads. */
|
|
83
|
+
async function recheck() {
|
|
84
|
+
if (healthHost) checklist(healthHost, { ...HEALTH, onEmpty: () => healthStep?.remove() });
|
|
85
|
+
try {
|
|
86
|
+
S = await getSetup();
|
|
87
|
+
for (const [id, mark] of fileSteps) mark(!(S.tenant.left ?? []).includes(id));
|
|
88
|
+
} catch {
|
|
89
|
+
// The saved file is saved either way; a stale tick is not worth an error on top of it.
|
|
90
|
+
}
|
|
91
|
+
if (App.data) {
|
|
92
|
+
const fresh = await getOrg().catch(() => null);
|
|
93
|
+
if (fresh?.unfinished) App.data.unfinished = fresh.unfinished;
|
|
94
|
+
paintSetupNav();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
21
98
|
function render(main) {
|
|
99
|
+
/* The sidebar is written by boot() before any of this is known. Once a tenant has been
|
|
100
|
+
adopted, leaving it on "no org yet" contradicts the card directly below it. */
|
|
101
|
+
const brand = $("#orgname");
|
|
102
|
+
if (brand) brand.textContent = S.tenant.found ? (S.tenant.org ?? "set up") : "no org yet";
|
|
103
|
+
|
|
22
104
|
main.replaceChildren();
|
|
23
105
|
main.append(el("h1", { textContent: "Set up your org" }));
|
|
24
106
|
main.append(
|
|
25
107
|
el("p", {
|
|
26
108
|
className: "sub",
|
|
27
109
|
textContent:
|
|
28
|
-
"
|
|
29
|
-
"you have read the plan.",
|
|
110
|
+
"Choose an organisation and a coding agent. Nothing is created until you press Create it.",
|
|
30
111
|
}),
|
|
31
112
|
);
|
|
32
113
|
|
|
@@ -36,7 +117,11 @@ function render(main) {
|
|
|
36
117
|
|
|
37
118
|
/* Drawn whenever a tenant exists, not only in the seconds after creating one. Setup takes
|
|
38
119
|
days: the repo picker and the two manual steps have to still be here tomorrow. */
|
|
39
|
-
if (S.tenant.found)
|
|
120
|
+
if (S.tenant.found) {
|
|
121
|
+
const rest = step(3, "Finish setting up", false);
|
|
122
|
+
rest.append(afterCreate());
|
|
123
|
+
main.append(rest);
|
|
124
|
+
}
|
|
40
125
|
}
|
|
41
126
|
|
|
42
127
|
/* ---------------------------------- 1 · gh ---------------------------------- */
|
|
@@ -52,8 +137,7 @@ function stepGh() {
|
|
|
52
137
|
el("p", {
|
|
53
138
|
className: "sub",
|
|
54
139
|
textContent:
|
|
55
|
-
"
|
|
56
|
-
"in a terminal, then reload:",
|
|
140
|
+
"Run this in a terminal, then reload:",
|
|
57
141
|
}),
|
|
58
142
|
el("pre", { className: "cmd", textContent: "gh auth login" }),
|
|
59
143
|
);
|
|
@@ -73,12 +157,10 @@ function stepOrg(main) {
|
|
|
73
157
|
}),
|
|
74
158
|
el("p", {
|
|
75
159
|
className: "sub",
|
|
76
|
-
textContent:
|
|
160
|
+
textContent:
|
|
161
|
+
"Finish the steps below. They stay under Getting started in the sidebar until they're done.",
|
|
77
162
|
}),
|
|
78
163
|
);
|
|
79
|
-
const rows = el("div", { style: "margin-top:12px" });
|
|
80
|
-
card.append(rows);
|
|
81
|
-
checklist(rows, {});
|
|
82
164
|
return card;
|
|
83
165
|
}
|
|
84
166
|
|
|
@@ -108,8 +190,7 @@ function stepOrg(main) {
|
|
|
108
190
|
orgWrap.append(
|
|
109
191
|
el("small", {
|
|
110
192
|
textContent:
|
|
111
|
-
"
|
|
112
|
-
"github.com first if you need to.",
|
|
193
|
+
"Don't have one? Create it on github.com first.",
|
|
113
194
|
}),
|
|
114
195
|
);
|
|
115
196
|
form.append(orgWrap);
|
|
@@ -152,32 +233,31 @@ function stepOrg(main) {
|
|
|
152
233
|
});
|
|
153
234
|
other.input.addEventListener("change", look);
|
|
154
235
|
|
|
155
|
-
const name = field("name", "What the business is called", "", "
|
|
156
|
-
const human = field("human", "Your GitHub login", S.gh.login ?? "", "The person the
|
|
236
|
+
const name = field("name", "What the business is called", "", "Defaults to the organisation name.");
|
|
237
|
+
const human = field("human", "Your GitHub login", S.gh.login ?? "", "The person the staff report to.");
|
|
157
238
|
|
|
158
239
|
card.append(form);
|
|
240
|
+
const agentHead = el("h3", { textContent: "Which coding agent runs a session" });
|
|
159
241
|
|
|
160
242
|
/* The agent. Its id decides the secret name later, so setup can name it exactly rather than
|
|
161
243
|
saying "it depends on the runner". */
|
|
162
|
-
card.append(
|
|
244
|
+
card.append(agentHead);
|
|
163
245
|
const agents = el("div", { className: "choices" });
|
|
164
246
|
S.agents.forEach((a, i) => {
|
|
165
247
|
const opt = el("label", { className: "choice" });
|
|
166
248
|
const radio = el("input", { name: "agent", value: a.id, checked: i === 0 });
|
|
167
249
|
radio.setAttribute("type", "radio");
|
|
168
|
-
opt.append(radio, el("b", { textContent: a.label })
|
|
250
|
+
opt.append(radio, el("b", { textContent: a.label }));
|
|
169
251
|
opt.append(el("code", { textContent: a.tokenEnv }));
|
|
170
252
|
agents.append(opt);
|
|
171
253
|
});
|
|
172
254
|
card.append(agents);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}),
|
|
180
|
-
);
|
|
255
|
+
const agentNote = el("p", {
|
|
256
|
+
className: "sub",
|
|
257
|
+
textContent:
|
|
258
|
+
"Using a different agent? Pick any of these, then set it in org.yaml afterwards.",
|
|
259
|
+
});
|
|
260
|
+
card.append(agentNote);
|
|
181
261
|
|
|
182
262
|
const params = () => ({
|
|
183
263
|
org: org.value === "__other" ? other.input.value.trim() : org.value,
|
|
@@ -200,11 +280,29 @@ function stepOrg(main) {
|
|
|
200
280
|
join.hidden = !joining;
|
|
201
281
|
plan.hidden = joining;
|
|
202
282
|
if (joining) go.hidden = true;
|
|
283
|
+
|
|
284
|
+
/* An org that already runs roster has answered all of these: its own org.yaml names the
|
|
285
|
+
business, the human and the agent. Leaving them on screen asks for values that will be
|
|
286
|
+
thrown away, and implies the checkout is going to use them. */
|
|
287
|
+
for (const bit of [name.wrap, human.wrap, agentHead, agents, agentNote]) bit.hidden = joining;
|
|
203
288
|
}
|
|
204
289
|
|
|
205
290
|
join.onclick = async () => {
|
|
206
291
|
join.disabled = true;
|
|
207
|
-
|
|
292
|
+
/* Cloning an ops repo and every brain took 38 seconds against a real org, behind a word
|
|
293
|
+
that never changed. A static "Cloning…" for that long reads as hung, so this says what
|
|
294
|
+
it is doing and keeps a clock running to show it is alive. */
|
|
295
|
+
const clock = el("p", { className: "sub" });
|
|
296
|
+
out.replaceChildren(clock);
|
|
297
|
+
const started = Date.now();
|
|
298
|
+
const tick = () => {
|
|
299
|
+
const s = Math.round((Date.now() - started) / 1000);
|
|
300
|
+
clock.textContent =
|
|
301
|
+
`Cloning ${existing}: the ops repo and one for each staff member. ${s}s. ` +
|
|
302
|
+
"A first checkout usually takes under a minute.";
|
|
303
|
+
};
|
|
304
|
+
tick();
|
|
305
|
+
const ticking = setInterval(tick, 1000);
|
|
208
306
|
try {
|
|
209
307
|
const result = await joinOrg(existing);
|
|
210
308
|
if (!result.ok) throw new Error(result.error);
|
|
@@ -219,6 +317,7 @@ function stepOrg(main) {
|
|
|
219
317
|
} catch (err) {
|
|
220
318
|
out.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
221
319
|
} finally {
|
|
320
|
+
clearInterval(ticking);
|
|
222
321
|
join.disabled = false;
|
|
223
322
|
}
|
|
224
323
|
};
|
|
@@ -232,23 +331,20 @@ function stepOrg(main) {
|
|
|
232
331
|
out.replaceChildren(el("p", { className: "sub", textContent: "Working…" }));
|
|
233
332
|
try {
|
|
234
333
|
const result = await planTenant(p);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
el("
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
"it is the one that decides whether any of this is worth running.",
|
|
250
|
-
}),
|
|
251
|
-
);
|
|
334
|
+
const steps = el("ol", { className: "plansteps" }, [
|
|
335
|
+
el("li", { textContent: `Create a private repo, ${p.org}/roster-ops, to hold the org's shared settings.` }),
|
|
336
|
+
el("li", { textContent: `Put a copy of it in ${result.dir}.` }),
|
|
337
|
+
el("li", { textContent: `Let the other repos in ${p.org} run its workflow.` }),
|
|
338
|
+
]);
|
|
339
|
+
const files = el("details", { className: "planfiles" }, [
|
|
340
|
+
el("summary", { textContent: `The ${result.files.length} files it writes` }),
|
|
341
|
+
el("div", { className: "filelist" }, result.files.map((f) => el("code", { textContent: f }))),
|
|
342
|
+
]);
|
|
343
|
+
const next = el("p", {
|
|
344
|
+
className: "sub",
|
|
345
|
+
textContent: "After that: describe the business, set this month's priorities, and hire your first staff member.",
|
|
346
|
+
});
|
|
347
|
+
out.replaceChildren(el("p", { textContent: "Create it will:" }), steps, files, next);
|
|
252
348
|
go.hidden = false;
|
|
253
349
|
} catch (err) {
|
|
254
350
|
out.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
@@ -279,53 +375,185 @@ function stepOrg(main) {
|
|
|
279
375
|
/* ------------------------- 5 · the two GitHub insists on ------------------------- */
|
|
280
376
|
|
|
281
377
|
function afterCreate() {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
"you skip it.",
|
|
288
|
-
}),
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
const one = el("div", { className: "manual" });
|
|
292
|
-
one.append(
|
|
293
|
-
el("b", { textContent: "Let the ops repo's workflow be called" }),
|
|
294
|
-
el("p", {
|
|
295
|
-
textContent:
|
|
296
|
-
"Settings → Actions → General → Access → “Accessible from repositories in the " +
|
|
297
|
-
"organisation”, on roster-ops.",
|
|
298
|
-
}),
|
|
299
|
-
el("small", {
|
|
300
|
-
textContent:
|
|
301
|
-
"Skip it and every workflow fails with “workflow not found”, which reads like a typo in " +
|
|
302
|
-
"a path and is not one. It is an org permission on a repo, so it is yours to click.",
|
|
303
|
-
}),
|
|
304
|
-
);
|
|
305
|
-
if (S.tenant.org) {
|
|
306
|
-
const url = `https://github.com/${S.tenant.org}/roster-ops/settings/actions`;
|
|
307
|
-
one.append(el("a", { className: "btn", href: url, target: "_blank", textContent: "Open that page" }));
|
|
308
|
-
}
|
|
309
|
-
card.append(one);
|
|
378
|
+
fileSteps.clear();
|
|
379
|
+
const list = el("div", { className: "todos" });
|
|
380
|
+
const left = S.tenant.left ?? [];
|
|
381
|
+
const staffCount = App.data?.staff?.length ?? 0;
|
|
382
|
+
let n = 0;
|
|
310
383
|
|
|
311
384
|
if (S.tenant.org) {
|
|
312
|
-
|
|
385
|
+
const repos = todo(++n, "Choose the repos your staff work on", (S.tenant.products ?? []).length > 0);
|
|
386
|
+
repos.hint("Pick at least one. Staff open pull requests there, and you review them.");
|
|
387
|
+
repos.body.append(
|
|
388
|
+
repoPicker({ org: S.tenant.org, known: S.tenant.repos ?? [], bare: true, onAdded: () => repos.setDone(true) }),
|
|
389
|
+
);
|
|
390
|
+
list.append(repos.card);
|
|
313
391
|
}
|
|
314
392
|
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
393
|
+
const business = todo(++n, "Describe the business", !left.includes("business"));
|
|
394
|
+
business.hint("A few short answers. Every staff member reads them before they start work.");
|
|
395
|
+
business.body.append(
|
|
396
|
+
businessForm({
|
|
397
|
+
path: opsName() + "/org/business.md",
|
|
398
|
+
name: App.data?.name ?? S.tenant.name ?? S.tenant.org,
|
|
399
|
+
onSaved: () => (business.setDone(true), recheck()),
|
|
322
400
|
}),
|
|
323
401
|
);
|
|
324
|
-
|
|
402
|
+
fileSteps.set("business", business.setDone);
|
|
403
|
+
list.append(business.card);
|
|
404
|
+
|
|
405
|
+
const priorities = todo(++n, "Set this month's priorities", !left.includes("priorities"));
|
|
406
|
+
priorities.hint("Up to three, in order. Staff pick work that serves them.");
|
|
407
|
+
priorities.body.append(
|
|
408
|
+
prioritiesForm({
|
|
409
|
+
path: opsName() + "/org/priorities.md",
|
|
410
|
+
onSaved: () => (priorities.setDone(true), recheck()),
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
fileSteps.set("priorities", priorities.setDone);
|
|
414
|
+
list.append(priorities.card);
|
|
415
|
+
|
|
416
|
+
const hire = todo(++n, "Hire your first staff member", staffCount > 0);
|
|
417
|
+
hire.hint("Give them a role, like cto. You'll create their GitHub App and write their charter from their card.");
|
|
418
|
+
const hireBtn = el("button", { className: "btn primary", textContent: "Hire someone" });
|
|
419
|
+
hireBtn.onclick = () => {
|
|
420
|
+
// Before the org has loaded, this screen is the whole app, so the Staff screen needs a reload.
|
|
421
|
+
if (App.data) go({ view: "staff" });
|
|
422
|
+
else location.assign("/#/-/staff") || location.reload();
|
|
423
|
+
};
|
|
424
|
+
hire.body.append(el("div", { className: "row" }, [hireBtn]));
|
|
425
|
+
list.append(hire.card);
|
|
325
426
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
427
|
+
const cred = todo(++n, "Add your agent credential", false);
|
|
428
|
+
cred.hint(staffCount ? "Staff use it to run. You only add it once." : "Do this after your first hire.");
|
|
429
|
+
cred.body.append(credentialPanel({ bare: true, onStatus: (ok) => cred.setDone(ok) }));
|
|
430
|
+
list.append(cred.card);
|
|
431
|
+
|
|
432
|
+
const access = todo(++n, "Let staff repos use the shared workflow", false);
|
|
433
|
+
access.body.append(accessStep({ onStatus: (ok) => access.setDone(ok) }));
|
|
434
|
+
list.append(access.card);
|
|
435
|
+
|
|
436
|
+
return list;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* One numbered thing to do. Done ones fold to a single ticked line, and Change opens them again,
|
|
441
|
+
* so the list shows what is left without hiding what was done.
|
|
442
|
+
*/
|
|
443
|
+
function todo(n, title, done) {
|
|
444
|
+
const card = el("section", { className: "todo" });
|
|
445
|
+
const num = el("span", { className: "todon" });
|
|
446
|
+
const pill = el("span", { className: "todopill" });
|
|
447
|
+
const change = el("button", { className: "ghbtn todochange", textContent: "Change" });
|
|
448
|
+
const head = el("div", { className: "todohead" }, [num, el("h3", { textContent: title }), pill, change]);
|
|
449
|
+
const hintEl = el("p", { className: "sub todohint" });
|
|
450
|
+
const body = el("div", { className: "todobody" });
|
|
451
|
+
card.append(head, hintEl, body);
|
|
452
|
+
let open = false;
|
|
453
|
+
const setDone = (d) => {
|
|
454
|
+
card.classList.toggle("done", d);
|
|
455
|
+
num.textContent = d ? "✓" : String(n);
|
|
456
|
+
pill.textContent = d ? "Done" : "To do";
|
|
457
|
+
change.hidden = !d;
|
|
458
|
+
const shown = !d || open;
|
|
459
|
+
body.hidden = !shown;
|
|
460
|
+
hintEl.hidden = !shown || !hintEl.textContent;
|
|
461
|
+
};
|
|
462
|
+
change.onclick = () => {
|
|
463
|
+
open = !open;
|
|
464
|
+
change.textContent = open ? "Close" : "Change";
|
|
465
|
+
setDone(card.classList.contains("done"));
|
|
466
|
+
};
|
|
467
|
+
setDone(done);
|
|
468
|
+
return {
|
|
469
|
+
card,
|
|
470
|
+
body,
|
|
471
|
+
setDone,
|
|
472
|
+
hint: (text) => {
|
|
473
|
+
hintEl.textContent = text;
|
|
474
|
+
setDone(card.classList.contains("done"));
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* One of the files only a person can write, with whether it is written yet.
|
|
481
|
+
*
|
|
482
|
+
* Read from the status rather than from the click that saved it, so a file written in an
|
|
483
|
+
* editor, or by a coding agent, shows as done here too.
|
|
484
|
+
*/
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* org/priorities.md, edited in place.
|
|
488
|
+
*
|
|
489
|
+
* There is no brief for it, and it does not want one: business.md needs an interview because
|
|
490
|
+
* nobody can write it off the top of their head, while a ranked list of three things is
|
|
491
|
+
* quicker to type than to explain to a model. So the file opens as it is on disk, stub and
|
|
492
|
+
* all, and saving it is the same commit-and-push as every other editor here.
|
|
493
|
+
*/
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
/** The ops repo's directory name, which is what every workspace-relative path starts with. */
|
|
501
|
+
function opsName() {
|
|
502
|
+
return App.data?.opsName ?? String(S.tenant.opsDir ?? "roster-ops").split("/").pop();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The ops repo's Actions access. Read on arrival, so a page reloaded after it was set says it
|
|
507
|
+
* is done without anybody pressing anything; the button asks the server to set it, and a
|
|
508
|
+
* refusal comes back with GitHub's reason and the page to click instead.
|
|
509
|
+
*
|
|
510
|
+
* Skipped, every workflow fails with "workflow not found", which reads like a typo in a path.
|
|
511
|
+
*/
|
|
512
|
+
function accessStep(opts = {}) {
|
|
513
|
+
const box = el("div");
|
|
514
|
+
const out = el("div");
|
|
515
|
+
box.append(out);
|
|
516
|
+
const say = (text, cls) => el("p", { className: cls ?? "sub", textContent: text });
|
|
517
|
+
|
|
518
|
+
const offer = (why) => {
|
|
519
|
+
const go = el("button", { className: "btn primary", textContent: "Set it for me" });
|
|
520
|
+
out.replaceChildren(
|
|
521
|
+
say(
|
|
522
|
+
(why ? why + " " : "") +
|
|
523
|
+
"Roster tried to set this when it created the org, and GitHub didn't allow it.",
|
|
524
|
+
),
|
|
525
|
+
el("div", { className: "row" }, [go]),
|
|
526
|
+
);
|
|
527
|
+
go.onclick = async () => {
|
|
528
|
+
go.disabled = true;
|
|
529
|
+
try {
|
|
530
|
+
const r = await setAccess();
|
|
531
|
+
if (r.ok) {
|
|
532
|
+
out.replaceChildren(say("Done."));
|
|
533
|
+
opts.onStatus?.(true);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
out.replaceChildren(
|
|
537
|
+
say("GitHub refused: " + r.error + ".", "err"),
|
|
538
|
+
el("a", { className: "btn", href: r.link, target: "_blank", textContent: "Set it by hand" }),
|
|
539
|
+
say("Access → “Accessible from repositories in the organisation”."),
|
|
540
|
+
);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
out.replaceChildren(say(String(err.message || err), "err"));
|
|
543
|
+
go.disabled = false;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
out.replaceChildren(say("Checking…"));
|
|
549
|
+
getAccess()
|
|
550
|
+
.then((r) => {
|
|
551
|
+
opts.onStatus?.(Boolean(r.ok));
|
|
552
|
+
if (r.ok) out.replaceChildren(say("Done."));
|
|
553
|
+
else offer(r.level ? "It is set to “" + r.level + "”." : "");
|
|
554
|
+
})
|
|
555
|
+
.catch(() => offer(""));
|
|
556
|
+
return box;
|
|
329
557
|
}
|
|
330
558
|
|
|
331
559
|
/* ---------------------------------- furniture ---------------------------------- */
|