@nanocollective/roster 0.1.0-alpha.5 → 0.1.0-alpha.51
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 +5161 -3012
- package/docs/README.md +9 -6
- package/docs/agents.md +24 -20
- 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 +93 -7
- package/docs/concepts.md +61 -14
- package/docs/cost.md +36 -1
- package/docs/developing.md +16 -21
- package/docs/doctor-codes.md +10 -2
- package/docs/export.md +2 -0
- package/docs/extending.md +2 -2
- package/docs/getting-started.md +128 -78
- 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 -123
- package/docs/memory.md +21 -3
- package/docs/org-yaml.md +40 -2
- package/docs/portal.md +177 -58
- package/docs/prompts.md +31 -4
- package/docs/security.md +37 -5
- package/docs/session-workflow.md +63 -17
- package/docs/staff-yaml.md +30 -3
- package/docs/troubleshooting.md +8 -8
- package/docs/upgrading.md +9 -3
- package/docs/writing-a-charter.md +28 -0
- package/package.json +18 -20
- package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +26 -1
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +59 -13
- 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 +4 -1
- package/templates/brain/strategy/ideas.md +7 -0
- package/templates/briefs/amend.md +4 -3
- package/templates/briefs/priorities.md +46 -0
- package/templates/ops/.github/workflows/session.yaml +236 -15
- package/templates/ops/agents.mjs +7 -3
- package/templates/ops/compose.mjs +31 -3
- package/templates/ops/inflight.mjs +157 -0
- package/templates/ops/org/operating.md +43 -4
- package/templates/ops/org/voice.md +9 -0
- package/templates/ops/prompts/_inflight.md +14 -0
- package/templates/ops/prompts/_paths.md +2 -1
- package/templates/ops/prompts/daily.md +37 -9
- package/templates/ops/prompts/mention.md +21 -0
- package/templates/ops/run-record.mjs +146 -0
- package/templates/portal/css/base.css +167 -73
- package/templates/portal/css/brain.css +23 -20
- package/templates/portal/css/diff.css +10 -9
- package/templates/portal/css/graph.css +12 -7
- package/templates/portal/css/health.css +26 -11
- package/templates/portal/css/home.css +95 -0
- package/templates/portal/css/inbox.css +45 -25
- package/templates/portal/css/layout.css +90 -46
- package/templates/portal/css/markdown.css +36 -14
- package/templates/portal/css/runs.css +13 -0
- package/templates/portal/css/setup.css +117 -34
- package/templates/portal/index.html +21 -9
- package/templates/portal/js/api.js +44 -4
- package/templates/portal/js/app.js +94 -9
- package/templates/portal/js/dialog.js +83 -0
- package/templates/portal/js/homesort.js +174 -0
- package/templates/portal/js/icons.js +37 -0
- package/templates/portal/js/inflight.js +18 -0
- package/templates/portal/js/md.js +5 -2
- package/templates/portal/js/mdedit.js +84 -0
- package/templates/portal/js/readiness.js +70 -0
- package/templates/portal/js/refresh.js +10 -2
- package/templates/portal/js/state.js +11 -5
- package/templates/portal/js/views/app.js +24 -7
- package/templates/portal/js/views/brain.js +1 -1
- package/templates/portal/js/views/checklist.js +10 -4
- package/templates/portal/js/views/credential.js +84 -0
- package/templates/portal/js/views/graph.js +1 -1
- package/templates/portal/js/views/health.js +27 -9
- package/templates/portal/js/views/hire.js +593 -0
- package/templates/portal/js/views/home.js +546 -0
- package/templates/portal/js/views/inbox.js +226 -70
- package/templates/portal/js/views/org.js +46 -106
- package/templates/portal/js/views/orgedit.js +234 -0
- package/templates/portal/js/views/paste.js +137 -63
- package/templates/portal/js/views/prompt.js +100 -42
- 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 +170 -0
- package/templates/portal/js/views/setup.js +261 -75
- package/templates/portal/js/views/staff.js +170 -243
- package/templates/portal/js/views/todo.js +62 -0
|
@@ -7,19 +7,27 @@
|
|
|
7
7
|
* Nothing here writes. The panel parses, shows a diff, and waits for a second click. */
|
|
8
8
|
|
|
9
9
|
import { getBrief, parsePaste, saveFile } from "../api.js";
|
|
10
|
+
import { mdlite } from "../md.js";
|
|
10
11
|
import { el, toClipboard } from "../dom.js";
|
|
11
12
|
import { unifiedDiff, diffStat } from "../textdiff.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* @param {{kind: string, staff?: string, title?: string, onSaved?: () => void}} opts
|
|
15
|
+
* @param {{kind: string, staff?: string, title?: string, about?: string, onSaved?: () => void}} opts
|
|
16
|
+
* `about` is a sentence on what the role does, added to a charter brief.
|
|
15
17
|
*/
|
|
16
18
|
export function paste(opts) {
|
|
17
19
|
const box = el("div", { className: "paste" });
|
|
18
|
-
box.append(el("h3", { textContent: opts.title ?? "Write it with your own AI" }));
|
|
20
|
+
if (opts.title !== "") box.append(el("h3", { textContent: opts.title ?? "Write it with your own AI" }));
|
|
19
21
|
|
|
20
22
|
const status = el("p", { className: "sub", textContent: "Building the prompt…" });
|
|
21
23
|
box.append(status);
|
|
22
24
|
|
|
25
|
+
/* A charter brief carries a worked example to model the shape on, matched to the role. The
|
|
26
|
+
choice is on the page because the match is a guess, and "none" is a fair answer. */
|
|
27
|
+
const model = el("select", { hidden: true });
|
|
28
|
+
const modelRow = el("label", { className: "sub", hidden: true }, ["Example to follow: ", model]);
|
|
29
|
+
box.append(modelRow);
|
|
30
|
+
|
|
23
31
|
const actions = el("div", { className: "row" });
|
|
24
32
|
const copy = el("button", { className: "btn primary", textContent: "Copy the prompt" });
|
|
25
33
|
const show = el("button", { className: "btn", textContent: "Show it" });
|
|
@@ -32,8 +40,7 @@ export function paste(opts) {
|
|
|
32
40
|
const answer = el("textarea", {
|
|
33
41
|
className: "pastebox",
|
|
34
42
|
placeholder:
|
|
35
|
-
"Paste
|
|
36
|
-
"Only the part between the sentinels is used.",
|
|
43
|
+
"Paste its whole reply here.",
|
|
37
44
|
});
|
|
38
45
|
answer.rows = 8;
|
|
39
46
|
|
|
@@ -42,16 +49,49 @@ export function paste(opts) {
|
|
|
42
49
|
|
|
43
50
|
let brief = null;
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
const NAMES = {
|
|
53
|
+
cto: "the CTO example",
|
|
54
|
+
cmo: "the CMO example",
|
|
55
|
+
support: "the support example",
|
|
56
|
+
pm: "the product manager example",
|
|
57
|
+
designer: "the designer example",
|
|
58
|
+
qa: "the QA example",
|
|
59
|
+
devops: "the DevOps example",
|
|
60
|
+
writer: "the writer example",
|
|
61
|
+
analyst: "the analyst example",
|
|
62
|
+
community: "the community example",
|
|
63
|
+
none: "no example",
|
|
64
|
+
};
|
|
65
|
+
model.onchange = () => {
|
|
66
|
+
copy.disabled = true;
|
|
67
|
+
getBrief(opts.kind, opts.staff, model.value, opts.about)
|
|
68
|
+
.then((data) => {
|
|
69
|
+
if (data.error) throw new Error(data.error);
|
|
70
|
+
brief = data;
|
|
71
|
+
if (!preview.hidden) preview.textContent = brief.text;
|
|
72
|
+
})
|
|
73
|
+
.catch((err) => {
|
|
74
|
+
status.className = "err";
|
|
75
|
+
status.textContent = String(err.message || err);
|
|
76
|
+
})
|
|
77
|
+
.finally(() => {
|
|
78
|
+
copy.disabled = false;
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
getBrief(opts.kind, opts.staff, undefined, opts.about)
|
|
46
83
|
.then((data) => {
|
|
47
84
|
if (data.error) throw new Error(data.error);
|
|
48
85
|
brief = data;
|
|
86
|
+
if (data.examples) {
|
|
87
|
+
model.replaceChildren(...data.examples.map((e) => el("option", { value: e, textContent: NAMES[e] ?? e })));
|
|
88
|
+
model.value = data.example ?? "none";
|
|
89
|
+
model.hidden = modelRow.hidden = false;
|
|
90
|
+
}
|
|
49
91
|
status.textContent =
|
|
50
|
-
"
|
|
51
|
-
"so there is nothing to attach and nothing for it to ask you for. It will interview you " +
|
|
52
|
-
"first, then hand back the finished file.";
|
|
92
|
+
"Copy the prompt into Claude or ChatGPT and answer its questions.";
|
|
53
93
|
box.append(
|
|
54
|
-
el("p", { className: "sub", textContent: "Then paste its
|
|
94
|
+
el("p", { className: "sub", textContent: "Then paste its reply here:" }),
|
|
55
95
|
answer,
|
|
56
96
|
el("div", { className: "row" }, [check]),
|
|
57
97
|
result,
|
|
@@ -80,63 +120,97 @@ export function paste(opts) {
|
|
|
80
120
|
result.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
81
121
|
return;
|
|
82
122
|
}
|
|
83
|
-
result.
|
|
84
|
-
|
|
85
|
-
/* Problems first, each with the sentence that gets a better answer. A model that replied in
|
|
86
|
-
prose is the common case and it is not the person's mistake, so it reads as a next step
|
|
87
|
-
rather than as an error. */
|
|
88
|
-
for (const p of data.problems ?? []) {
|
|
89
|
-
const note = el("div", { className: "problem" });
|
|
90
|
-
note.append(el("b", { textContent: p.message }));
|
|
91
|
-
if (p.retry) {
|
|
92
|
-
const retry = el("pre", { className: "cmd", textContent: p.retry });
|
|
93
|
-
const again = el("button", { className: "btn", textContent: "Copy this reply" });
|
|
94
|
-
again.onclick = () => toClipboard(p.retry, again, "Copied");
|
|
95
|
-
note.append(retry, again);
|
|
96
|
-
}
|
|
97
|
-
result.append(note);
|
|
98
|
-
}
|
|
123
|
+
showPasteResult(result, data, answer.value, opts.onSaved);
|
|
124
|
+
};
|
|
99
125
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
return box;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* What a pasted reply turned into: problems first, then each file with its diff and a Save.
|
|
131
|
+
* Shared by every copy-a-prompt, paste-the-answer loop, whichever endpoint parsed it.
|
|
132
|
+
*/
|
|
133
|
+
export function showPasteResult(result, data, answerText, onSaved) {
|
|
134
|
+
result.replaceChildren();
|
|
135
|
+
|
|
136
|
+
/* Problems first, each with the sentence that gets a better answer. A model that replied in
|
|
137
|
+
prose is the common case and it is not the person's mistake, so it reads as a next step
|
|
138
|
+
rather than as an error. */
|
|
139
|
+
for (const p of data.problems ?? []) {
|
|
140
|
+
const note = el("div", { className: "problem" });
|
|
141
|
+
note.append(el("b", { textContent: p.message }));
|
|
142
|
+
if (p.retry) {
|
|
143
|
+
const retry = el("pre", { className: "cmd", textContent: p.retry });
|
|
144
|
+
const again = el("button", { className: "btn", textContent: "Copy this reply" });
|
|
145
|
+
again.onclick = () => toClipboard(p.retry, again, "Copied");
|
|
146
|
+
note.append(retry, again);
|
|
147
|
+
}
|
|
148
|
+
result.append(note);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// What the AI said around the file: its caveats and "check these" are worth reading first.
|
|
152
|
+
const notes = String(answerText ?? "")
|
|
153
|
+
.replace(/<<<ROSTER FILE [^>]+>>>[\s\S]*?<<<ROSTER END>>>/g, "")
|
|
154
|
+
.trim();
|
|
155
|
+
|
|
156
|
+
for (const file of data.files ?? []) {
|
|
157
|
+
const card = el("div", { className: "pastefile" });
|
|
158
|
+
const name = file.path.split("/").slice(-2).join("/");
|
|
159
|
+
if (file.unchanged) {
|
|
160
|
+
card.append(el("p", { className: "verdict", textContent: "✓ Same as what's already in " + name + ". Nothing to save." }));
|
|
133
161
|
result.append(card);
|
|
162
|
+
continue;
|
|
134
163
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
164
|
+
card.append(
|
|
165
|
+
el("p", {
|
|
166
|
+
className: "verdict" + (file.writable ? " ok" : " bad"),
|
|
167
|
+
textContent: file.writable
|
|
168
|
+
? "✓ Looks good. Saving replaces " + name + " with this:"
|
|
169
|
+
: "✗ " + file.path + " can't be written from here.",
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
card.append(el("div", { className: "md doc pastepreview", innerHTML: mdlite(file.text) }));
|
|
173
|
+
|
|
174
|
+
if (notes) {
|
|
175
|
+
const said = el("details", { className: "pastenotes", open: true });
|
|
176
|
+
said.append(el("summary", { textContent: "Your AI also said" }), el("div", { className: "md doc", innerHTML: mdlite(notes) }));
|
|
177
|
+
card.append(said);
|
|
138
178
|
}
|
|
139
|
-
};
|
|
140
179
|
|
|
141
|
-
|
|
180
|
+
const diff = el("pre", { className: "diff", hidden: true });
|
|
181
|
+
diff.textContent = unifiedDiff(file.before, file.text, file.path);
|
|
182
|
+
const stat = diffStat(file.before, file.text);
|
|
183
|
+
const toggle = el("button", { className: "ghbtn", textContent: `Show changes (+${stat.added} −${stat.removed})` });
|
|
184
|
+
toggle.onclick = () => {
|
|
185
|
+
diff.hidden = !diff.hidden;
|
|
186
|
+
toggle.textContent = (diff.hidden ? "Show changes" : "Hide changes") + ` (+${stat.added} −${stat.removed})`;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const row = el("div", { className: "row" });
|
|
190
|
+
if (file.writable) {
|
|
191
|
+
const save = el("button", { className: "btn primary", textContent: "Save" });
|
|
192
|
+
save.onclick = async () => {
|
|
193
|
+
save.disabled = true;
|
|
194
|
+
save.textContent = "Saving…";
|
|
195
|
+
try {
|
|
196
|
+
await saveFile(file.path, file.text, `portal: write ${file.path}`);
|
|
197
|
+
save.textContent = "Saved";
|
|
198
|
+
card.classList.add("saved");
|
|
199
|
+
onSaved?.();
|
|
200
|
+
} catch (err) {
|
|
201
|
+
save.disabled = false;
|
|
202
|
+
save.textContent = "Save";
|
|
203
|
+
card.append(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
row.append(save);
|
|
207
|
+
}
|
|
208
|
+
row.append(toggle);
|
|
209
|
+
card.append(row, diff);
|
|
210
|
+
result.append(card);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!(data.problems ?? []).length && !(data.files ?? []).length) {
|
|
214
|
+
result.append(el("p", { className: "verdict bad", textContent: "✗ No file found in that reply." }));
|
|
215
|
+
}
|
|
142
216
|
}
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* changing a byte of the composed text, and nothing said so before.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { highlighted } from "../mdedit.js";
|
|
12
13
|
import { getFile, post } from "../api.js";
|
|
13
|
-
import {
|
|
14
|
+
import { askYes, sheet } from "../dialog.js";
|
|
14
15
|
import { el, esc, grow, kb, toClipboard } from "../dom.js";
|
|
15
16
|
import { icon } from "../icons.js";
|
|
16
17
|
import { mdlite } from "../md.js";
|
|
@@ -18,6 +19,7 @@ import { render } from "../router.js";
|
|
|
18
19
|
import { S, staff, writeHash } from "../state.js";
|
|
19
20
|
import { diffStat, unifiedDiff } from "../textdiff.js";
|
|
20
21
|
import { renderDiff } from "./changed.js";
|
|
22
|
+
import { showPasteResult } from "./paste.js";
|
|
21
23
|
|
|
22
24
|
const KINDS = [
|
|
23
25
|
["daily", "Daily", "the scheduled run"],
|
|
@@ -25,48 +27,98 @@ const KINDS = [
|
|
|
25
27
|
];
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
|
-
*
|
|
30
|
+
* Fix a prompt with your own AI, in a side sheet: what is wrong, what you want changed, the
|
|
31
|
+
* prompt as it is today, a brief to copy, and a box for the reply. The reply is parsed into
|
|
32
|
+
* whole files and shown as diffs; nothing is saved until you press Save on one.
|
|
29
33
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* the same brief whichever screen you asked from.
|
|
34
|
+
* @param {{handle: string, name?: string, kind: string, want?: string, title?: string,
|
|
35
|
+
* detail?: string}} o
|
|
36
|
+
* @returns {{box: object | null, body: HTMLElement, want: HTMLTextAreaElement}} what it built,
|
|
37
|
+
* so a caller (or a test, where there is no <dialog>) can see it.
|
|
35
38
|
*/
|
|
36
|
-
export
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
export function amendSheet(o) {
|
|
40
|
+
const body = el("div", { className: "amend" });
|
|
41
|
+
const x = el("button", { className: "iconbtn", title: "Close (Esc)", ariaLabel: "Close" });
|
|
42
|
+
x.append(icon("close"));
|
|
43
|
+
body.append(el("div", { className: "sidebar-x" }, [x]));
|
|
44
|
+
|
|
45
|
+
body.append(el("h3", { className: "sheettitle", textContent: o.title ?? "Change what they are told" }));
|
|
46
|
+
if (o.detail) body.append(el("p", { className: "sub", textContent: o.detail }));
|
|
47
|
+
|
|
48
|
+
const want = el("textarea", {
|
|
49
|
+
className: "pastebox",
|
|
50
|
+
rows: 3,
|
|
51
|
+
value: o.want ?? "",
|
|
44
52
|
placeholder: "stop opening decision issues for anything reversible",
|
|
45
|
-
confirm: "Copy the brief",
|
|
46
53
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
btn
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
body.append(el("label", { className: "amendlabel", textContent: "1. What you want changed" }), want);
|
|
55
|
+
|
|
56
|
+
const copy = el("button", { className: "btn primary", textContent: "Copy the prompt" });
|
|
57
|
+
const copied = el("span", { className: "meta" });
|
|
58
|
+
copy.onclick = async () => {
|
|
59
|
+
copy.disabled = true;
|
|
60
|
+
try {
|
|
61
|
+
const url =
|
|
62
|
+
"/api/amend?staff=" + encodeURIComponent(o.handle) +
|
|
63
|
+
"&kind=" + encodeURIComponent(o.kind) +
|
|
64
|
+
"&want=" + encodeURIComponent(want.value);
|
|
65
|
+
const text = await (await fetch(url, { cache: "no-store" })).text();
|
|
66
|
+
await toClipboard(text, copy, "Copy the prompt");
|
|
67
|
+
copied.textContent = "Paste it into Claude, ChatGPT or any other AI.";
|
|
68
|
+
copied.className = "meta";
|
|
69
|
+
} catch (e) {
|
|
70
|
+
copied.textContent = String(e.message || e);
|
|
71
|
+
copied.className = "meta err";
|
|
72
|
+
} finally {
|
|
73
|
+
copy.disabled = false;
|
|
61
74
|
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
};
|
|
76
|
+
body.append(
|
|
77
|
+
el("label", { className: "amendlabel", textContent: "2. Copy the prompt into your AI" }),
|
|
78
|
+
el("div", { className: "row" }, [copy, copied]),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// The prompt this is about, to read before deciding what to ask for.
|
|
82
|
+
const today = el("details", { className: "amendtoday" });
|
|
83
|
+
const summary = el("summary", { textContent: "The " + o.kind + " prompt as it is today" });
|
|
84
|
+
const text = el("pre", { className: "briefpreview", textContent: "Composing…" });
|
|
85
|
+
today.append(summary, text);
|
|
86
|
+
body.append(today);
|
|
87
|
+
fetch("/api/prompt?staff=" + encodeURIComponent(o.handle) + "&kind=" + encodeURIComponent(o.kind), { cache: "no-store" })
|
|
88
|
+
.then((r) => r.json())
|
|
89
|
+
.then((v) => {
|
|
90
|
+
if (v.error) throw new Error(v.error);
|
|
91
|
+
const words = String(v.composed ?? "").split(/\s+/).filter(Boolean).length;
|
|
92
|
+
summary.textContent = "The " + o.kind + " prompt as it is today · " + words.toLocaleString() + " words";
|
|
93
|
+
text.textContent = v.composed ?? "";
|
|
94
|
+
})
|
|
95
|
+
.catch((e) => {
|
|
96
|
+
text.textContent = String(e.message || e);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const answer = el("textarea", { className: "pastebox", rows: 8, placeholder: "Paste its whole reply here." });
|
|
100
|
+
const check = el("button", { className: "btn", textContent: "Check the answer" });
|
|
101
|
+
const result = el("div", { className: "pasteresult" });
|
|
102
|
+
check.onclick = async () => {
|
|
103
|
+
if (!answer.value.trim()) return;
|
|
104
|
+
result.replaceChildren(el("p", { className: "sub", textContent: "Reading…" }));
|
|
105
|
+
try {
|
|
106
|
+
const data = await post({ staff: o.handle, kind: o.kind, answer: answer.value }, "/api/amend/check");
|
|
107
|
+
showPasteResult(result, data, answer.value, () => {});
|
|
108
|
+
} catch (e) {
|
|
109
|
+
result.replaceChildren(el("p", { className: "err", textContent: String(e.message || e) }));
|
|
68
110
|
}
|
|
69
|
-
}
|
|
111
|
+
};
|
|
112
|
+
body.append(
|
|
113
|
+
el("label", { className: "amendlabel", textContent: "3. Paste its reply" }),
|
|
114
|
+
answer,
|
|
115
|
+
el("div", { className: "row" }, [check]),
|
|
116
|
+
result,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const box = sheet({ node: body, side: true });
|
|
120
|
+
if (box) x.onclick = () => box.close();
|
|
121
|
+
return { box, body, want };
|
|
70
122
|
}
|
|
71
123
|
|
|
72
124
|
export function viewPrompt(m) {
|
|
@@ -90,7 +142,7 @@ export function viewPrompt(m) {
|
|
|
90
142
|
};
|
|
91
143
|
/* The point of the screen: you can see the prompt, and you can get help changing it
|
|
92
144
|
without first working out which of eight files to open. */
|
|
93
|
-
const help = el("button", { className: "ghbtn primary", textContent: "
|
|
145
|
+
const help = el("button", { className: "ghbtn primary", textContent: "Change this with your AI" });
|
|
94
146
|
help.onclick = () => copyAmend("");
|
|
95
147
|
const copy = el("button", { className: "ghbtn", textContent: "Copy the prompt" });
|
|
96
148
|
copy.onclick = () => {
|
|
@@ -99,7 +151,7 @@ export function viewPrompt(m) {
|
|
|
99
151
|
const note = el("span", { className: "meta" });
|
|
100
152
|
m.append(el("div", { className: "row", style: "margin-bottom:16px" }, [kind, help, copy, note]));
|
|
101
153
|
|
|
102
|
-
const copyAmend = (want
|
|
154
|
+
const copyAmend = (want) => amendSheet({ handle: s.handle, name: s.name, kind: S.promptKind, want });
|
|
103
155
|
|
|
104
156
|
const split = el("div", { className: "split" });
|
|
105
157
|
const tree = el("div", { className: "tree" });
|
|
@@ -351,8 +403,10 @@ export function viewPrompt(m) {
|
|
|
351
403
|
);
|
|
352
404
|
|
|
353
405
|
const ta = el("textarea", { value: text, className: "editor" });
|
|
354
|
-
|
|
406
|
+
const hl = highlighted(ta, "md");
|
|
407
|
+
viewer.append(hl);
|
|
355
408
|
grow(ta);
|
|
409
|
+
hl.repaint();
|
|
356
410
|
ta.addEventListener("input", () => grow(ta));
|
|
357
411
|
|
|
358
412
|
const status = el("span", { className: "meta" });
|
|
@@ -375,8 +429,12 @@ export function viewPrompt(m) {
|
|
|
375
429
|
status.textContent = "nothing changed";
|
|
376
430
|
return;
|
|
377
431
|
}
|
|
378
|
-
|
|
379
|
-
|
|
432
|
+
const yes = await askYes({
|
|
433
|
+
title: "Commit " + layer.path + " to " + layer.repo + " and push?",
|
|
434
|
+
hint: "Picked up by " + reach + ".",
|
|
435
|
+
confirm: "Commit and push",
|
|
436
|
+
});
|
|
437
|
+
if (!yes) return;
|
|
380
438
|
save.disabled = cancel.disabled = true;
|
|
381
439
|
status.textContent = "committing…";
|
|
382
440
|
status.className = "meta";
|
|
@@ -5,31 +5,36 @@
|
|
|
5
5
|
* spelling a repo name exactly right in a YAML file, which is the kind of thing that fails
|
|
6
6
|
* quietly and reads as the tool being broken. */
|
|
7
7
|
|
|
8
|
-
import { addRepo, listRepos } from "../api.js";
|
|
8
|
+
import { addRepo, getRepos, listRepos } from "../api.js";
|
|
9
9
|
import { el } from "../dom.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* @param {{org: string, known
|
|
12
|
+
* @param {{org: string, known?: string[], onAdded?: () => void}} opts without `known`, what
|
|
13
|
+
* org.yaml already lists is read off disk, which is how the Org screen uses it long after setup.
|
|
13
14
|
*/
|
|
14
15
|
export function repoPicker(opts) {
|
|
15
|
-
const box = el("div", { className: "manual" });
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
const box = el("div", { className: opts.bare ? "" : "manual" });
|
|
17
|
+
if (!opts.bare) {
|
|
18
|
+
box.append(
|
|
19
|
+
el("b", { textContent: "Which repos the staff work in" }),
|
|
20
|
+
el("p", {
|
|
21
|
+
textContent:
|
|
22
|
+
"Add the repos your staff should work on. New hires get access to them automatically.",
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
24
26
|
|
|
25
27
|
const out = el("div", { style: "margin-top:9px" });
|
|
26
28
|
box.append(out);
|
|
27
29
|
out.replaceChildren(el("p", { className: "sub", textContent: "Reading your repos…" }));
|
|
28
30
|
|
|
29
|
-
|
|
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?.();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/* One daily run, now, followed to the end.
|
|
2
|
+
*
|
|
3
|
+
* The last step of setting anybody up. Doctor calls a workflow that has never run unproven,
|
|
4
|
+
* because nothing short of a finished run shows the App, its grant, the secrets and the
|
|
5
|
+
* callers all work together. This starts one, and says how it ended. */
|
|
6
|
+
|
|
7
|
+
import { runStatus, startRun } from "../api.js";
|
|
8
|
+
import { askYes } from "../dialog.js";
|
|
9
|
+
import { el } from "../dom.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {{staff: string, name: string, onDone?: (ok: boolean) => void}} opts
|
|
13
|
+
*/
|
|
14
|
+
export function runOnce(opts) {
|
|
15
|
+
const box = el("div", { className: "manual" });
|
|
16
|
+
box.append(
|
|
17
|
+
el("b", { textContent: "Run " + opts.name + " once, now" }),
|
|
18
|
+
el("p", {
|
|
19
|
+
className: "sub",
|
|
20
|
+
textContent:
|
|
21
|
+
"A real daily run: it does a day's work and spends what a scheduled one would. " +
|
|
22
|
+
"When it finishes you get the outcome and the log.",
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
const go = el("button", { className: "btn primary", textContent: "Run once now" });
|
|
26
|
+
const out = el("div", { style: "margin-top:9px" });
|
|
27
|
+
box.append(el("div", { className: "row" }, [go]), out);
|
|
28
|
+
|
|
29
|
+
go.onclick = async () => {
|
|
30
|
+
const yes = await askYes({
|
|
31
|
+
title: "Start a daily run for " + opts.name + " now?",
|
|
32
|
+
hint: "It is a real run, and it costs what one does.",
|
|
33
|
+
confirm: "Start the run",
|
|
34
|
+
});
|
|
35
|
+
if (!yes) return;
|
|
36
|
+
go.disabled = true;
|
|
37
|
+
const say = (text, cls) => out.replaceChildren(el("p", { className: cls ?? "sub", textContent: text }));
|
|
38
|
+
say("Starting…");
|
|
39
|
+
let run;
|
|
40
|
+
try {
|
|
41
|
+
run = await startRun(opts.staff);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
say(String(err.message || err), "err");
|
|
44
|
+
go.disabled = false;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const link = el("a", { href: run.url, target: "_blank", rel: "noopener", textContent: "the log" });
|
|
48
|
+
if (!run.id) {
|
|
49
|
+
out.replaceChildren(el("p", { className: "sub" }, ["Started. It has not shown up yet; follow it at ", link, "."]));
|
|
50
|
+
go.disabled = false;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Polled rather than streamed: a run takes minutes, the status changes a handful of times,
|
|
55
|
+
and a clock beside it is what shows the page has not hung. */
|
|
56
|
+
const began = Date.now();
|
|
57
|
+
const tick = async () => {
|
|
58
|
+
let now;
|
|
59
|
+
try {
|
|
60
|
+
now = await runStatus(opts.staff, run.id);
|
|
61
|
+
if (now.error) throw new Error(now.error);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
say(String(err.message || err), "err");
|
|
64
|
+
go.disabled = false;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const mins = Math.floor((Date.now() - began) / 60000);
|
|
68
|
+
if (now.status !== "completed") {
|
|
69
|
+
out.replaceChildren(
|
|
70
|
+
el("p", { className: "sub" }, [now.status.replace("_", " ") + ", " + mins + " min so far. ", link]),
|
|
71
|
+
);
|
|
72
|
+
setTimeout(tick, 15000);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
go.disabled = false;
|
|
76
|
+
const ok = now.conclusion === "success";
|
|
77
|
+
const done = el("div");
|
|
78
|
+
done.append(
|
|
79
|
+
el("p", { className: ok ? "" : "err" }, [
|
|
80
|
+
ok
|
|
81
|
+
? "It worked. The App, its grant, the secrets and the callers are proven, and Health now says so. "
|
|
82
|
+
: "It ended " + (now.conclusion || "without a conclusion") + ". ",
|
|
83
|
+
el("a", { href: now.url, target: "_blank", rel: "noopener", textContent: "The log" }),
|
|
84
|
+
]),
|
|
85
|
+
);
|
|
86
|
+
for (const f of now.failedAt ?? []) done.append(el("p", { className: "sub", textContent: "Failed at " + f }));
|
|
87
|
+
if (!ok) done.append(el("p", { className: "sub", textContent: "Troubleshooting, in the docs, has what each failure usually means." }));
|
|
88
|
+
out.replaceChildren(done);
|
|
89
|
+
opts.onDone?.(ok);
|
|
90
|
+
};
|
|
91
|
+
tick();
|
|
92
|
+
};
|
|
93
|
+
return box;
|
|
94
|
+
}
|