@nanocollective/roster 0.1.0-alpha.3 → 0.1.0-alpha.31
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 +70 -84
- package/dist/cli.js +4817 -2374
- package/docs/README.md +19 -11
- package/docs/agents.md +328 -13
- package/docs/architecture.md +13 -5
- package/docs/charters/analyst.md +65 -0
- package/docs/charters/cmo.md +69 -0
- package/docs/charters/community.md +63 -0
- package/docs/charters/cto.md +71 -0
- package/docs/charters/designer.md +65 -0
- package/docs/charters/devops.md +65 -0
- package/docs/charters/pm.md +70 -0
- package/docs/charters/qa.md +65 -0
- package/docs/charters/support.md +60 -0
- package/docs/charters/writer.md +63 -0
- package/docs/commands.md +95 -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 +4 -1
- package/docs/extending.md +13 -4
- package/docs/getting-started.md +133 -79
- 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 +95 -101
- package/docs/memory.md +29 -8
- package/docs/org-yaml.md +76 -11
- package/docs/portal.md +290 -49
- package/docs/prompts.md +77 -11
- package/docs/security.md +51 -7
- package/docs/session-workflow.md +51 -21
- package/docs/staff-yaml.md +17 -7
- package/docs/troubleshooting.md +23 -20
- package/docs/upgrading.md +9 -3
- package/docs/writing-a-charter.md +46 -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/briefs/priorities.md +46 -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 +245 -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 +114 -41
- package/templates/portal/css/markdown.css +57 -15
- package/templates/portal/css/runs.css +13 -0
- package/templates/portal/css/setup.css +126 -39
- package/templates/portal/index.html +25 -3
- package/templates/portal/js/api.js +74 -4
- package/templates/portal/js/app.js +156 -14
- package/templates/portal/js/dialog.js +129 -4
- package/templates/portal/js/dom.js +25 -0
- package/templates/portal/js/icons.js +45 -1
- package/templates/portal/js/inflight.js +18 -0
- 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/readiness.js +35 -0
- package/templates/portal/js/refresh.js +136 -6
- package/templates/portal/js/state.js +59 -8
- package/templates/portal/js/views/app.js +24 -7
- package/templates/portal/js/views/checklist.js +29 -10
- package/templates/portal/js/views/credential.js +84 -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/hire.js +583 -0
- package/templates/portal/js/views/inbox.js +959 -126
- 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 +234 -0
- package/templates/portal/js/views/paste.js +87 -21
- 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 +257 -75
- package/templates/portal/js/views/staff.js +157 -182
- package/templates/portal/js/views/todo.js +62 -0
- 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
|
@@ -1,44 +1,145 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* The Staff screen: a card per staff member, retiring, and the way in to hiring.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Plan then apply, the same as every roster command that changes something: you see every
|
|
8
|
-
* file, every label and every existing staff member it would edit, and nothing happens until
|
|
9
|
-
* you say so.
|
|
3
|
+
* Hiring itself is in hire.js. Retiring runs the CLI's own `buildRetirePlan` and
|
|
4
|
+
* `applyRetirePlan` on the server, plan then apply: you see what it stops and what it keeps,
|
|
5
|
+
* and nothing happens until you say so.
|
|
10
6
|
*/
|
|
11
7
|
|
|
12
8
|
import { post } from "../api.js";
|
|
9
|
+
import { askYes, sheet } from "../dialog.js";
|
|
10
|
+
import { pending } from "../inflight.js";
|
|
13
11
|
import { ago, el, esc } from "../dom.js";
|
|
14
|
-
import {
|
|
12
|
+
import { whatsLeft, sentence } from "../readiness.js";
|
|
15
13
|
import { refreshAll } from "../refresh.js";
|
|
16
14
|
import { S } from "../state.js";
|
|
17
|
-
import {
|
|
18
|
-
|
|
15
|
+
import { hireFlow, line, rolePicker } from "./hire.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolves once nothing is loading and `node` has stopped changing for a moment, or after a few
|
|
19
|
+
* seconds regardless. Quiet alone was not enough: while GitHub was still answering, nothing
|
|
20
|
+
* changed, and the half-built list was shown.
|
|
21
|
+
*/
|
|
22
|
+
function settled(node, quiet = 250, most = 6000) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
if (typeof MutationObserver !== "function") return resolve();
|
|
25
|
+
let lastChange = Date.now();
|
|
26
|
+
const obs = new MutationObserver(() => {
|
|
27
|
+
lastChange = Date.now();
|
|
28
|
+
});
|
|
29
|
+
obs.observe(node, { childList: true, subtree: true, attributes: true, characterData: true });
|
|
30
|
+
const started = Date.now();
|
|
31
|
+
const tick = setInterval(() => {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if ((pending() === 0 && now - lastChange >= quiet) || now - started >= most) {
|
|
34
|
+
clearInterval(tick);
|
|
35
|
+
obs.disconnect();
|
|
36
|
+
resolve();
|
|
37
|
+
}
|
|
38
|
+
}, 50);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
19
41
|
|
|
20
|
-
|
|
42
|
+
/* The open dialog, if any. It lives on <body>, so it outlives a repaint of this screen, and a
|
|
43
|
+
background refresh leaves whatever was typed into it alone. */
|
|
44
|
+
let current = null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {{open?: string, result?: object}} [o] `open` keeps one staff member's steps on
|
|
48
|
+
* screen across a repaint, which is how a finished hire lands on step 2.
|
|
49
|
+
*/
|
|
50
|
+
export function viewStaff(m, o = {}) {
|
|
21
51
|
m.append(el("h1", { textContent: "Staff" }));
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
className: "sub",
|
|
25
|
-
textContent:
|
|
26
|
-
S.data.staff.length +
|
|
27
|
-
" on the roster. Hiring creates a repo, its workflows, its labels and its pinned issue, " +
|
|
28
|
-
"and wires it to everyone already here.",
|
|
29
|
-
}),
|
|
30
|
-
);
|
|
52
|
+
const empty = !S.data.staff.length;
|
|
53
|
+
if (empty) m.append(el("p", { className: "sub", textContent: "No staff yet. Pick a role to hire your first." }));
|
|
31
54
|
|
|
32
|
-
const
|
|
33
|
-
for (const s of S.data.staff) list.append(card(s));
|
|
34
|
-
m.append(list);
|
|
55
|
+
const pane = el("div", { className: "hirepane" });
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
57
|
+
/* Tasks open in a dialog. Where there is no dialog element (the test shim) they draw into
|
|
58
|
+
the pane instead, which is how this screen always drew them. */
|
|
59
|
+
function present(node) {
|
|
60
|
+
const spin = el("div", { className: "sheetloading" }, [el("span", { className: "spinner" })]);
|
|
61
|
+
if (current?.isOpen()) current.set(spin);
|
|
62
|
+
else {
|
|
63
|
+
current = sheet({
|
|
64
|
+
node: spin,
|
|
65
|
+
onClose: () => {
|
|
66
|
+
current = null;
|
|
67
|
+
S.staffOpen = null;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
if (!current) {
|
|
71
|
+
pane.replaceChildren(node);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/* The list is built from half a dozen answers arriving one by one, and showing each as it
|
|
76
|
+
landed meant the dialog reflowed four times. It is shown once it has stopped changing. */
|
|
77
|
+
const mine = current;
|
|
78
|
+
settled(node).then(() => {
|
|
79
|
+
if (mine.isOpen() && current === mine) mine.set(node);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function dismiss() {
|
|
83
|
+
if (current) current.close();
|
|
84
|
+
else pane.replaceChildren();
|
|
85
|
+
S.staffOpen = null;
|
|
86
|
+
if (empty) pick();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// With nobody hired the roles are the page. After that they are a dialog behind Hire someone.
|
|
90
|
+
function pick() {
|
|
91
|
+
if (empty) {
|
|
92
|
+
pane.replaceChildren(rolePicker({ onPick: (role) => open(role) }));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
|
|
96
|
+
cancel.onclick = dismiss;
|
|
97
|
+
present(
|
|
98
|
+
el("div", {}, [
|
|
99
|
+
el("h2", { className: "sheettitle", textContent: "Pick a role" }),
|
|
100
|
+
rolePicker({ onPick: (role) => open(role) }),
|
|
101
|
+
el("div", { className: "row", style: "margin-top:14px" }, [cancel]),
|
|
102
|
+
]),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* One staff member's steps, hired or not yet. A hire repaints the whole screen, so the new
|
|
107
|
+
card is in the list and the steps reopen on the same person. */
|
|
108
|
+
function open(role, result) {
|
|
109
|
+
// Remembered, so a refresh after a trip to GitHub (creating the App) reopens it.
|
|
110
|
+
S.staffOpen = role;
|
|
111
|
+
present(
|
|
112
|
+
hireFlow({
|
|
113
|
+
role,
|
|
114
|
+
result,
|
|
115
|
+
onHired: (handle, r) => {
|
|
116
|
+
m.replaceChildren();
|
|
117
|
+
viewStaff(m, { open: handle, result: r });
|
|
118
|
+
},
|
|
119
|
+
onClose: dismiss,
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!empty) {
|
|
125
|
+
const list = el("div", { className: "grid", style: "margin-bottom:18px" });
|
|
126
|
+
for (const s of S.data.staff) list.append(card(s));
|
|
127
|
+
// In the grid, where the next person would go, rather than a button under it.
|
|
128
|
+
const hire = el("button", { className: "hirecard", textContent: "Hire someone" });
|
|
129
|
+
hire.onclick = pick;
|
|
130
|
+
list.append(hire);
|
|
131
|
+
m.append(list);
|
|
132
|
+
}
|
|
40
133
|
m.append(pane);
|
|
41
134
|
|
|
135
|
+
const opened = o.open && S.data.staff.find((s) => s.handle === o.open);
|
|
136
|
+
const kept = !opened && S.staffOpen;
|
|
137
|
+
const keptStaff = kept && S.data.staff.find((s) => s.handle === kept.handle);
|
|
138
|
+
if (empty) pick();
|
|
139
|
+
if (opened) open(roleOf(opened), o.result);
|
|
140
|
+
// An open dialog is still open; only the inline fallback has to be drawn again.
|
|
141
|
+
else if (kept && !current?.isOpen()) open(keptStaff ? roleOf(keptStaff) : kept);
|
|
142
|
+
|
|
42
143
|
/* --------------------------- one staff member --------------------------- */
|
|
43
144
|
|
|
44
145
|
function card(s) {
|
|
@@ -53,143 +154,32 @@ export function viewStaff(m) {
|
|
|
53
154
|
|
|
54
155
|
const status = el("span", { className: "meta" });
|
|
55
156
|
const go = el("button", { className: "ghbtn", textContent: "Retire" });
|
|
56
|
-
go.onclick = () =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
it refers to already inside it and somewhere to put the answer. */
|
|
61
|
-
const write = el("button", { className: "ghbtn", textContent: "Write the charter" });
|
|
62
|
-
write.onclick = () => {
|
|
63
|
-
pane.replaceChildren(
|
|
64
|
-
el("div", { className: "card" }, [
|
|
65
|
-
paste({
|
|
66
|
-
kind: "charter",
|
|
67
|
-
staff: s.handle,
|
|
68
|
-
title: "Write " + s.name + "'s charter with your own AI",
|
|
69
|
-
onSaved: () => refreshAll(false),
|
|
70
|
-
}),
|
|
71
|
-
]),
|
|
72
|
-
);
|
|
73
|
-
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
157
|
+
go.onclick = () => {
|
|
158
|
+
const host = el("div");
|
|
159
|
+
present(host);
|
|
160
|
+
retireForm(host, s, status);
|
|
74
161
|
};
|
|
75
162
|
|
|
76
|
-
/*
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
|
|
163
|
+
/* Every setup step (the App, the charter, the credential, a run) is in the one list, so the
|
|
164
|
+
card has one way in. It says Finish setting up until setup looks finished: the charter
|
|
165
|
+
from disk at once, and the App's secrets and a first run from GitHub when it answers. */
|
|
166
|
+
const finish = el("button", { className: "ghbtn", textContent: "Set up" });
|
|
167
|
+
finish.onclick = () => open(roleOf(s));
|
|
168
|
+
const warn = el("p", { className: "warnline", hidden: true });
|
|
169
|
+
whatsLeft(s).then((left) => {
|
|
170
|
+
if (!left.length) return;
|
|
171
|
+
finish.textContent = "Finish setting up";
|
|
172
|
+
finish.className = "ghbtn primary";
|
|
173
|
+
d.classList.add("notready");
|
|
174
|
+
warn.hidden = false;
|
|
175
|
+
warn.textContent = "Not ready to run. Still to do: " + sentence(left) + ".";
|
|
176
|
+
});
|
|
177
|
+
d.append(warn);
|
|
89
178
|
|
|
90
|
-
d.append(
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/* ------------------------------- hiring -------------------------------- */
|
|
95
|
-
|
|
96
|
-
function hireForm(host) {
|
|
97
|
-
const box = el("div", { className: "card" });
|
|
98
|
-
box.append(
|
|
99
|
-
el("h3", { textContent: "Hire someone" }),
|
|
100
|
-
el("p", {
|
|
101
|
-
className: "sub",
|
|
102
|
-
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.",
|
|
107
|
-
}),
|
|
179
|
+
d.append(
|
|
180
|
+
el("div", { className: "row", style: "margin-top:10px" }, [finish, go, status]),
|
|
108
181
|
);
|
|
109
|
-
|
|
110
|
-
const handle = field("handle", "cfo", "lowercase, digits and dashes");
|
|
111
|
-
const name = field("name", "Chief Financial Officer", "defaults to the handle, uppercased");
|
|
112
|
-
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);
|
|
115
|
-
|
|
116
|
-
const status = el("span", { className: "meta" });
|
|
117
|
-
const plan = el("button", { className: "ghbtn primary", textContent: "Show the plan" });
|
|
118
|
-
const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
|
|
119
|
-
cancel.onclick = () => host.replaceChildren();
|
|
120
|
-
box.append(el("div", { className: "row", style: "margin-top:12px" }, [plan, cancel, status]));
|
|
121
|
-
|
|
122
|
-
const out = el("div");
|
|
123
|
-
box.append(out);
|
|
124
|
-
host.replaceChildren(box);
|
|
125
|
-
handle.input.focus();
|
|
126
|
-
|
|
127
|
-
plan.onclick = async () => {
|
|
128
|
-
if (!handle.input.value.trim()) {
|
|
129
|
-
handle.input.focus();
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
status.textContent = "planning…";
|
|
133
|
-
status.className = "meta";
|
|
134
|
-
out.replaceChildren();
|
|
135
|
-
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());
|
|
138
|
-
}
|
|
139
|
-
try {
|
|
140
|
-
const data = await (await fetch("/api/staff/plan?" + params, { cache: "no-store" })).json();
|
|
141
|
-
if (data.error) throw new Error(data.error);
|
|
142
|
-
status.textContent = "";
|
|
143
|
-
out.replaceChildren(hirePlan(data.plan, params));
|
|
144
|
-
} catch (e) {
|
|
145
|
-
status.textContent = e.message;
|
|
146
|
-
status.className = "meta err";
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function hirePlan(plan, params) {
|
|
152
|
-
const box = el("div", { className: "plan" });
|
|
153
|
-
box.append(el("div", { className: "planhead", textContent: "This would create" }));
|
|
154
|
-
|
|
155
|
-
const s = plan.staff;
|
|
156
|
-
box.append(
|
|
157
|
-
line("ok", s.brain + ", private"),
|
|
158
|
-
line("ok", plan.files.length + " files, including three caller workflows"),
|
|
159
|
-
line("ok", plan.labels.length + " labels: " + plan.labels.join(", ")),
|
|
160
|
-
line("ok", "a pinned status issue"),
|
|
161
|
-
line("ok", "runs at " + s.schedule + ", on " + s.model),
|
|
162
|
-
);
|
|
163
|
-
for (const p of plan.peers) {
|
|
164
|
-
box.append(line("ok", p.dir + "/staff.yaml gains a peer entry and a " + p.label + " label"));
|
|
165
|
-
}
|
|
166
|
-
box.append(line("ok", "org.yaml gains a staff entry and a repo entry"));
|
|
167
|
-
|
|
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));
|
|
176
|
-
|
|
177
|
-
const status = el("span", { className: "meta" });
|
|
178
|
-
const go = el("button", { className: "ghbtn primary", textContent: "Hire " + s.handle });
|
|
179
|
-
go.onclick = () =>
|
|
180
|
-
apply(
|
|
181
|
-
{
|
|
182
|
-
action: "hire",
|
|
183
|
-
handle: s.handle,
|
|
184
|
-
flags: Object.fromEntries(params),
|
|
185
|
-
},
|
|
186
|
-
"Create " + s.brain + " and wire it up?\n\nThis creates a repository on GitHub.",
|
|
187
|
-
go,
|
|
188
|
-
status,
|
|
189
|
-
box,
|
|
190
|
-
);
|
|
191
|
-
box.append(el("div", { className: "row", style: "margin-top:14px" }, [go, status]));
|
|
192
|
-
return box;
|
|
182
|
+
return d;
|
|
193
183
|
}
|
|
194
184
|
|
|
195
185
|
/* ------------------------------- retiring ------------------------------- */
|
|
@@ -234,11 +224,11 @@ export function viewStaff(m) {
|
|
|
234
224
|
const status = el("span", { className: "meta" });
|
|
235
225
|
const go = el("button", { className: "ghbtn", textContent: "Retire " + plan.handle });
|
|
236
226
|
const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
|
|
237
|
-
cancel.onclick =
|
|
227
|
+
cancel.onclick = dismiss;
|
|
238
228
|
go.onclick = () =>
|
|
239
229
|
apply(
|
|
240
230
|
{ action: "retire", handle: plan.handle },
|
|
241
|
-
"Retire " + plan.name + "
|
|
231
|
+
{ title: "Retire " + plan.name + "?", hint: (plan.brain ?? plan.dir) + " is not touched.", confirm: "Retire " + plan.handle },
|
|
242
232
|
go,
|
|
243
233
|
status,
|
|
244
234
|
box,
|
|
@@ -250,7 +240,7 @@ export function viewStaff(m) {
|
|
|
250
240
|
/* -------------------------------- apply -------------------------------- */
|
|
251
241
|
|
|
252
242
|
async function apply(payload, ask, btn, status, box) {
|
|
253
|
-
if (!
|
|
243
|
+
if (!(await askYes(ask))) return;
|
|
254
244
|
btn.disabled = true;
|
|
255
245
|
status.textContent = "working…";
|
|
256
246
|
status.className = "meta";
|
|
@@ -269,22 +259,7 @@ export function viewStaff(m) {
|
|
|
269
259
|
}
|
|
270
260
|
}
|
|
271
261
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const row = el("div", { className: "ffield" });
|
|
276
|
-
row.append(
|
|
277
|
-
el("label", { textContent: label }),
|
|
278
|
-
input,
|
|
279
|
-
el("small", { textContent: hint }),
|
|
280
|
-
);
|
|
281
|
-
return { row, input };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const MARK = { ok: "check", stop: "closed", keep: "issue-open", todo: "dash", warn: "label" };
|
|
285
|
-
|
|
286
|
-
function line(kind, text) {
|
|
287
|
-
const d = el("div", { className: "planline " + kind });
|
|
288
|
-
d.append(icon(MARK[kind] ?? "dot", "ic"), el("span", { textContent: text }));
|
|
289
|
-
return d;
|
|
262
|
+
/** The flow's view of somebody already hired. */
|
|
263
|
+
function roleOf(s) {
|
|
264
|
+
return { handle: s.handle, name: s.name, dir: s.dir };
|
|
290
265
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* The numbered step card shared by Getting started and the hiring flow on the Staff screen. */
|
|
2
|
+
|
|
3
|
+
import { el } from "../dom.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One numbered thing to do. Done ones fold to a single ticked line, and Change opens them again,
|
|
7
|
+
* so the list shows what is left without hiding what was done.
|
|
8
|
+
*
|
|
9
|
+
* `setLocked(reason)` greys a step that cannot start yet and says why in the pill; `null`
|
|
10
|
+
* unlocks it. A locked step shows no body, because a form you cannot use yet is a trap.
|
|
11
|
+
*/
|
|
12
|
+
export function todo(n, title, done) {
|
|
13
|
+
const card = el("section", { className: "todo" });
|
|
14
|
+
const num = el("span", { className: "todon" });
|
|
15
|
+
const pill = el("span", { className: "todopill" });
|
|
16
|
+
const change = el("button", { className: "ghbtn todochange", textContent: "Change" });
|
|
17
|
+
const head = el("div", { className: "todohead" }, [num, el("h3", { textContent: title }), pill, change]);
|
|
18
|
+
const hintEl = el("p", { className: "sub todohint" });
|
|
19
|
+
const body = el("div", { className: "todobody" });
|
|
20
|
+
card.append(head, hintEl, body);
|
|
21
|
+
let open = false;
|
|
22
|
+
let locked = null;
|
|
23
|
+
let isDone = done;
|
|
24
|
+
const paint = () => {
|
|
25
|
+
card.classList.toggle("done", isDone && !locked);
|
|
26
|
+
card.classList.toggle("locked", Boolean(locked));
|
|
27
|
+
num.textContent = isDone && !locked ? "✓" : String(n);
|
|
28
|
+
pill.textContent = locked ?? (isDone ? "Done" : "To do");
|
|
29
|
+
change.hidden = !isDone || Boolean(locked);
|
|
30
|
+
const shown = !locked && (!isDone || open);
|
|
31
|
+
body.hidden = !shown;
|
|
32
|
+
hintEl.hidden = !shown || !hintEl.textContent;
|
|
33
|
+
};
|
|
34
|
+
const setDone = (d) => {
|
|
35
|
+
isDone = Boolean(d);
|
|
36
|
+
paint();
|
|
37
|
+
};
|
|
38
|
+
change.onclick = () => {
|
|
39
|
+
open = !open;
|
|
40
|
+
change.textContent = open ? "Close" : "Change";
|
|
41
|
+
paint();
|
|
42
|
+
};
|
|
43
|
+
paint();
|
|
44
|
+
return {
|
|
45
|
+
card,
|
|
46
|
+
body,
|
|
47
|
+
setDone,
|
|
48
|
+
isDone: () => isDone,
|
|
49
|
+
setLocked: (reason) => {
|
|
50
|
+
locked = reason || null;
|
|
51
|
+
paint();
|
|
52
|
+
},
|
|
53
|
+
renumber: (m) => {
|
|
54
|
+
n = m;
|
|
55
|
+
paint();
|
|
56
|
+
},
|
|
57
|
+
hint: (text) => {
|
|
58
|
+
hintEl.textContent = text;
|
|
59
|
+
paint();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -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%% }}
|