@nanocollective/roster 0.1.0-alpha.1

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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/cli.js +5679 -0
  4. package/docs/README.md +99 -0
  5. package/docs/agents.md +163 -0
  6. package/docs/architecture.md +121 -0
  7. package/docs/commands.md +223 -0
  8. package/docs/concepts.md +112 -0
  9. package/docs/cost.md +61 -0
  10. package/docs/developing.md +147 -0
  11. package/docs/doctor-codes.md +74 -0
  12. package/docs/export.md +113 -0
  13. package/docs/extending.md +97 -0
  14. package/docs/getting-started.md +134 -0
  15. package/docs/hosting.md +72 -0
  16. package/docs/manual-steps.md +163 -0
  17. package/docs/memory.md +71 -0
  18. package/docs/org-yaml.md +143 -0
  19. package/docs/portal.md +342 -0
  20. package/docs/prompts.md +133 -0
  21. package/docs/security.md +122 -0
  22. package/docs/session-workflow.md +112 -0
  23. package/docs/staff-yaml.md +163 -0
  24. package/docs/troubleshooting.md +189 -0
  25. package/docs/upgrading.md +83 -0
  26. package/docs/writing-a-charter.md +83 -0
  27. package/package.json +60 -0
  28. package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +33 -0
  29. package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +65 -0
  30. package/templates/brain/.github/workflows/%%STAFF%%-pr-mention.yaml +50 -0
  31. package/templates/brain/CHARTER.md +49 -0
  32. package/templates/brain/README.md +18 -0
  33. package/templates/brain/drafts/README.md +7 -0
  34. package/templates/brain/log/decisions.md +6 -0
  35. package/templates/brain/memory/INDEX.md +28 -0
  36. package/templates/brain/staff.yaml +44 -0
  37. package/templates/brain/strategy/README.md +7 -0
  38. package/templates/briefs/amend.md +60 -0
  39. package/templates/briefs/charter.md +47 -0
  40. package/templates/briefs/discover.md +61 -0
  41. package/templates/briefs/voice.md +53 -0
  42. package/templates/ops/.github/workflows/session.yaml +333 -0
  43. package/templates/ops/agents.mjs +143 -0
  44. package/templates/ops/compose.mjs +333 -0
  45. package/templates/ops/org/guardrails.md +14 -0
  46. package/templates/ops/org/operating.md +82 -0
  47. package/templates/ops/org/voice.md +40 -0
  48. package/templates/ops/prompts/_identity.md +14 -0
  49. package/templates/ops/prompts/_paths.md +15 -0
  50. package/templates/ops/prompts/daily.md +82 -0
  51. package/templates/ops/prompts/mention.md +53 -0
  52. package/templates/ops/prompts/pr-mention.md +57 -0
  53. package/templates/ops/runner-plan.mjs +65 -0
  54. package/templates/portal/css/base.css +104 -0
  55. package/templates/portal/css/brain.css +106 -0
  56. package/templates/portal/css/diff.css +28 -0
  57. package/templates/portal/css/graph.css +34 -0
  58. package/templates/portal/css/health.css +41 -0
  59. package/templates/portal/css/inbox.css +79 -0
  60. package/templates/portal/css/layout.css +98 -0
  61. package/templates/portal/css/markdown.css +54 -0
  62. package/templates/portal/css/setup.css +106 -0
  63. package/templates/portal/index.html +55 -0
  64. package/templates/portal/js/api.js +74 -0
  65. package/templates/portal/js/app.js +282 -0
  66. package/templates/portal/js/dialog.js +70 -0
  67. package/templates/portal/js/dom.js +106 -0
  68. package/templates/portal/js/icons.js +94 -0
  69. package/templates/portal/js/md.js +386 -0
  70. package/templates/portal/js/refresh.js +59 -0
  71. package/templates/portal/js/router.js +20 -0
  72. package/templates/portal/js/state.js +160 -0
  73. package/templates/portal/js/textdiff.js +96 -0
  74. package/templates/portal/js/views/app.js +128 -0
  75. package/templates/portal/js/views/brain.js +260 -0
  76. package/templates/portal/js/views/changed.js +157 -0
  77. package/templates/portal/js/views/checklist.js +87 -0
  78. package/templates/portal/js/views/docs.js +84 -0
  79. package/templates/portal/js/views/files.js +95 -0
  80. package/templates/portal/js/views/graph.js +436 -0
  81. package/templates/portal/js/views/health.js +158 -0
  82. package/templates/portal/js/views/inbox.js +549 -0
  83. package/templates/portal/js/views/memory.js +135 -0
  84. package/templates/portal/js/views/org.js +175 -0
  85. package/templates/portal/js/views/paste.js +142 -0
  86. package/templates/portal/js/views/prompt.js +412 -0
  87. package/templates/portal/js/views/repos.js +92 -0
  88. package/templates/portal/js/views/setup.js +344 -0
  89. package/templates/portal/js/views/staff.js +290 -0
@@ -0,0 +1,175 @@
1
+ /* The org layer, in one place.
2
+ *
3
+ * These five files are what every staff member inherits. Editing `org/voice.md` once is the
4
+ * whole extensibility story — the thing this arrangement exists to make possible — and until
5
+ * now you reached them through a staff member's Prompt screen, which is a strange route to a
6
+ * file that belongs to nobody in particular.
7
+ *
8
+ * `org.yaml` is here too, and is the one file that stops every prompt composing when it is
9
+ * wrong. The server validates it before writing.
10
+ */
11
+
12
+ import { getFile, post } from "../api.js";
13
+ import { el, esc, grow, kb } from "../dom.js";
14
+ import { mdlite } from "../md.js";
15
+ import { S, writeHash } from "../state.js";
16
+
17
+ /* What each file is for, in one line, because a list of five filenames tells you nothing
18
+ about which one to open. */
19
+ const DOCS = [
20
+ ["org.yaml", "Who and what: the humans, the repos, the defaults, which agent runs them.", "yaml"],
21
+ ["org/business.md", "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.", "md"],
22
+ ["org/operating.md", "The autonomy contract: what they do without asking, what they escalate, how a run starts and ends.", "md"],
23
+ ["org/guardrails.md", "The non-negotiables. Every staff member is bound by these and cannot argue with them.", "md"],
24
+ ["org/voice.md", "How everything they write reads. Change it once, and every surface changes on the next run.", "md"],
25
+ ];
26
+
27
+ export function viewOrg(m) {
28
+ const org = S.data;
29
+ m.append(el("h1", { textContent: org.name + " · org" }));
30
+ m.append(
31
+ el("p", {
32
+ className: "sub",
33
+ textContent:
34
+ "The layer every staff member inherits. Editing one of these reaches all " +
35
+ org.staff.length + " of them on their next run.",
36
+ }),
37
+ );
38
+
39
+ const facts = el("div", { className: "grid", style: "margin-bottom:18px" });
40
+ const human = org.human ?? {};
41
+ facts.append(
42
+ kv("Organisation", esc(org.org), org.name),
43
+ kv("They answer to", esc(human.name ?? human.github ?? "nobody"), human.github ? "@" + human.github : "no github handle in org.yaml"),
44
+ kv("Staff", String(org.staff.length), org.staff.map((s) => s.handle).join(", ")),
45
+ );
46
+ m.append(facts);
47
+
48
+ const split = el("div", { className: "split" });
49
+ const tree = el("div", { className: "tree" });
50
+ const viewer = el("div", { className: "viewer" });
51
+ split.append(tree, viewer);
52
+ m.append(split);
53
+
54
+ const group = el("div", { className: "navgroup" });
55
+ group.append(el("div", { className: "ghead", textContent: "The org layer" }));
56
+ for (const [path, why] of DOCS) {
57
+ const b = el("button", { className: "tfile tlayer", title: why });
58
+ b.dataset.key = path;
59
+ const t = el("span", { className: "t" });
60
+ t.append(
61
+ el("span", { className: "lname", textContent: path.replace(/^org\//, "") }),
62
+ el("span", { className: "lrepo", textContent: why }),
63
+ );
64
+ b.append(t);
65
+ b.onclick = () => open(path);
66
+ group.append(b);
67
+ }
68
+ tree.append(group);
69
+
70
+ /* Declared before the first `open()`, not after: `const` is not hoisted, and putting it
71
+ below the call made the first paint fail with "cannot access 'full' before
72
+ initialization" rather than rendering anything. */
73
+ const full = (path) => S.data.opsName + "/" + path;
74
+
75
+ open(S.orgOpen ?? DOCS[0][0]);
76
+
77
+ function open(path) {
78
+ S.orgOpen = path;
79
+ writeHash(false);
80
+ for (const o of tree.querySelectorAll(".tfile")) {
81
+ o.setAttribute("aria-current", String(o.dataset.key === path));
82
+ }
83
+ show(path);
84
+ }
85
+
86
+ async function show(path) {
87
+ viewer.replaceChildren(el("p", { className: "empty", textContent: "Loading…" }));
88
+ let text;
89
+ try {
90
+ text = await getFile(full(path));
91
+ } catch (e) {
92
+ viewer.replaceChildren(el("p", { className: "empty err", textContent: e.message }));
93
+ return;
94
+ }
95
+ render(path, text);
96
+ }
97
+
98
+ function render(path, text) {
99
+ viewer.replaceChildren();
100
+ const why = DOCS.find(([p]) => p === path)?.[1] ?? "";
101
+ const head = el("div", { className: "row", style: "margin-bottom:4px" });
102
+ head.append(el("span", { className: "meta", textContent: full(path) + " · " + kb(text.length) }));
103
+ head.append(el("button", { className: "ghbtn", textContent: "Edit", onclick: () => edit(path, text) }));
104
+ viewer.append(head, el("p", { className: "factsub", textContent: why }));
105
+
106
+ if (path.endsWith(".yaml")) {
107
+ viewer.append(el("pre", { className: "code", textContent: text }));
108
+ return;
109
+ }
110
+ viewer.append(el("div", { className: "md doc", innerHTML: mdlite(text) }));
111
+ }
112
+
113
+ function edit(path, text) {
114
+ viewer.replaceChildren();
115
+ viewer.append(
116
+ el("div", { className: "row", style: "margin-bottom:10px" }, [
117
+ el("span", { className: "meta", textContent: full(path) }),
118
+ ]),
119
+ );
120
+
121
+ const ta = el("textarea", { value: text, className: "editor" });
122
+ viewer.append(ta);
123
+ grow(ta);
124
+ ta.addEventListener("input", () => grow(ta));
125
+
126
+ const status = el("span", { className: "meta" });
127
+ const save = el("button", { className: "ghbtn primary", textContent: "Save and commit" });
128
+ const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
129
+ cancel.onclick = () => render(path, text);
130
+ viewer.append(el("div", { className: "row", style: "margin-top:10px" }, [save, cancel, status]));
131
+ viewer.append(
132
+ el("p", {
133
+ className: "editnote",
134
+ textContent:
135
+ "Commits to " + S.data.opsName + " and pushes. Picked up by every staff member on their " +
136
+ "next run." +
137
+ (path.endsWith(".yaml")
138
+ ? " org.yaml is checked before it is written: bad YAML, or a missing org or name, is refused rather than committed."
139
+ : ""),
140
+ }),
141
+ );
142
+
143
+ save.onclick = async () => {
144
+ if (ta.value === text) {
145
+ status.textContent = "nothing changed";
146
+ return;
147
+ }
148
+ if (!confirm("Commit " + full(path) + " and push?\n\nEvery staff member picks this up on their next run.")) return;
149
+ save.disabled = cancel.disabled = true;
150
+ status.textContent = "committing…";
151
+ status.className = "meta";
152
+ try {
153
+ const r = await post({ path: full(path), text: ta.value }, "/api/save");
154
+ status.textContent = r.pushed
155
+ ? "committed and pushed · " + r.sha
156
+ : "committed " + r.sha + ", but the push failed: " + (r.note ?? "");
157
+ status.className = r.pushed ? "meta ok" : "meta warn";
158
+ render(path, ta.value);
159
+ } catch (e) {
160
+ // A refused org.yaml is the interesting case: the reason is the whole message.
161
+ status.textContent = e.message;
162
+ status.className = "meta err";
163
+ save.disabled = cancel.disabled = false;
164
+ }
165
+ };
166
+ }
167
+ }
168
+
169
+ function kv(label, value, sub) {
170
+ const d = el("div", { className: "kv" });
171
+ d.innerHTML =
172
+ '<div class="k">' + esc(label) + '</div><div class="v">' + value +
173
+ (sub ? "<small>" + esc(sub) + "</small>" : "") + "</div>";
174
+ return d;
175
+ }
@@ -0,0 +1,142 @@
1
+ /* Copy a prompt, paste it into whatever you use, paste the answer back.
2
+ *
3
+ * The two files nothing can generate — org/business.md and every CHARTER.md — have always been
4
+ * handed off with "paste this into your AI". The half that was missing is the way back: the
5
+ * model has no filesystem, so its answer is a message, and a message has to become a file.
6
+ *
7
+ * Nothing here writes. The panel parses, shows a diff, and waits for a second click. */
8
+
9
+ import { getBrief, parsePaste, saveFile } from "../api.js";
10
+ import { el, toClipboard } from "../dom.js";
11
+ import { unifiedDiff, diffStat } from "../textdiff.js";
12
+
13
+ /**
14
+ * @param {{kind: string, staff?: string, title?: string, onSaved?: () => void}} opts
15
+ */
16
+ export function paste(opts) {
17
+ const box = el("div", { className: "paste" });
18
+ box.append(el("h3", { textContent: opts.title ?? "Write it with your own AI" }));
19
+
20
+ const status = el("p", { className: "sub", textContent: "Building the prompt…" });
21
+ box.append(status);
22
+
23
+ const actions = el("div", { className: "row" });
24
+ const copy = el("button", { className: "btn primary", textContent: "Copy the prompt" });
25
+ const show = el("button", { className: "btn", textContent: "Show it" });
26
+ actions.append(copy, show);
27
+ box.append(actions);
28
+
29
+ const preview = el("pre", { className: "briefpreview", hidden: true });
30
+ box.append(preview);
31
+
32
+ const answer = el("textarea", {
33
+ className: "pastebox",
34
+ placeholder:
35
+ "Paste the whole reply here, including anything your AI said around the file. " +
36
+ "Only the part between the sentinels is used.",
37
+ });
38
+ answer.rows = 8;
39
+
40
+ const check = el("button", { className: "btn", textContent: "Check the answer", disabled: true });
41
+ const result = el("div", { className: "pasteresult" });
42
+
43
+ let brief = null;
44
+
45
+ getBrief(opts.kind, opts.staff)
46
+ .then((data) => {
47
+ if (data.error) throw new Error(data.error);
48
+ brief = data;
49
+ status.textContent =
50
+ "Paste this into Claude, ChatGPT or anything else. It carries every file it refers to, " +
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.";
53
+ box.append(
54
+ el("p", { className: "sub", textContent: "Then paste its whole reply back here:" }),
55
+ answer,
56
+ el("div", { className: "row" }, [check]),
57
+ result,
58
+ );
59
+ check.disabled = false;
60
+ })
61
+ .catch((err) => {
62
+ status.className = "err";
63
+ status.textContent = String(err.message || err);
64
+ copy.disabled = show.disabled = true;
65
+ });
66
+
67
+ copy.onclick = () => brief && toClipboard(brief.text, copy, "Copied");
68
+ show.onclick = () => {
69
+ preview.hidden = !preview.hidden;
70
+ preview.textContent = brief?.text ?? "";
71
+ show.textContent = preview.hidden ? "Show it" : "Hide it";
72
+ };
73
+
74
+ check.onclick = async () => {
75
+ result.replaceChildren(el("p", { className: "sub", textContent: "Reading…" }));
76
+ let data;
77
+ try {
78
+ data = await parsePaste(opts.kind, opts.staff, answer.value);
79
+ } catch (err) {
80
+ result.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) }));
81
+ return;
82
+ }
83
+ result.replaceChildren();
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
+ }
99
+
100
+ for (const file of data.files ?? []) {
101
+ if (file.unchanged) continue;
102
+ const card = el("div", { className: "pastefile" });
103
+ const stat = diffStat(file.before, file.text);
104
+ card.append(
105
+ el("b", { textContent: file.path }),
106
+ el("span", {
107
+ className: "meta",
108
+ textContent: `+${stat.added} −${stat.removed}` + (file.writable ? "" : " · not writable"),
109
+ }),
110
+ );
111
+ const diff = el("pre", { className: "diff" });
112
+ diff.textContent = unifiedDiff(file.before, file.text, file.path);
113
+ card.append(diff);
114
+
115
+ if (file.writable) {
116
+ const save = el("button", { className: "btn primary", textContent: "Save and commit" });
117
+ save.onclick = async () => {
118
+ save.disabled = true;
119
+ save.textContent = "Saving…";
120
+ try {
121
+ await saveFile(file.path, file.text, `portal: write ${file.path}`);
122
+ save.textContent = "Saved";
123
+ card.classList.add("saved");
124
+ opts.onSaved?.();
125
+ } catch (err) {
126
+ save.disabled = false;
127
+ save.textContent = "Save and commit";
128
+ card.append(el("p", { className: "err", textContent: String(err.message || err) }));
129
+ }
130
+ };
131
+ card.append(save);
132
+ }
133
+ result.append(card);
134
+ }
135
+
136
+ if (!(data.problems ?? []).length && !(data.files ?? []).length) {
137
+ result.append(el("p", { className: "sub", textContent: "Nothing to do." }));
138
+ }
139
+ };
140
+
141
+ return box;
142
+ }