@nanocollective/roster 0.1.0-alpha.1 → 0.1.0-alpha.10
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 +4687 -2495
- 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 +74 -11
- package/docs/portal.md +261 -47
- package/docs/prompts.md +71 -11
- package/docs/security.md +43 -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 +31 -10
- package/templates/portal/css/inbox.css +117 -14
- package/templates/portal/css/layout.css +92 -41
- package/templates/portal/css/markdown.css +38 -15
- package/templates/portal/css/runs.css +13 -0
- package/templates/portal/css/setup.css +50 -37
- 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/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 +20 -7
- package/templates/portal/js/views/credential.js +93 -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 +161 -61
- package/templates/portal/js/views/paste.js +29 -0
- package/templates/portal/js/views/prompt.js +57 -66
- package/templates/portal/js/views/repos.js +12 -7
- 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 +302 -56
- 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
|
@@ -13,6 +13,19 @@ import { inline } from "../md.js";
|
|
|
13
13
|
import { S } from "../state.js";
|
|
14
14
|
import { matchesFact } from "./brain.js";
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Which of the three kinds of provenance a tag is, as a class the stylesheet knows.
|
|
18
|
+
*
|
|
19
|
+
* A fact is measured, derived, or ruled on by a human — and the third one is written with that
|
|
20
|
+
* human's own marker, `[will]` or `[sam]` or whatever the org chose. The class used to be the
|
|
21
|
+
* marker itself against a stylesheet that knew one word, so every org but the one this was
|
|
22
|
+
* written in lost the "a person decided this" colour, which is the one that matters most.
|
|
23
|
+
*/
|
|
24
|
+
export function provenanceKind(provenance) {
|
|
25
|
+
const word = String(provenance).toLowerCase();
|
|
26
|
+
return word === "measured" || word === "derived" ? word : "ruled";
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
export function showMemory(viewer, s, key, pick) {
|
|
17
30
|
const section = key.startsWith("mem:") ? key.slice(4) : null;
|
|
18
31
|
const slug = key.startsWith("fact:") ? key.slice(5) : null;
|
|
@@ -61,7 +74,9 @@ export function showMemory(viewer, s, key, pick) {
|
|
|
61
74
|
d.innerHTML =
|
|
62
75
|
'<span class="slug" data-goto="' + esc(f.slug) + '" title="Show just this fact">' +
|
|
63
76
|
esc(f.slug) + "</span>" +
|
|
64
|
-
(f.provenance
|
|
77
|
+
(f.provenance
|
|
78
|
+
? '<span class="tag ' + provenanceKind(f.provenance) + '">' + esc(f.provenance) + "</span>"
|
|
79
|
+
: "") +
|
|
65
80
|
(f.note ? '<span class="notelink" data-note="' + esc(f.note) + '">' + esc(f.note) + "</span>" : "") +
|
|
66
81
|
'<p class="stmt">' + inline(f.statement) + "</p>" +
|
|
67
82
|
(f.consequence
|
|
@@ -1,27 +1,51 @@
|
|
|
1
1
|
/* The org layer, in one place.
|
|
2
2
|
*
|
|
3
|
-
* These
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* These files are what every staff member inherits. Editing `org/voice.md` once is the whole
|
|
4
|
+
* extensibility story — the thing this arrangement exists to make possible — and until recently
|
|
5
|
+
* you reached them through a staff member's Prompt screen, which is a strange route to a file
|
|
6
|
+
* that belongs to nobody in particular.
|
|
7
|
+
*
|
|
8
|
+
* The list comes off disk rather than out of this file. It used to be five paths written here,
|
|
9
|
+
* which meant a tenant that added `org/pricing.md` could not open it at all, and one that had
|
|
10
|
+
* not written `org/business.md` yet got "not found" with nothing to do about it. Both are the
|
|
11
|
+
* same mistake: a list of what a tenant *should* have, standing in for what it has.
|
|
7
12
|
*
|
|
8
13
|
* `org.yaml` is here too, and is the one file that stops every prompt composing when it is
|
|
9
14
|
* wrong. The server validates it before writing.
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
|
-
import { getFile, post } from "../api.js";
|
|
13
|
-
import {
|
|
17
|
+
import { getFile, getOrgLayer, post } from "../api.js";
|
|
18
|
+
import { askYes } from "../dialog.js";
|
|
19
|
+
import { el, esc, grow, kb, skeleton } from "../dom.js";
|
|
20
|
+
import { icon } from "../icons.js";
|
|
14
21
|
import { mdlite } from "../md.js";
|
|
15
|
-
import { S, writeHash } from "../state.js";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
import { humansOf, S, writeHash } from "../state.js";
|
|
23
|
+
import { yamlPre } from "../yaml.js";
|
|
24
|
+
import { repoPicker } from "./repos.js";
|
|
25
|
+
|
|
26
|
+
/* What the files roster itself ships are for, in one line each. A filename says nothing about
|
|
27
|
+
which one to open; anything a tenant has added of its own falls back to its first heading. */
|
|
28
|
+
const KNOWN = {
|
|
29
|
+
"org.yaml": "Who and what: the humans, the repos, the defaults, which agent runs them.",
|
|
30
|
+
"org/business.md":
|
|
31
|
+
"What the business actually is. Every prompt is composed on top of it, and it is the file that stops the agents writing generic slop.",
|
|
32
|
+
"org/priorities.md":
|
|
33
|
+
"What matters this month, ranked, and what is out of scope. Every daily run reads it, and every PR says which priority it serves.",
|
|
34
|
+
"org/operating.md":
|
|
35
|
+
"The autonomy contract: what they do without asking, what they escalate, how a run starts and ends.",
|
|
36
|
+
"org/guardrails.md":
|
|
37
|
+
"The non-negotiables. Every staff member is bound by these and cannot argue with them.",
|
|
38
|
+
"org/voice.md":
|
|
39
|
+
"How everything they write reads. Change it once, and every surface changes on the next run.",
|
|
40
|
+
"prompts/daily.md": "The whole of a scheduled run, before the fragments are pulled into it.",
|
|
41
|
+
"prompts/mention.md": "What they are sent when you @-mention them.",
|
|
42
|
+
"prompts/_identity.md": "Who they post as, and what wakes you.",
|
|
43
|
+
"prompts/_paths.md": "Where everything is in the runner's checkout.",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const GROUPS = [
|
|
47
|
+
["org", "The org layer", "Inherited by everybody, on their next run."],
|
|
48
|
+
["prompts", "The prompts", "The runs themselves. Edit these last: they are the framework's."],
|
|
25
49
|
];
|
|
26
50
|
|
|
27
51
|
export function viewOrg(m) {
|
|
@@ -37,44 +61,91 @@ export function viewOrg(m) {
|
|
|
37
61
|
);
|
|
38
62
|
|
|
39
63
|
const facts = el("div", { className: "grid", style: "margin-bottom:18px" });
|
|
40
|
-
const
|
|
64
|
+
const humans = humansOf();
|
|
41
65
|
facts.append(
|
|
42
66
|
kv("Organisation", esc(org.org), org.name),
|
|
43
|
-
|
|
67
|
+
/* Plural on purpose. An org can answer to more than one person, and a card that names one
|
|
68
|
+
of two is worse than one that names neither: it reads as the only one who counts. */
|
|
69
|
+
kv(
|
|
70
|
+
"They answer to",
|
|
71
|
+
humans.length
|
|
72
|
+
? humans.map((h) => esc(h.name ?? h.github)).join(", ")
|
|
73
|
+
: "nobody",
|
|
74
|
+
humans.length
|
|
75
|
+
? humans.map((h) => (h.github ? "@" + h.github : "no github handle")).join(" · ")
|
|
76
|
+
: "org.yaml names no human, so nothing can wake an agent",
|
|
77
|
+
),
|
|
44
78
|
kv("Staff", String(org.staff.length), org.staff.map((s) => s.handle).join(", ")),
|
|
45
79
|
);
|
|
46
80
|
m.append(facts);
|
|
47
81
|
|
|
82
|
+
/* Marking a product repo after setup. The picker only lived on the setup screen, so a repo
|
|
83
|
+
created later meant typing `role: product` into org.yaml by hand. Folded, because it asks
|
|
84
|
+
GitHub for every repo in the org and most visits here are to read a file. */
|
|
85
|
+
const repoPane = el("div");
|
|
86
|
+
const repos = el("button", { className: "ghbtn", textContent: "Add a product repo" });
|
|
87
|
+
repos.onclick = () => {
|
|
88
|
+
if (repoPane.children.length) {
|
|
89
|
+
repoPane.replaceChildren();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
repoPane.replaceChildren(repoPicker({ org: org.org, onAdded: () => open("org.yaml") }));
|
|
93
|
+
};
|
|
94
|
+
m.append(el("div", { className: "row", style: "margin-bottom:14px" }, [repos]), repoPane);
|
|
95
|
+
|
|
48
96
|
const split = el("div", { className: "split" });
|
|
49
97
|
const tree = el("div", { className: "tree" });
|
|
50
98
|
const viewer = el("div", { className: "viewer" });
|
|
51
99
|
split.append(tree, viewer);
|
|
52
100
|
m.append(split);
|
|
53
101
|
|
|
54
|
-
const group = el("div", { className: "navgroup" });
|
|
55
|
-
group.append(el("div", { className: "ghead", textContent: "The org layer" }));
|
|
56
|
-
for (const [path, why] of DOCS) {
|
|
57
|
-
const b = el("button", { className: "tfile tlayer", title: why });
|
|
58
|
-
b.dataset.key = path;
|
|
59
|
-
const t = el("span", { className: "t" });
|
|
60
|
-
t.append(
|
|
61
|
-
el("span", { className: "lname", textContent: path.replace(/^org\//, "") }),
|
|
62
|
-
el("span", { className: "lrepo", textContent: why }),
|
|
63
|
-
);
|
|
64
|
-
b.append(t);
|
|
65
|
-
b.onclick = () => open(path);
|
|
66
|
-
group.append(b);
|
|
67
|
-
}
|
|
68
|
-
tree.append(group);
|
|
69
|
-
|
|
70
|
-
/* Declared before the first `open()`, not after: `const` is not hoisted, and putting it
|
|
71
|
-
below the call made the first paint fail with "cannot access 'full' before
|
|
72
|
-
initialization" rather than rendering anything. */
|
|
73
102
|
const full = (path) => S.data.opsName + "/" + path;
|
|
74
103
|
|
|
75
|
-
|
|
104
|
+
tree.replaceChildren(...skeleton("row", 6));
|
|
105
|
+
getOrgLayer()
|
|
106
|
+
.then(({ files }) => {
|
|
107
|
+
S.orgFiles = files ?? [];
|
|
108
|
+
paintTree();
|
|
109
|
+
const first = S.orgFiles[0]?.path;
|
|
110
|
+
open(S.orgFiles.some((f) => f.path === S.orgOpen) ? S.orgOpen : first);
|
|
111
|
+
})
|
|
112
|
+
.catch((e) => {
|
|
113
|
+
tree.replaceChildren(el("p", { className: "empty err", textContent: e.message }));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function why(file) {
|
|
117
|
+
return KNOWN[file.path] ?? file.title ?? "";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function paintTree() {
|
|
121
|
+
tree.replaceChildren();
|
|
122
|
+
if (!S.orgFiles.length) {
|
|
123
|
+
tree.append(el("p", { className: "empty", textContent: "No org files here." }));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const [id, heading, note] of GROUPS) {
|
|
127
|
+
const mine = S.orgFiles.filter((f) => (f.group ?? "org") === id);
|
|
128
|
+
if (!mine.length) continue;
|
|
129
|
+
const group = el("div", { className: "navgroup" });
|
|
130
|
+
group.append(el("div", { className: "ghead", textContent: heading, title: note }));
|
|
131
|
+
for (const f of mine) {
|
|
132
|
+
const b = el("button", { className: "tfile tlayer", title: why(f) });
|
|
133
|
+
b.dataset.key = f.path;
|
|
134
|
+
const t = el("span", { className: "t" });
|
|
135
|
+
t.append(
|
|
136
|
+
el("span", { className: "lname", textContent: f.path.replace(/^(org|prompts)\//, "") }),
|
|
137
|
+
el("span", { className: "lrepo", textContent: why(f) }),
|
|
138
|
+
);
|
|
139
|
+
b.append(t);
|
|
140
|
+
b.onclick = () => open(f.path);
|
|
141
|
+
group.append(b);
|
|
142
|
+
}
|
|
143
|
+
tree.append(group);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
76
146
|
|
|
77
147
|
function open(path) {
|
|
148
|
+
if (!path) return;
|
|
78
149
|
S.orgOpen = path;
|
|
79
150
|
writeHash(false);
|
|
80
151
|
for (const o of tree.querySelectorAll(".tfile")) {
|
|
@@ -84,7 +155,7 @@ export function viewOrg(m) {
|
|
|
84
155
|
}
|
|
85
156
|
|
|
86
157
|
async function show(path) {
|
|
87
|
-
viewer.replaceChildren(
|
|
158
|
+
viewer.replaceChildren(...skeleton("head", 1), ...skeleton("line", 7));
|
|
88
159
|
let text;
|
|
89
160
|
try {
|
|
90
161
|
text = await getFile(full(path));
|
|
@@ -97,55 +168,84 @@ export function viewOrg(m) {
|
|
|
97
168
|
|
|
98
169
|
function render(path, text) {
|
|
99
170
|
viewer.replaceChildren();
|
|
100
|
-
const
|
|
101
|
-
const head = el("div", { className: "row", style: "margin-bottom:4px" });
|
|
171
|
+
const head = el("div", { className: "row filehead" });
|
|
102
172
|
head.append(el("span", { className: "meta", textContent: full(path) + " · " + kb(text.length) }));
|
|
103
|
-
|
|
104
|
-
|
|
173
|
+
/* The edit control used to be a word in a row of grey text, which is why "there is no easy
|
|
174
|
+
way to edit the org docs" was a fair thing to say about a screen that could edit them. */
|
|
175
|
+
const edit = el("button", { className: "ghbtn primary editbtn", onclick: () => editor(path, text) });
|
|
176
|
+
edit.append(icon("edit", "ic"), el("span", { textContent: "Edit" }));
|
|
177
|
+
head.append(edit);
|
|
178
|
+
const file = S.orgFiles.find((f) => f.path === path) ?? { path };
|
|
179
|
+
viewer.append(head, el("p", { className: "factsub", textContent: why(file) }));
|
|
105
180
|
|
|
106
|
-
if (path.endsWith(".yaml")) {
|
|
107
|
-
viewer.append(
|
|
181
|
+
if (path.endsWith(".yaml") || path.endsWith(".yml")) {
|
|
182
|
+
viewer.append(yamlPre(text));
|
|
108
183
|
return;
|
|
109
184
|
}
|
|
110
185
|
viewer.append(el("div", { className: "md doc", innerHTML: mdlite(text) }));
|
|
111
186
|
}
|
|
112
187
|
|
|
113
|
-
function
|
|
188
|
+
function editor(path, text) {
|
|
114
189
|
viewer.replaceChildren();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
);
|
|
190
|
+
const isYaml = /\.ya?ml$/.test(path);
|
|
191
|
+
|
|
192
|
+
const status = el("span", { className: "meta" });
|
|
193
|
+
const save = el("button", { className: "ghbtn primary", textContent: "Save and commit" });
|
|
194
|
+
const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
|
|
195
|
+
|
|
196
|
+
const bar = el("div", { className: "row filehead" });
|
|
197
|
+
bar.append(el("span", { className: "meta", textContent: full(path) }), save, cancel, status);
|
|
198
|
+
viewer.append(bar);
|
|
120
199
|
|
|
121
200
|
const ta = el("textarea", { value: text, className: "editor" });
|
|
122
201
|
viewer.append(ta);
|
|
123
202
|
grow(ta);
|
|
124
203
|
ta.addEventListener("input", () => grow(ta));
|
|
204
|
+
// ⌘S is what a person's hands do in a text box. Without it, saving means finding a button
|
|
205
|
+
// above a screenful of textarea you have just scrolled past.
|
|
206
|
+
ta.addEventListener("keydown", (e) => {
|
|
207
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
commit();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
ta.focus?.();
|
|
125
213
|
|
|
126
|
-
const status = el("span", { className: "meta" });
|
|
127
|
-
const save = el("button", { className: "ghbtn primary", textContent: "Save and commit" });
|
|
128
|
-
const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
|
|
129
214
|
cancel.onclick = () => render(path, text);
|
|
130
|
-
|
|
215
|
+
save.onclick = commit;
|
|
216
|
+
|
|
131
217
|
viewer.append(
|
|
132
218
|
el("p", {
|
|
133
219
|
className: "editnote",
|
|
134
220
|
textContent:
|
|
135
|
-
"Commits to " + S.data.opsName + " and pushes
|
|
136
|
-
"next run." +
|
|
137
|
-
(
|
|
221
|
+
"⌘S saves. Commits to " + S.data.opsName + " and pushes, and every staff member picks " +
|
|
222
|
+
"it up on their next run." +
|
|
223
|
+
(isYaml
|
|
138
224
|
? " org.yaml is checked before it is written: bad YAML, or a missing org or name, is refused rather than committed."
|
|
139
225
|
: ""),
|
|
140
226
|
}),
|
|
141
227
|
);
|
|
142
228
|
|
|
143
|
-
|
|
229
|
+
async function commit() {
|
|
144
230
|
if (ta.value === text) {
|
|
145
231
|
status.textContent = "nothing changed";
|
|
232
|
+
status.className = "meta";
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
/* Only the manifest asks. A prose edit is one commit to revert and the agents pick it up
|
|
236
|
+
on their next run; org.yaml is the file that can stop every prompt composing, and the
|
|
237
|
+
portal has just told you it validates it. Asking every time taught people to click
|
|
238
|
+
through the question, which is worse than not asking. */
|
|
239
|
+
if (
|
|
240
|
+
isYaml &&
|
|
241
|
+
!(await askYes({
|
|
242
|
+
title: "Commit " + full(path) + " and push?",
|
|
243
|
+
hint: "Every staff member picks this up on their next run.",
|
|
244
|
+
confirm: "Commit and push",
|
|
245
|
+
}))
|
|
246
|
+
) {
|
|
146
247
|
return;
|
|
147
248
|
}
|
|
148
|
-
if (!confirm("Commit " + full(path) + " and push?\n\nEvery staff member picks this up on their next run.")) return;
|
|
149
249
|
save.disabled = cancel.disabled = true;
|
|
150
250
|
status.textContent = "committing…";
|
|
151
251
|
status.className = "meta";
|
|
@@ -162,7 +262,7 @@ export function viewOrg(m) {
|
|
|
162
262
|
status.className = "meta err";
|
|
163
263
|
save.disabled = cancel.disabled = false;
|
|
164
264
|
}
|
|
165
|
-
}
|
|
265
|
+
}
|
|
166
266
|
}
|
|
167
267
|
}
|
|
168
268
|
|
|
@@ -20,6 +20,12 @@ export function paste(opts) {
|
|
|
20
20
|
const status = el("p", { className: "sub", textContent: "Building the prompt…" });
|
|
21
21
|
box.append(status);
|
|
22
22
|
|
|
23
|
+
/* A charter brief carries a worked example to model the shape on, matched to the role. The
|
|
24
|
+
choice is on the page because the match is a guess, and "none" is a fair answer. */
|
|
25
|
+
const model = el("select", { hidden: true });
|
|
26
|
+
const modelRow = el("label", { className: "sub", hidden: true }, ["Model it on: ", model]);
|
|
27
|
+
box.append(modelRow);
|
|
28
|
+
|
|
23
29
|
const actions = el("div", { className: "row" });
|
|
24
30
|
const copy = el("button", { className: "btn primary", textContent: "Copy the prompt" });
|
|
25
31
|
const show = el("button", { className: "btn", textContent: "Show it" });
|
|
@@ -42,10 +48,33 @@ export function paste(opts) {
|
|
|
42
48
|
|
|
43
49
|
let brief = null;
|
|
44
50
|
|
|
51
|
+
const NAMES = { cto: "the CTO example", cmo: "the CMO example", support: "the support example", none: "no example" };
|
|
52
|
+
model.onchange = () => {
|
|
53
|
+
copy.disabled = true;
|
|
54
|
+
getBrief(opts.kind, opts.staff, model.value)
|
|
55
|
+
.then((data) => {
|
|
56
|
+
if (data.error) throw new Error(data.error);
|
|
57
|
+
brief = data;
|
|
58
|
+
if (!preview.hidden) preview.textContent = brief.text;
|
|
59
|
+
})
|
|
60
|
+
.catch((err) => {
|
|
61
|
+
status.className = "err";
|
|
62
|
+
status.textContent = String(err.message || err);
|
|
63
|
+
})
|
|
64
|
+
.finally(() => {
|
|
65
|
+
copy.disabled = false;
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
45
69
|
getBrief(opts.kind, opts.staff)
|
|
46
70
|
.then((data) => {
|
|
47
71
|
if (data.error) throw new Error(data.error);
|
|
48
72
|
brief = data;
|
|
73
|
+
if (data.examples) {
|
|
74
|
+
model.replaceChildren(...data.examples.map((e) => el("option", { value: e, textContent: NAMES[e] ?? e })));
|
|
75
|
+
model.value = data.example ?? "none";
|
|
76
|
+
model.hidden = modelRow.hidden = false;
|
|
77
|
+
}
|
|
49
78
|
status.textContent =
|
|
50
79
|
"Paste this into Claude, ChatGPT or anything else. It carries every file it refers to, " +
|
|
51
80
|
"so there is nothing to attach and nothing for it to ask you for. It will interview you " +
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { getFile, post } from "../api.js";
|
|
13
|
-
import { askText } from "../dialog.js";
|
|
13
|
+
import { askText, askYes } from "../dialog.js";
|
|
14
14
|
import { el, esc, grow, kb, toClipboard } from "../dom.js";
|
|
15
15
|
import { icon } from "../icons.js";
|
|
16
16
|
import { mdlite } from "../md.js";
|
|
17
|
+
import { render } from "../router.js";
|
|
17
18
|
import { S, staff, writeHash } from "../state.js";
|
|
18
19
|
import { diffStat, unifiedDiff } from "../textdiff.js";
|
|
19
20
|
import { renderDiff } from "./changed.js";
|
|
@@ -21,9 +22,53 @@ import { renderDiff } from "./changed.js";
|
|
|
21
22
|
const KINDS = [
|
|
22
23
|
["daily", "Daily", "the scheduled run"],
|
|
23
24
|
["mention", "Mention", "someone typed @them in an issue"],
|
|
24
|
-
["pr-mention", "PR mention", "someone typed @them on a pull request"],
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Build the paste-ready brief on the server, where compose.mjs lives, and copy it.
|
|
29
|
+
*
|
|
30
|
+
* A finding pre-fills the box rather than skipping it: what it wrote is a starting point, and
|
|
31
|
+
* "and keep it in operating.md" is exactly the sort of thing you want to add.
|
|
32
|
+
*
|
|
33
|
+
* Exported because the findings themselves live on Health now, and the fix for one of them is
|
|
34
|
+
* the same brief whichever screen you asked from.
|
|
35
|
+
*/
|
|
36
|
+
export async function copyAmendBrief(handle, kind, want, btn, note) {
|
|
37
|
+
const label = btn.textContent;
|
|
38
|
+
const asked = await askText({
|
|
39
|
+
title: "What do you want changed?",
|
|
40
|
+
hint:
|
|
41
|
+
"This goes at the top of a brief carrying the whole prompt and every file it is " +
|
|
42
|
+
"made of. Say it the way you would say it to a person.",
|
|
43
|
+
value: want,
|
|
44
|
+
placeholder: "stop opening decision issues for anything reversible",
|
|
45
|
+
confirm: "Copy the brief",
|
|
46
|
+
});
|
|
47
|
+
if (!asked) return;
|
|
48
|
+
btn.disabled = true;
|
|
49
|
+
btn.textContent = "building…";
|
|
50
|
+
try {
|
|
51
|
+
const url =
|
|
52
|
+
"/api/amend?staff=" + encodeURIComponent(handle) +
|
|
53
|
+
"&kind=" + encodeURIComponent(kind) +
|
|
54
|
+
"&want=" + encodeURIComponent(asked);
|
|
55
|
+
const text = await (await fetch(url, { cache: "no-store" })).text();
|
|
56
|
+
btn.disabled = false;
|
|
57
|
+
toClipboard(text, btn, label);
|
|
58
|
+
if (note) {
|
|
59
|
+
note.textContent = "paste it into whatever agent you use";
|
|
60
|
+
note.className = "meta";
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
btn.disabled = false;
|
|
64
|
+
btn.textContent = label;
|
|
65
|
+
if (note) {
|
|
66
|
+
note.textContent = e.message;
|
|
67
|
+
note.className = "meta err";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
27
72
|
export function viewPrompt(m) {
|
|
28
73
|
const s = staff();
|
|
29
74
|
if (!S.promptKind) S.promptKind = "daily";
|
|
@@ -54,40 +99,7 @@ export function viewPrompt(m) {
|
|
|
54
99
|
const note = el("span", { className: "meta" });
|
|
55
100
|
m.append(el("div", { className: "row", style: "margin-bottom:16px" }, [kind, help, copy, note]));
|
|
56
101
|
|
|
57
|
-
|
|
58
|
-
A finding pre-fills the box rather than skipping it: what it wrote is a starting point,
|
|
59
|
-
and "and keep it in operating.md" is exactly the sort of thing you want to add. */
|
|
60
|
-
async function copyAmend(want, btn) {
|
|
61
|
-
const target = btn ?? help;
|
|
62
|
-
const label = target.textContent;
|
|
63
|
-
const asked = await askText({
|
|
64
|
-
title: "What do you want changed?",
|
|
65
|
-
hint:
|
|
66
|
-
"This goes at the top of a brief carrying the whole prompt and every file it is " +
|
|
67
|
-
"made of. Say it the way you would say it to a person.",
|
|
68
|
-
value: want,
|
|
69
|
-
placeholder: "stop opening decision issues for anything reversible",
|
|
70
|
-
confirm: "Copy the brief",
|
|
71
|
-
});
|
|
72
|
-
if (!asked) return;
|
|
73
|
-
target.disabled = true;
|
|
74
|
-
target.textContent = "building…";
|
|
75
|
-
try {
|
|
76
|
-
const url =
|
|
77
|
-
"/api/amend?staff=" + encodeURIComponent(s.handle) +
|
|
78
|
-
"&kind=" + encodeURIComponent(S.promptKind) +
|
|
79
|
-
"&want=" + encodeURIComponent(asked);
|
|
80
|
-
const text = await (await fetch(url, { cache: "no-store" })).text();
|
|
81
|
-
target.disabled = false;
|
|
82
|
-
toClipboard(text, target, label);
|
|
83
|
-
note.textContent = "paste it into whatever agent you use";
|
|
84
|
-
} catch (e) {
|
|
85
|
-
target.disabled = false;
|
|
86
|
-
target.textContent = label;
|
|
87
|
-
note.textContent = e.message;
|
|
88
|
-
note.className = "meta err";
|
|
89
|
-
}
|
|
90
|
-
}
|
|
102
|
+
const copyAmend = (want, btn) => copyAmendBrief(s.handle, S.promptKind, want, btn ?? help, note);
|
|
91
103
|
|
|
92
104
|
const split = el("div", { className: "split" });
|
|
93
105
|
const tree = el("div", { className: "tree" });
|
|
@@ -140,12 +152,9 @@ export function viewPrompt(m) {
|
|
|
140
152
|
for (const l of view.layers) inlined.append(layerRow(l));
|
|
141
153
|
tree.append(inlined);
|
|
142
154
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
tree.append(bad);
|
|
147
|
-
}
|
|
148
|
-
|
|
155
|
+
/* No Problems section. This screen answers "what is sent"; whether what is sent is any
|
|
156
|
+
good is a health question, and it is asked and answered on Health. Having both here
|
|
157
|
+
made the tree beside the prompt half prompt and half complaints. */
|
|
149
158
|
const named = group("Named, not inlined");
|
|
150
159
|
named.append(
|
|
151
160
|
el("p", {
|
|
@@ -160,28 +169,6 @@ export function viewPrompt(m) {
|
|
|
160
169
|
open(S.promptOpen ?? "composed");
|
|
161
170
|
}
|
|
162
171
|
|
|
163
|
-
/* A finding is only half useful. The other half is the sentence that goes into the brief,
|
|
164
|
-
which is why every one of them carries a `want`. */
|
|
165
|
-
function problemRow(p) {
|
|
166
|
-
const b = el("div", { className: "prob " + p.level });
|
|
167
|
-
b.append(el("div", { className: "ptitle", textContent: p.title }));
|
|
168
|
-
b.append(el("div", { className: "pdetail", textContent: p.detail }));
|
|
169
|
-
const fix = el("button", { className: "ghbtn", textContent: "Copy a prompt to fix this" });
|
|
170
|
-
fix.onclick = () => copyAmend(p.want, fix);
|
|
171
|
-
const row = el("div", { className: "row", style: "margin-top:8px" }, [fix]);
|
|
172
|
-
if (p.path) {
|
|
173
|
-
row.append(
|
|
174
|
-
el("button", {
|
|
175
|
-
className: "ghbtn",
|
|
176
|
-
textContent: "Open the file",
|
|
177
|
-
onclick: () => open(p.path),
|
|
178
|
-
}),
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
b.append(row);
|
|
182
|
-
return b;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
172
|
function group(title, right) {
|
|
186
173
|
const g = el("div", { className: "navgroup" });
|
|
187
174
|
const h = el("div", { className: "ghead", textContent: title });
|
|
@@ -388,8 +375,12 @@ export function viewPrompt(m) {
|
|
|
388
375
|
status.textContent = "nothing changed";
|
|
389
376
|
return;
|
|
390
377
|
}
|
|
391
|
-
|
|
392
|
-
|
|
378
|
+
const yes = await askYes({
|
|
379
|
+
title: "Commit " + layer.path + " to " + layer.repo + " and push?",
|
|
380
|
+
hint: "Picked up by " + reach + ".",
|
|
381
|
+
confirm: "Commit and push",
|
|
382
|
+
});
|
|
383
|
+
if (!yes) return;
|
|
393
384
|
save.disabled = cancel.disabled = true;
|
|
394
385
|
status.textContent = "committing…";
|
|
395
386
|
status.className = "meta";
|
|
@@ -5,11 +5,12 @@
|
|
|
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
|
|
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
16
|
const box = el("div", { className: "manual" });
|
|
@@ -18,7 +19,8 @@ export function repoPicker(opts) {
|
|
|
18
19
|
el("p", {
|
|
19
20
|
textContent:
|
|
20
21
|
"Repos marked as products are the ones staff contribute to without owning. A new hire " +
|
|
21
|
-
"picks them up automatically
|
|
22
|
+
"picks them up automatically; somebody already hired keeps the works_in list in their " +
|
|
23
|
+
"own staff.yaml.",
|
|
22
24
|
}),
|
|
23
25
|
);
|
|
24
26
|
|
|
@@ -26,10 +28,13 @@ export function repoPicker(opts) {
|
|
|
26
28
|
box.append(out);
|
|
27
29
|
out.replaceChildren(el("p", { className: "sub", textContent: "Reading your repos…" }));
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
.
|
|
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(
|
|
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?.();
|