@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(res, {
6917
- action,
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
- res.end(withBin(JSON.stringify(data)));
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;
@@ -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; write priorities.md too
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.21",
3
+ "version": "0.1.0-alpha.23",
4
4
  "description": "An agent-run org, powered by GitHub. Scaffold AI staff members whose brain is a repo.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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([...(o.before ?? []), { label: "Fill in", node: form }, rawMode(o.path, o.onSaved)]);
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 stat = diffStat(file.before, file.text);
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("b", { textContent: file.path }),
132
- el("span", {
133
- className: "meta",
134
- textContent: `+${stat.added} −${stat.removed}` + (file.writable ? "" : " · not writable"),
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
- const diff = el("pre", { className: "diff" });
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
- card.append(diff);
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 and commit" });
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 and commit";
178
+ save.textContent = "Save";
154
179
  card.append(el("p", { className: "err", textContent: String(err.message || err) }));
155
180
  }
156
181
  };
157
- card.append(save);
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: "sub", textContent: "Nothing to do." }));
190
+ result.append(el("p", { className: "verdict bad", textContent: "✗ No file found in that reply." }));
164
191
  }
165
192
  };
166
193