@nanocollective/roster 0.1.0-alpha.21 → 0.1.0-alpha.22

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
@@ -6439,7 +6439,7 @@ async function portalCommand(argv) {
6439
6439
  const handleSetup = async (url, req, res) => {
6440
6440
  const route = url.pathname.slice("/api/setup/".length);
6441
6441
  if (route === "status") {
6442
- json(res, { ...await setupStatus(startedIn), startedIn });
6442
+ json(res, { ...await setupStatus(startedIn), startedIn }, true);
6443
6443
  return;
6444
6444
  }
6445
6445
  if (route === "app") {
@@ -6820,7 +6820,7 @@ async function portalCommand(argv) {
6820
6820
  return;
6821
6821
  }
6822
6822
  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) => {
6823
+ collect({ ops: ws?.opsDir, offline: url.searchParams.get("offline") === "1" }).then((report3) => json(res, report3 ?? { findings: [], online: false, empty: true }, true)).catch((err) => {
6824
6824
  res.writeHead(200, { "content-type": "application/json" });
6825
6825
  res.end(JSON.stringify({ findings: [], error: String(err?.message ?? err) }));
6826
6826
  });
@@ -6901,7 +6901,7 @@ async function portalCommand(argv) {
6901
6901
  throw new Error(`"${handle}" is not in org.yaml`);
6902
6902
  }
6903
6903
  const plan3 = buildRetirePlan(w, org, handle, parseYaml);
6904
- json(res, { action, plan: plan3 });
6904
+ json(res, { action, plan: plan3 }, true);
6905
6905
  return;
6906
6906
  }
6907
6907
  if (!/^[a-z][a-z0-9-]{1,20}$/.test(handle)) {
@@ -6913,10 +6913,11 @@ async function portalCommand(argv) {
6913
6913
  const flags = hireFlags(url.searchParams);
6914
6914
  const plan2 = buildPlan(w, org, handle, flags, parseYaml);
6915
6915
  void readOrgSecret(org.org, plan2.staff.agentSecret).then(
6916
- (orgSecret) => json(res, {
6917
- action,
6918
- plan: { ...plan2, orgSecret, files: [...plan2.files.keys()].sort() }
6919
- })
6916
+ (orgSecret) => json(
6917
+ res,
6918
+ { action, plan: { ...plan2, orgSecret, files: [...plan2.files.keys()].sort() } },
6919
+ true
6920
+ )
6920
6921
  );
6921
6922
  } catch (err) {
6922
6923
  res.writeHead(400, { "content-type": "application/json" });
@@ -6990,7 +6991,7 @@ async function portalCommand(argv) {
6990
6991
  return;
6991
6992
  }
6992
6993
  if (url.pathname === "/api/fix") {
6993
- gather(w, url.searchParams.get("offline") === "1").then((items) => json(res, { items, text: fixBrief(w, items) })).catch((err) => {
6994
+ gather(w, url.searchParams.get("offline") === "1").then((items) => json(res, { items, text: fixBrief(w, items) }, true)).catch((err) => {
6994
6995
  res.writeHead(200, { "content-type": "application/json" });
6995
6996
  res.end(JSON.stringify({ items: [], error: String(err?.message ?? err) }));
6996
6997
  });
@@ -7515,12 +7516,13 @@ function refuseWrite(res) {
7515
7516
  res.writeHead(403, { "content-type": "application/json" });
7516
7517
  res.end(JSON.stringify({ error: "write actions need a local POST from the portal" }));
7517
7518
  }
7518
- function json(res, data) {
7519
+ function json(res, data, advice = false) {
7519
7520
  res.writeHead(200, {
7520
7521
  "content-type": "application/json; charset=utf-8",
7521
7522
  "cache-control": "no-store"
7522
7523
  });
7523
- res.end(withBin(JSON.stringify(data)));
7524
+ const text = JSON.stringify(data);
7525
+ res.end(advice ? withBin(text) : text);
7524
7526
  }
7525
7527
  function hireFlags(params) {
7526
7528
  const num = (k) => params.get(k) ? Number(params.get(k)) : void 0;
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.22",
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": {
@@ -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];
@@ -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