@nanocollective/roster 0.1.0-alpha.21 → 0.1.0-alpha.23
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/dist/cli.js
CHANGED
|
@@ -1029,6 +1029,7 @@ roster brief <kind> [handle]
|
|
|
1029
1029
|
discover write org/business.md, which every prompt is composed on top of
|
|
1030
1030
|
charter <who> write a staff member's CHARTER.md
|
|
1031
1031
|
voice revise org/voice.md, the house style every surface inherits
|
|
1032
|
+
priorities write org/priorities.md, this month's priorities
|
|
1032
1033
|
amend <who> change what a staff member is told, with the whole prompt attached
|
|
1033
1034
|
|
|
1034
1035
|
\`charter\` carries one of the worked examples in docs/charters as a model for the shape,
|
|
@@ -4890,7 +4891,7 @@ async function initFiles(o) {
|
|
|
4890
4891
|
}
|
|
4891
4892
|
files.set("org/business.md", businessStub(o.name, o.org));
|
|
4892
4893
|
files.set("org/priorities.md", prioritiesStub());
|
|
4893
|
-
for (const [rel, text] of briefCommands(["discover", "voice"], {
|
|
4894
|
+
for (const [rel, text] of briefCommands(["discover", "priorities", "voice"], {
|
|
4894
4895
|
ORG: o.org,
|
|
4895
4896
|
ORG_NAME: o.name,
|
|
4896
4897
|
OPS_REPO: `${o.org}/${o.opsName}`,
|
|
@@ -5861,6 +5862,7 @@ import { join as join24 } from "path";
|
|
|
5861
5862
|
var WRITES = {
|
|
5862
5863
|
discover: (c) => [`${c.opsName}/org/business.md`],
|
|
5863
5864
|
voice: (c) => [`${c.opsName}/org/voice.md`],
|
|
5865
|
+
priorities: (c) => [`${c.opsName}/org/priorities.md`],
|
|
5864
5866
|
charter: (c) => [`${c.dir}/CHARTER.md`]
|
|
5865
5867
|
};
|
|
5866
5868
|
var READS = {
|
|
@@ -5870,6 +5872,7 @@ var READS = {
|
|
|
5870
5872
|
`${c.opsName}/org/guardrails.md`
|
|
5871
5873
|
],
|
|
5872
5874
|
voice: (c) => [`${c.opsName}/org/business.md`, `${c.opsName}/org/guardrails.md`],
|
|
5875
|
+
priorities: (c) => [`${c.opsName}/org/business.md`],
|
|
5873
5876
|
/* A charter is the difference between this staff member and everyone else, so the peers'
|
|
5874
5877
|
charters are the most useful thing in the room: without them the model writes a second
|
|
5875
5878
|
copy of whoever it was shown. */
|
|
@@ -6439,7 +6442,7 @@ async function portalCommand(argv) {
|
|
|
6439
6442
|
const handleSetup = async (url, req, res) => {
|
|
6440
6443
|
const route = url.pathname.slice("/api/setup/".length);
|
|
6441
6444
|
if (route === "status") {
|
|
6442
|
-
json(res, { ...await setupStatus(startedIn), startedIn });
|
|
6445
|
+
json(res, { ...await setupStatus(startedIn), startedIn }, true);
|
|
6443
6446
|
return;
|
|
6444
6447
|
}
|
|
6445
6448
|
if (route === "app") {
|
|
@@ -6820,7 +6823,7 @@ async function portalCommand(argv) {
|
|
|
6820
6823
|
return;
|
|
6821
6824
|
}
|
|
6822
6825
|
if (url.pathname === "/api/doctor") {
|
|
6823
|
-
collect({ ops: ws?.opsDir, offline: url.searchParams.get("offline") === "1" }).then((report3) => json(res, report3 ?? { findings: [], online: false, empty: true })).catch((err) => {
|
|
6826
|
+
collect({ ops: ws?.opsDir, offline: url.searchParams.get("offline") === "1" }).then((report3) => json(res, report3 ?? { findings: [], online: false, empty: true }, true)).catch((err) => {
|
|
6824
6827
|
res.writeHead(200, { "content-type": "application/json" });
|
|
6825
6828
|
res.end(JSON.stringify({ findings: [], error: String(err?.message ?? err) }));
|
|
6826
6829
|
});
|
|
@@ -6901,7 +6904,7 @@ async function portalCommand(argv) {
|
|
|
6901
6904
|
throw new Error(`"${handle}" is not in org.yaml`);
|
|
6902
6905
|
}
|
|
6903
6906
|
const plan3 = buildRetirePlan(w, org, handle, parseYaml);
|
|
6904
|
-
json(res, { action, plan: plan3 });
|
|
6907
|
+
json(res, { action, plan: plan3 }, true);
|
|
6905
6908
|
return;
|
|
6906
6909
|
}
|
|
6907
6910
|
if (!/^[a-z][a-z0-9-]{1,20}$/.test(handle)) {
|
|
@@ -6913,10 +6916,11 @@ async function portalCommand(argv) {
|
|
|
6913
6916
|
const flags = hireFlags(url.searchParams);
|
|
6914
6917
|
const plan2 = buildPlan(w, org, handle, flags, parseYaml);
|
|
6915
6918
|
void readOrgSecret(org.org, plan2.staff.agentSecret).then(
|
|
6916
|
-
(orgSecret) => json(
|
|
6917
|
-
|
|
6918
|
-
plan: { ...plan2, orgSecret, files: [...plan2.files.keys()].sort() }
|
|
6919
|
-
|
|
6919
|
+
(orgSecret) => json(
|
|
6920
|
+
res,
|
|
6921
|
+
{ action, plan: { ...plan2, orgSecret, files: [...plan2.files.keys()].sort() } },
|
|
6922
|
+
true
|
|
6923
|
+
)
|
|
6920
6924
|
);
|
|
6921
6925
|
} catch (err) {
|
|
6922
6926
|
res.writeHead(400, { "content-type": "application/json" });
|
|
@@ -6990,7 +6994,7 @@ async function portalCommand(argv) {
|
|
|
6990
6994
|
return;
|
|
6991
6995
|
}
|
|
6992
6996
|
if (url.pathname === "/api/fix") {
|
|
6993
|
-
gather(w, url.searchParams.get("offline") === "1").then((items) => json(res, { items, text: fixBrief(w, items) })).catch((err) => {
|
|
6997
|
+
gather(w, url.searchParams.get("offline") === "1").then((items) => json(res, { items, text: fixBrief(w, items) }, true)).catch((err) => {
|
|
6994
6998
|
res.writeHead(200, { "content-type": "application/json" });
|
|
6995
6999
|
res.end(JSON.stringify({ items: [], error: String(err?.message ?? err) }));
|
|
6996
7000
|
});
|
|
@@ -7515,12 +7519,13 @@ function refuseWrite(res) {
|
|
|
7515
7519
|
res.writeHead(403, { "content-type": "application/json" });
|
|
7516
7520
|
res.end(JSON.stringify({ error: "write actions need a local POST from the portal" }));
|
|
7517
7521
|
}
|
|
7518
|
-
function json(res, data) {
|
|
7522
|
+
function json(res, data, advice = false) {
|
|
7519
7523
|
res.writeHead(200, {
|
|
7520
7524
|
"content-type": "application/json; charset=utf-8",
|
|
7521
7525
|
"cache-control": "no-store"
|
|
7522
7526
|
});
|
|
7523
|
-
|
|
7527
|
+
const text = JSON.stringify(data);
|
|
7528
|
+
res.end(advice ? withBin(text) : text);
|
|
7524
7529
|
}
|
|
7525
7530
|
function hireFlags(params) {
|
|
7526
7531
|
const num = (k) => params.get(k) ? Number(params.get(k)) : void 0;
|
package/docs/getting-started.md
CHANGED
|
@@ -149,7 +149,8 @@ The same road, command by command. Each prints its plan and changes nothing with
|
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
151
|
roster init --org acme --apply # the ops repo, and its Actions access
|
|
152
|
-
roster brief discover # a brief for org/business.md
|
|
152
|
+
roster brief discover # a brief for org/business.md
|
|
153
|
+
roster brief priorities # a brief for org/priorities.md
|
|
153
154
|
roster hire cto --apply # the brain, the wiring, the commits as you
|
|
154
155
|
roster app cto --apply # the App, its secrets, and a pre-ticked install link
|
|
155
156
|
roster credential --apply # the agent credential, once for the org
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Write `org/priorities.md` for %%ORG_NAME%%.
|
|
2
|
+
|
|
3
|
+
You are helping %%HUMAN%% decide what their AI staff should work on this month. Every staff
|
|
4
|
+
member reads this file at the start of every run and picks work that serves it, so a vague
|
|
5
|
+
priority produces scattered work.
|
|
6
|
+
|
|
7
|
+
## Where you are
|
|
8
|
+
|
|
9
|
+
- `%%OPS_REPO_DIR%%/org/business.md`: what the business is. **Read it first**, and don't ask
|
|
10
|
+
anything it already answers.
|
|
11
|
+
- `%%OPS_REPO_DIR%%/org/priorities.md`: the current file, which you are replacing.
|
|
12
|
+
|
|
13
|
+
If you cannot read files where you are running, ask %%HUMAN%% to paste `org/business.md`.
|
|
14
|
+
|
|
15
|
+
## Interview
|
|
16
|
+
|
|
17
|
+
Ask one question at a time. You are after:
|
|
18
|
+
|
|
19
|
+
- **The one outcome that matters most this month**, and how they will know it happened.
|
|
20
|
+
- **At most two more**, in order. Push back on a fourth: past three it is a wish list.
|
|
21
|
+
- **What is out of scope this month**: work that is tempting but not now. Naming it is what
|
|
22
|
+
stops the staff doing it.
|
|
23
|
+
|
|
24
|
+
Prefer outcomes ("a stranger pays for it") to activities ("improve the landing page"). If an
|
|
25
|
+
answer is an activity, ask what it is for.
|
|
26
|
+
|
|
27
|
+
## Write it
|
|
28
|
+
|
|
29
|
+
Use exactly this shape:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
## What matters this month
|
|
33
|
+
|
|
34
|
+
### Priorities, in order
|
|
35
|
+
|
|
36
|
+
1. The first outcome, and how you will know it happened.
|
|
37
|
+
2. The second.
|
|
38
|
+
3. The third.
|
|
39
|
+
|
|
40
|
+
### Out of scope this month
|
|
41
|
+
|
|
42
|
+
- One thing per line.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Keep each priority to a sentence or two. Don't add anything %%HUMAN%% didn't say without
|
|
46
|
+
listing it after the file so they can check it.
|
|
@@ -148,3 +148,13 @@
|
|
|
148
148
|
.mode:hover{color:var(--ink)}
|
|
149
149
|
.mode.on{background:var(--bg-elev);color:var(--ink);box-shadow:var(--shadow-sm)}
|
|
150
150
|
.modes + div > .paste{border-top:0;margin-top:0;padding-top:0}
|
|
151
|
+
|
|
152
|
+
/* ---------- a checked paste ---------- */
|
|
153
|
+
.verdict{margin:0 0 10px;font:600 13.5px var(--sans)}
|
|
154
|
+
.verdict.ok{color:var(--accent)}
|
|
155
|
+
.verdict.bad{color:var(--err)}
|
|
156
|
+
.pastepreview{border:1px solid var(--line);border-radius:var(--r);padding:14px 16px;margin-bottom:12px;background:var(--bg)}
|
|
157
|
+
.pastenotes{margin:0 0 12px}
|
|
158
|
+
.pastenotes summary{cursor:pointer;font:600 12.5px var(--sans);color:var(--ink-dim);margin-bottom:6px}
|
|
159
|
+
.pastenotes .md{border-left:3px solid var(--line);padding-left:12px}
|
|
160
|
+
.pastefile .diff{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
@@ -17,6 +17,10 @@ const BUSINESS_QUESTIONS = [
|
|
|
17
17
|
|
|
18
18
|
const val = (input) => String(input.value ?? "").trim();
|
|
19
19
|
|
|
20
|
+
/* A file still in the shape it shipped in holds the questions, not answers, and pre-filling the
|
|
21
|
+
fields with "Not a segment. The person…" reads as if somebody had already answered them. */
|
|
22
|
+
const isStub = (text) => /This file is a stub|Or answer these by hand/.test(text);
|
|
23
|
+
|
|
20
24
|
/**
|
|
21
25
|
* org/business.md as five questions. With `text`, the answers already in the file fill the
|
|
22
26
|
* fields, so changing one answer does not mean retyping the other four.
|
|
@@ -25,7 +29,7 @@ const val = (input) => String(input.value ?? "").trim();
|
|
|
25
29
|
*/
|
|
26
30
|
export function businessForm(o) {
|
|
27
31
|
const form = el("div");
|
|
28
|
-
const known = sections(o.text ?? "");
|
|
32
|
+
const known = isStub(o.text ?? "") ? new Map() : sections(o.text ?? "");
|
|
29
33
|
const fields = BUSINESS_QUESTIONS.map(([id, label, heading, required]) => {
|
|
30
34
|
const input = id === "line" ? el("input", { type: "text" }) : el("textarea", { className: "pastebox", rows: 3 });
|
|
31
35
|
const had = known.get(heading.toLowerCase());
|
|
@@ -64,7 +68,7 @@ export function businessForm(o) {
|
|
|
64
68
|
/** org/priorities.md as three lines and an out-of-scope list, filled from the file if it has them. */
|
|
65
69
|
export function prioritiesForm(o) {
|
|
66
70
|
const form = el("div");
|
|
67
|
-
const { items, outs } = parsePriorities(o.text ?? "");
|
|
71
|
+
const { items, outs } = isStub(o.text ?? "") ? { items: [], outs: [] } : parsePriorities(o.text ?? "");
|
|
68
72
|
const ranks = [1, 2, 3].map((i) => {
|
|
69
73
|
const input = el("input", { type: "text", placeholder: i === 1 ? "e.g. Tasks can be ticked off and removed" : "" });
|
|
70
74
|
if (items[i - 1]) input.value = items[i - 1];
|
|
@@ -95,7 +99,12 @@ export function prioritiesForm(o) {
|
|
|
95
99
|
(scoped.length ? "\n### Out of scope this month\n\n" + scoped.map((t) => `- ${t}`).join("\n") + "\n" : "");
|
|
96
100
|
await saveAndSay(save, out, o.path, text, "portal: write org/priorities.md", o.onSaved);
|
|
97
101
|
};
|
|
98
|
-
return modes([
|
|
102
|
+
return modes([
|
|
103
|
+
...(o.before ?? []),
|
|
104
|
+
{ label: "Fill in", node: form },
|
|
105
|
+
{ label: "Let an AI interview you", node: paste({ kind: "priorities", title: "", onSaved: o.onSaved }) },
|
|
106
|
+
rawMode(o.path, o.onSaved),
|
|
107
|
+
]);
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
/** Any org file as highlighted text, with Read ahead of it on the Org screen. */
|
|
@@ -7,6 +7,7 @@
|
|
|
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
|
|
|
@@ -123,23 +124,47 @@ export function paste(opts) {
|
|
|
123
124
|
result.append(note);
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
// What the AI said around the file: its caveats and "check these" are worth reading first.
|
|
128
|
+
const notes = String(answer.value ?? "")
|
|
129
|
+
.replace(/<<<ROSTER FILE [^>]+>>>[\s\S]*?<<<ROSTER END>>>/g, "")
|
|
130
|
+
.trim();
|
|
131
|
+
|
|
126
132
|
for (const file of data.files ?? []) {
|
|
127
|
-
if (file.unchanged) continue;
|
|
128
133
|
const card = el("div", { className: "pastefile" });
|
|
129
|
-
const
|
|
134
|
+
const name = file.path.split("/").slice(-2).join("/");
|
|
135
|
+
if (file.unchanged) {
|
|
136
|
+
card.append(el("p", { className: "verdict", textContent: "✓ Same as what's already in " + name + ". Nothing to save." }));
|
|
137
|
+
result.append(card);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
130
140
|
card.append(
|
|
131
|
-
el("
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
el("p", {
|
|
142
|
+
className: "verdict" + (file.writable ? " ok" : " bad"),
|
|
143
|
+
textContent: file.writable
|
|
144
|
+
? "✓ Looks good. Saving replaces " + name + " with this:"
|
|
145
|
+
: "✗ " + file.path + " can't be written from here.",
|
|
135
146
|
}),
|
|
136
147
|
);
|
|
137
|
-
|
|
148
|
+
card.append(el("div", { className: "md doc pastepreview", innerHTML: mdlite(file.text) }));
|
|
149
|
+
|
|
150
|
+
if (notes) {
|
|
151
|
+
const said = el("details", { className: "pastenotes", open: true });
|
|
152
|
+
said.append(el("summary", { textContent: "Your AI also said" }), el("div", { className: "md doc", innerHTML: mdlite(notes) }));
|
|
153
|
+
card.append(said);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const diff = el("pre", { className: "diff", hidden: true });
|
|
138
157
|
diff.textContent = unifiedDiff(file.before, file.text, file.path);
|
|
139
|
-
|
|
158
|
+
const stat = diffStat(file.before, file.text);
|
|
159
|
+
const toggle = el("button", { className: "ghbtn", textContent: `Show changes (+${stat.added} −${stat.removed})` });
|
|
160
|
+
toggle.onclick = () => {
|
|
161
|
+
diff.hidden = !diff.hidden;
|
|
162
|
+
toggle.textContent = (diff.hidden ? "Show changes" : "Hide changes") + ` (+${stat.added} −${stat.removed})`;
|
|
163
|
+
};
|
|
140
164
|
|
|
165
|
+
const row = el("div", { className: "row" });
|
|
141
166
|
if (file.writable) {
|
|
142
|
-
const save = el("button", { className: "btn primary", textContent: "Save
|
|
167
|
+
const save = el("button", { className: "btn primary", textContent: "Save" });
|
|
143
168
|
save.onclick = async () => {
|
|
144
169
|
save.disabled = true;
|
|
145
170
|
save.textContent = "Saving…";
|
|
@@ -150,17 +175,19 @@ export function paste(opts) {
|
|
|
150
175
|
opts.onSaved?.();
|
|
151
176
|
} catch (err) {
|
|
152
177
|
save.disabled = false;
|
|
153
|
-
save.textContent = "Save
|
|
178
|
+
save.textContent = "Save";
|
|
154
179
|
card.append(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
155
180
|
}
|
|
156
181
|
};
|
|
157
|
-
|
|
182
|
+
row.append(save);
|
|
158
183
|
}
|
|
184
|
+
row.append(toggle);
|
|
185
|
+
card.append(row, diff);
|
|
159
186
|
result.append(card);
|
|
160
187
|
}
|
|
161
188
|
|
|
162
189
|
if (!(data.problems ?? []).length && !(data.files ?? []).length) {
|
|
163
|
-
result.append(el("p", { className: "
|
|
190
|
+
result.append(el("p", { className: "verdict bad", textContent: "✗ No file found in that reply." }));
|
|
164
191
|
}
|
|
165
192
|
};
|
|
166
193
|
|