@nanocollective/roster 0.1.0-alpha.6 → 0.1.0-alpha.8
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 +4187 -2709
- package/docs/README.md +9 -6
- package/docs/agents.md +24 -20
- 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 +81 -5
- package/docs/concepts.md +48 -14
- package/docs/cost.md +36 -1
- package/docs/developing.md +16 -21
- package/docs/doctor-codes.md +8 -2
- package/docs/extending.md +2 -2
- package/docs/getting-started.md +110 -77
- package/docs/manual-steps.md +93 -123
- package/docs/memory.md +21 -3
- package/docs/org-yaml.md +37 -2
- package/docs/portal.md +83 -35
- package/docs/prompts.md +25 -4
- package/docs/security.md +29 -5
- package/docs/session-workflow.md +49 -17
- package/docs/staff-yaml.md +13 -2
- package/docs/troubleshooting.md +8 -8
- package/docs/upgrading.md +6 -0
- package/docs/writing-a-charter.md +15 -0
- package/package.json +1 -1
- package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +6 -0
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +6 -0
- 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/strategy/ideas.md +7 -0
- package/templates/ops/.github/workflows/session.yaml +108 -14
- package/templates/ops/agents.mjs +7 -3
- package/templates/ops/compose.mjs +16 -3
- package/templates/ops/inflight.mjs +157 -0
- package/templates/ops/org/operating.md +21 -1
- 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 +16 -7
- package/templates/ops/prompts/mention.md +2 -0
- package/templates/ops/run-record.mjs +144 -0
- package/templates/portal/css/runs.css +13 -0
- package/templates/portal/css/setup.css +2 -0
- package/templates/portal/index.html +12 -1
- package/templates/portal/js/api.js +32 -4
- package/templates/portal/js/app.js +42 -8
- package/templates/portal/js/dialog.js +47 -0
- package/templates/portal/js/state.js +3 -1
- package/templates/portal/js/views/app.js +23 -5
- package/templates/portal/js/views/credential.js +93 -0
- package/templates/portal/js/views/health.js +17 -4
- package/templates/portal/js/views/inbox.js +6 -2
- package/templates/portal/js/views/org.js +23 -3
- package/templates/portal/js/views/paste.js +29 -0
- package/templates/portal/js/views/prompt.js +7 -3
- 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 +267 -45
- package/templates/portal/js/views/staff.js +81 -20
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
syncNotice,
|
|
15
15
|
} from "./refresh.js";
|
|
16
16
|
import { onRender } from "./router.js";
|
|
17
|
-
import { ORG_WIDE, S, VIEWS, applyHash, writeHash } from "./state.js";
|
|
17
|
+
import { ORG_WIDE, S, VIEWS, applyHash, readHash, writeHash } from "./state.js";
|
|
18
18
|
import { viewBrain } from "./views/brain.js";
|
|
19
19
|
import { viewChanged } from "./views/changed.js";
|
|
20
20
|
import { viewDocs } from "./views/docs.js";
|
|
@@ -23,12 +23,15 @@ import { viewHealth } from "./views/health.js";
|
|
|
23
23
|
import { viewInbox, viewPrs } from "./views/inbox.js";
|
|
24
24
|
import { viewOrg } from "./views/org.js";
|
|
25
25
|
import { viewPrompt } from "./views/prompt.js";
|
|
26
|
-
import {
|
|
26
|
+
import { viewRuns } from "./views/runs.js";
|
|
27
|
+
import { paintSetupNav, viewGettingStarted, viewSetup } from "./views/setup.js";
|
|
27
28
|
import { viewStaff } from "./views/staff.js";
|
|
28
29
|
|
|
29
30
|
const SCREEN = {
|
|
31
|
+
setup: viewGettingStarted,
|
|
30
32
|
inbox: viewInbox,
|
|
31
33
|
prs: viewPrs,
|
|
34
|
+
runs: viewRuns,
|
|
32
35
|
org: viewOrg,
|
|
33
36
|
staff: viewStaff,
|
|
34
37
|
docs: viewDocs,
|
|
@@ -50,8 +53,38 @@ const SCREEN = {
|
|
|
50
53
|
*/
|
|
51
54
|
const NEEDS = ["org", "name", "opsName", "staff"];
|
|
52
55
|
|
|
56
|
+
/** The org-wide screens in the sidebar, by button. */
|
|
57
|
+
const NAV = [
|
|
58
|
+
["#setupnav", "setup"],
|
|
59
|
+
["#inboxnav", "inbox"],
|
|
60
|
+
["#prsnav", "prs"],
|
|
61
|
+
["#runsnav", "runs"],
|
|
62
|
+
["#staffnav", "staff"],
|
|
63
|
+
["#orgnav", "org"],
|
|
64
|
+
["#docsnav", "docs"],
|
|
65
|
+
];
|
|
66
|
+
|
|
53
67
|
export async function boot() {
|
|
54
68
|
initTheme();
|
|
69
|
+
|
|
70
|
+
/* Bound before the first request rather than after it. The sidebar is markup, so it is on
|
|
71
|
+
screen and clickable while /api/org is still building the export, and a click in that
|
|
72
|
+
window used to do nothing at all: the first click on Staff after a load was simply lost.
|
|
73
|
+
Now it is remembered and honoured once there is something to render. */
|
|
74
|
+
let early = null;
|
|
75
|
+
for (const [id, view] of NAV) {
|
|
76
|
+
const b = $(id);
|
|
77
|
+
if (!b) continue;
|
|
78
|
+
b.onclick = () => {
|
|
79
|
+
if (!S.data) {
|
|
80
|
+
early = view;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
S.view = view;
|
|
84
|
+
render();
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
55
88
|
/* Before the first render, and once: it is delegated, so it covers every image any screen
|
|
56
89
|
draws from here on, including the setup screen. */
|
|
57
90
|
installLightbox();
|
|
@@ -66,7 +99,7 @@ export async function boot() {
|
|
|
66
99
|
/* Every one of these reads a tenant, so during setup they answer 409 and the sidebar is a
|
|
67
100
|
row of four dead ends beside an empty Staff heading. There is one thing to do on this
|
|
68
101
|
screen; the navigation comes back with the org. */
|
|
69
|
-
for (const el of document.querySelectorAll("#inboxnav, #prsnav, #orgnav, #staffnav, #docsnav, #refreshall, .sect, #stafflist")) {
|
|
102
|
+
for (const el of document.querySelectorAll("#inboxnav, #prsnav, #runsnav, #orgnav, #staffnav, #docsnav, #refreshall, .sect, #stafflist")) {
|
|
70
103
|
el.hidden = true;
|
|
71
104
|
}
|
|
72
105
|
for (const slot of document.querySelectorAll("[data-icon]")) {
|
|
@@ -88,15 +121,15 @@ export async function boot() {
|
|
|
88
121
|
slot.innerHTML = iconHTML(slot.dataset.icon);
|
|
89
122
|
}
|
|
90
123
|
onRender(render);
|
|
124
|
+
const linked = readHash() !== null;
|
|
91
125
|
applyHash();
|
|
126
|
+
/* An org nobody has been hired into has an empty Inbox, which says nothing about what to do
|
|
127
|
+
next. It opens on what is left instead, unless a link or a click asked for somewhere else. */
|
|
128
|
+
if (early) S.view = early;
|
|
129
|
+
else if (!linked && (S.data.unfinished ?? []).includes("hire")) S.view = "setup";
|
|
92
130
|
initStaffFold();
|
|
93
131
|
paintSidebar();
|
|
94
132
|
|
|
95
|
-
$("#inboxnav").onclick = () => { S.view = "inbox"; render(); };
|
|
96
|
-
$("#prsnav").onclick = () => { S.view = "prs"; render(); };
|
|
97
|
-
$("#staffnav").onclick = () => { S.view = "staff"; render(); };
|
|
98
|
-
$("#orgnav").onclick = () => { S.view = "org"; render(); };
|
|
99
|
-
$("#docsnav").onclick = () => { S.view = "docs"; render(); };
|
|
100
133
|
$("#refreshall").onclick = () => refreshAll(false);
|
|
101
134
|
|
|
102
135
|
/* Coming back to the tab after a run should show the run — and coming back to it thirty
|
|
@@ -296,6 +329,7 @@ function render() {
|
|
|
296
329
|
// A refresh can add or remove a staff member, and the sidebar is built once.
|
|
297
330
|
const roster = S.data.staff.map((s) => s.handle).join(" ");
|
|
298
331
|
if (roster !== paintedRoster) paintSidebar();
|
|
332
|
+
paintSetupNav();
|
|
299
333
|
markSidebar();
|
|
300
334
|
stampCounts();
|
|
301
335
|
writeHash(true);
|
|
@@ -111,3 +111,50 @@ export function askText({
|
|
|
111
111
|
ta.setSelectionRange?.(ta.value.length, ta.value.length);
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Asking before something that cannot be taken back: a merge, a push, a new repo, a paid run.
|
|
117
|
+
*
|
|
118
|
+
* Native `confirm()` looked like the browser rather than this page, and it blocks the whole
|
|
119
|
+
* tab, which froze browser automation mid-run. This is the same `<dialog>` as askText, so the
|
|
120
|
+
* backdrop, Escape and the focus trap come free. The wording is the caller's: `title` is the
|
|
121
|
+
* question, `hint` says what will happen.
|
|
122
|
+
*
|
|
123
|
+
* @returns true only for the confirm button. Escape, Cancel and the backdrop are all no.
|
|
124
|
+
*/
|
|
125
|
+
export function askYes({ title, hint, confirm = "Continue" }) {
|
|
126
|
+
const box = document.createElement("dialog");
|
|
127
|
+
// The test shim has no dialog element; see askText.
|
|
128
|
+
if (typeof box.showModal !== "function") {
|
|
129
|
+
return Promise.resolve(Boolean(globalThis.confirm?.(hint ? title + "\n\n" + hint : title)));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
box.className = "ask";
|
|
133
|
+
box.append(el("h3", { textContent: title }));
|
|
134
|
+
if (hint) box.append(el("p", { className: "askhint", textContent: hint }));
|
|
135
|
+
|
|
136
|
+
const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
|
|
137
|
+
const go = el("button", { className: "ghbtn primary", textContent: confirm });
|
|
138
|
+
box.append(el("div", { className: "row askrow" }, [cancel, go]));
|
|
139
|
+
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
let answer = false;
|
|
142
|
+
const done = (yes) => {
|
|
143
|
+
answer = yes;
|
|
144
|
+
box.close();
|
|
145
|
+
};
|
|
146
|
+
cancel.onclick = () => done(false);
|
|
147
|
+
go.onclick = () => done(true);
|
|
148
|
+
box.addEventListener("close", () => {
|
|
149
|
+
box.remove();
|
|
150
|
+
resolve(answer);
|
|
151
|
+
});
|
|
152
|
+
box.addEventListener("click", (e) => {
|
|
153
|
+
if (onBackdrop(e, box.getBoundingClientRect?.())) done(false);
|
|
154
|
+
});
|
|
155
|
+
document.body.append(box);
|
|
156
|
+
box.showModal();
|
|
157
|
+
// Cancel has the focus, so an Enter pressed out of habit does not merge anything.
|
|
158
|
+
cancel.focus();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -22,7 +22,7 @@ export const VIEWS = [
|
|
|
22
22
|
export const VIEW_ALIAS = { memory: "brain" };
|
|
23
23
|
|
|
24
24
|
/** Screens that belong to the org rather than to one staff member. */
|
|
25
|
-
export const ORG_WIDE = new Set(["inbox", "prs", "org", "staff", "docs"]);
|
|
25
|
+
export const ORG_WIDE = new Set(["setup", "inbox", "prs", "runs", "org", "staff", "docs"]);
|
|
26
26
|
|
|
27
27
|
/** The two screens that read the inbox, and so share its filters and its `?t=` thread. */
|
|
28
28
|
const INBOXY = new Set(["inbox", "prs"]);
|
|
@@ -32,6 +32,8 @@ export const S = {
|
|
|
32
32
|
data: null,
|
|
33
33
|
docs: null,
|
|
34
34
|
inbox: null,
|
|
35
|
+
/** /api/runs. Kept once read: it is minutes of downloads on a cold cache. */
|
|
36
|
+
runs: null,
|
|
35
37
|
sync: null,
|
|
36
38
|
loadedAt: null,
|
|
37
39
|
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* it is one browser, one origin, and nothing extra to explain.
|
|
7
7
|
*
|
|
8
8
|
* What it still cannot do is install the App. Installing is a grant of access to specific
|
|
9
|
-
* repositories and GitHub asks a human to
|
|
10
|
-
* be worked around. */
|
|
9
|
+
* repositories and GitHub asks a human to confirm them, which is correct behaviour and should not
|
|
10
|
+
* be worked around. The link it hands back has the right repos already selected. */
|
|
11
11
|
|
|
12
12
|
import { appResult, startApp } from "../api.js";
|
|
13
13
|
import { el } from "../dom.js";
|
|
@@ -48,6 +48,11 @@ export function appPanel(opts) {
|
|
|
48
48
|
it: an App that exists is not a problem, it is a step you have already done. */
|
|
49
49
|
const note = el("div");
|
|
50
50
|
note.append(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
51
|
+
if (err.data?.install) {
|
|
52
|
+
note.append(
|
|
53
|
+
el("a", { className: "btn", href: err.data.install, target: "_blank", textContent: "Install it" }),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
51
56
|
out.replaceChildren(note);
|
|
52
57
|
return;
|
|
53
58
|
}
|
|
@@ -102,9 +107,21 @@ export function appPanel(opts) {
|
|
|
102
107
|
el("p", {
|
|
103
108
|
className: "sub",
|
|
104
109
|
textContent:
|
|
105
|
-
"
|
|
106
|
-
"
|
|
110
|
+
"The link opens GitHub's install page with the org and the repos already selected: " +
|
|
111
|
+
"this staff member's brain, every peer tracker it writes to, and the product repos. " +
|
|
112
|
+
"Check the list and confirm.",
|
|
107
113
|
}),
|
|
114
|
+
...((result.preselected ?? []).length
|
|
115
|
+
? [el("p", { className: "sub", textContent: "Selected: " + result.preselected.join(", ") })]
|
|
116
|
+
: []),
|
|
117
|
+
...((result.missing ?? []).length
|
|
118
|
+
? [
|
|
119
|
+
el("p", {
|
|
120
|
+
className: "err",
|
|
121
|
+
textContent: "Tick these yourself, their ids could not be read: " + result.missing.join(", "),
|
|
122
|
+
}),
|
|
123
|
+
]
|
|
124
|
+
: []),
|
|
108
125
|
el("a", {
|
|
109
126
|
className: "btn primary",
|
|
110
127
|
href: result.install,
|
|
@@ -115,7 +132,8 @@ export function appPanel(opts) {
|
|
|
115
132
|
className: "sub",
|
|
116
133
|
textContent:
|
|
117
134
|
"Do not verify this by reading the API. It reports what an App declares separately " +
|
|
118
|
-
"from what an installation was granted. Only a run that finished proves the chain
|
|
135
|
+
"from what an installation was granted. Only a run that finished proves the chain, " +
|
|
136
|
+
"which is what Run once now on the staff card is for.",
|
|
119
137
|
}),
|
|
120
138
|
);
|
|
121
139
|
out.replaceChildren(done);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* The agent's credential, stored once for the org.
|
|
2
|
+
*
|
|
3
|
+
* The value goes from this box to the local server in a POST, and from there to gh on its
|
|
4
|
+
* standard input. It is not kept in the page after it is sent, and nothing echoes it back. */
|
|
5
|
+
|
|
6
|
+
import { getCredential, storeCredential } from "../api.js";
|
|
7
|
+
import { el } from "../dom.js";
|
|
8
|
+
|
|
9
|
+
export function credentialPanel() {
|
|
10
|
+
const box = el("div", { className: "manual" });
|
|
11
|
+
box.append(el("b", { textContent: "Your agent's credential, once" }));
|
|
12
|
+
const body = el("div");
|
|
13
|
+
box.append(body);
|
|
14
|
+
body.append(el("p", { className: "sub", textContent: "Looking at where it would go…" }));
|
|
15
|
+
|
|
16
|
+
getCredential()
|
|
17
|
+
.then((c) => draw(body, c))
|
|
18
|
+
.catch((err) => body.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) })));
|
|
19
|
+
return box;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function draw(body, c) {
|
|
23
|
+
body.replaceChildren();
|
|
24
|
+
if (c.howTo) body.append(el("p", { textContent: "Where to get one: " + c.howTo }));
|
|
25
|
+
|
|
26
|
+
if (!c.brains.length) {
|
|
27
|
+
body.append(
|
|
28
|
+
el("p", {
|
|
29
|
+
className: "sub",
|
|
30
|
+
textContent:
|
|
31
|
+
"Nothing reads it until somebody is hired, so this box opens once there is a staff " +
|
|
32
|
+
"member. It is asked for once, not once per hire.",
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (c.orgSecret) {
|
|
39
|
+
body.append(
|
|
40
|
+
el("p", {
|
|
41
|
+
className: "sub",
|
|
42
|
+
textContent:
|
|
43
|
+
c.name + " is already an org secret, shared with " + c.orgSecret.visibility + " repos. " +
|
|
44
|
+
"Each hire adds its brain to it. Paste a new one only to replace it.",
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
body.append(
|
|
50
|
+
el("p", {
|
|
51
|
+
className: "sub",
|
|
52
|
+
textContent:
|
|
53
|
+
(c.plan.mode === "org"
|
|
54
|
+
? "Stored as one org secret, " + c.name + ", shared with " + c.brains.join(", ") + ". "
|
|
55
|
+
: "Stored as " + c.name + " on each of " + c.brains.join(", ") + ", because " + c.plan.reason + ". ") +
|
|
56
|
+
"It goes to gh on standard input and is never written to disk.",
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const input = el("input", { type: "password", placeholder: c.name, autocomplete: "off" });
|
|
61
|
+
input.style.width = "100%";
|
|
62
|
+
const go = el("button", { className: "btn primary", textContent: "Store it" });
|
|
63
|
+
const out = el("div", { style: "margin-top:9px" });
|
|
64
|
+
body.append(input, el("div", { className: "row", style: "margin-top:9px" }, [go]), out);
|
|
65
|
+
|
|
66
|
+
go.onclick = async () => {
|
|
67
|
+
const value = input.value.trim();
|
|
68
|
+
if (!value) {
|
|
69
|
+
input.focus();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
go.disabled = true;
|
|
73
|
+
out.replaceChildren(el("p", { className: "sub", textContent: "Storing…" }));
|
|
74
|
+
try {
|
|
75
|
+
const r = await storeCredential(value, false);
|
|
76
|
+
input.value = "";
|
|
77
|
+
if (!r.ok) throw new Error(r.error);
|
|
78
|
+
out.replaceChildren(
|
|
79
|
+
...(r.fellBack ? [el("p", { className: "err", textContent: "The org secret was refused: " + r.fellBack })] : []),
|
|
80
|
+
el("p", {
|
|
81
|
+
textContent:
|
|
82
|
+
r.mode === "org"
|
|
83
|
+
? "Stored once for the org, shared with " + r.repos.length + " brains."
|
|
84
|
+
: "Stored on " + r.repos.join(", ") + ".",
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
out.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
89
|
+
} finally {
|
|
90
|
+
go.disabled = false;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/* Is the scaffolding still there, has the agent been running, and what does lint say. */
|
|
2
2
|
|
|
3
3
|
import { post } from "../api.js";
|
|
4
|
+
import { askYes } from "../dialog.js";
|
|
4
5
|
import { ago, el, esc, kb } from "../dom.js";
|
|
5
6
|
import { inline } from "../md.js";
|
|
6
7
|
import { render } from "../router.js";
|
|
7
8
|
import { S, staff } from "../state.js";
|
|
8
9
|
import { checklist } from "./checklist.js";
|
|
9
10
|
import { copyAmendBrief } from "./prompt.js";
|
|
11
|
+
import { runOnce } from "./runonce.js";
|
|
10
12
|
|
|
11
13
|
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
12
14
|
|
|
@@ -81,7 +83,18 @@ export function viewHealth(m) {
|
|
|
81
83
|
doctor, bordered boxes for the prompt audit, cards with two buttons each for lint. Same
|
|
82
84
|
question in all three, so one shape: a heading that carries its own count, a line saying
|
|
83
85
|
what the section is, the findings as rows, and the section's actions at the end. */
|
|
84
|
-
|
|
86
|
+
/* A workflow that has never run is unproven, and the way to prove it is to run it. Here
|
|
87
|
+
because Health is where "unproven" is read; a success re-runs the checks below. */
|
|
88
|
+
let doctor = null;
|
|
89
|
+
m.append(
|
|
90
|
+
runOnce({
|
|
91
|
+
staff: s.handle,
|
|
92
|
+
name: s.name,
|
|
93
|
+
onDone: (ok) => ok && doctor && checklist(doctor.body, { shell: doctor }),
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
doctor = section(m, "The org, from roster doctor", "doctor");
|
|
85
98
|
checklist(doctor.body, { shell: doctor });
|
|
86
99
|
|
|
87
100
|
/* What is wrong with what this agent is sent. It used to sit in the tree on the Prompt
|
|
@@ -132,7 +145,7 @@ function memoryProblems({ body, actions, say }, s) {
|
|
|
132
145
|
const { row, body: text } = finding(
|
|
133
146
|
p.level === "error" ? "error" : "warning",
|
|
134
147
|
{ html: inline(p.message) },
|
|
135
|
-
[p.rule, p.line ? "INDEX.md:" + p.line : ""],
|
|
148
|
+
[p.rule, p.line ? "INDEX.md:" + p.line : (p.file ?? "")],
|
|
136
149
|
);
|
|
137
150
|
const status = el("span", { className: "meta" });
|
|
138
151
|
const btn = el("button", { className: "ghbtn", textContent: "Ask " + s.handle.toUpperCase() + " to fix" });
|
|
@@ -263,13 +276,13 @@ export async function askToFix(s, problems, btn, status) {
|
|
|
263
276
|
"",
|
|
264
277
|
"| rule | where | what |",
|
|
265
278
|
"|---|---|---|",
|
|
266
|
-
...problems.map((p) => "| `" + p.rule + "` | " + (p.line ? "INDEX.md:" + p.line : "—") + " | " +
|
|
279
|
+
...problems.map((p) => "| `" + p.rule + "` | " + (p.line ? "INDEX.md:" + p.line : (p.file ?? "—")) + " | " +
|
|
267
280
|
String(p.message).replace(/\|/g, "\\|") + " |"),
|
|
268
281
|
"",
|
|
269
282
|
"Fix in your next session and close this.",
|
|
270
283
|
].join("\n");
|
|
271
284
|
|
|
272
|
-
if (!
|
|
285
|
+
if (!(await askYes({ title: "Open an issue in " + s.brain + "?", confirm: "Open it" }))) return;
|
|
273
286
|
btn.disabled = true;
|
|
274
287
|
status.textContent = "opening…";
|
|
275
288
|
status.className = "meta";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* The inbox: everything open across the org, and the thread beside it. */
|
|
2
2
|
|
|
3
3
|
import { getLabels, getPr, getThread, post, upload } from "../api.js";
|
|
4
|
-
import { askText } from "../dialog.js";
|
|
4
|
+
import { askText, askYes } from "../dialog.js";
|
|
5
5
|
import { ago, el, esc, markCurrent, skeleton } from "../dom.js";
|
|
6
6
|
import { icon, iconHTML } from "../icons.js";
|
|
7
7
|
import { mdlite } from "../md.js";
|
|
@@ -693,7 +693,11 @@ function inboxScreen(m, opts) {
|
|
|
693
693
|
merge.title = "This branch conflicts with its base";
|
|
694
694
|
}
|
|
695
695
|
merge.onclick = async () => {
|
|
696
|
-
|
|
696
|
+
const yes = await askYes({
|
|
697
|
+
title: "Merge " + item.repo + " #" + item.number + " into its base branch?",
|
|
698
|
+
confirm: "Merge",
|
|
699
|
+
});
|
|
700
|
+
if (!yes) return;
|
|
697
701
|
busy(true, "merging…");
|
|
698
702
|
try {
|
|
699
703
|
await post({ action: "merge", repo: item.repo, number: item.number });
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { getFile, getOrgLayer, post } from "../api.js";
|
|
18
|
+
import { askYes } from "../dialog.js";
|
|
18
19
|
import { el, esc, grow, kb, skeleton } from "../dom.js";
|
|
19
20
|
import { icon } from "../icons.js";
|
|
20
21
|
import { mdlite } from "../md.js";
|
|
21
22
|
import { humansOf, S, writeHash } from "../state.js";
|
|
22
23
|
import { yamlPre } from "../yaml.js";
|
|
24
|
+
import { repoPicker } from "./repos.js";
|
|
23
25
|
|
|
24
26
|
/* What the files roster itself ships are for, in one line each. A filename says nothing about
|
|
25
27
|
which one to open; anything a tenant has added of its own falls back to its first heading. */
|
|
@@ -27,6 +29,8 @@ const KNOWN = {
|
|
|
27
29
|
"org.yaml": "Who and what: the humans, the repos, the defaults, which agent runs them.",
|
|
28
30
|
"org/business.md":
|
|
29
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.",
|
|
30
34
|
"org/operating.md":
|
|
31
35
|
"The autonomy contract: what they do without asking, what they escalate, how a run starts and ends.",
|
|
32
36
|
"org/guardrails.md":
|
|
@@ -75,6 +79,20 @@ export function viewOrg(m) {
|
|
|
75
79
|
);
|
|
76
80
|
m.append(facts);
|
|
77
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
|
+
|
|
78
96
|
const split = el("div", { className: "split" });
|
|
79
97
|
const tree = el("div", { className: "tree" });
|
|
80
98
|
const viewer = el("div", { className: "viewer" });
|
|
@@ -220,9 +238,11 @@ export function viewOrg(m) {
|
|
|
220
238
|
through the question, which is worse than not asking. */
|
|
221
239
|
if (
|
|
222
240
|
isYaml &&
|
|
223
|
-
!
|
|
224
|
-
"Commit " + full(path) + " and push
|
|
225
|
-
|
|
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
|
+
}))
|
|
226
246
|
) {
|
|
227
247
|
return;
|
|
228
248
|
}
|
|
@@ -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,7 +10,7 @@
|
|
|
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";
|
|
@@ -375,8 +375,12 @@ export function viewPrompt(m) {
|
|
|
375
375
|
status.textContent = "nothing changed";
|
|
376
376
|
return;
|
|
377
377
|
}
|
|
378
|
-
|
|
379
|
-
|
|
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;
|
|
380
384
|
save.disabled = cancel.disabled = true;
|
|
381
385
|
status.textContent = "committing…";
|
|
382
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?.();
|