@kubitnodes/stellaris 0.3.18 → 0.3.20

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 (57) hide show
  1. package/dist/cli/index.js +602 -196
  2. package/dist/server/index.js +606 -156
  3. package/package.json +1 -1
  4. package/web/dist/assets/{BackLink-DUOMS3fv.js → BackLink-BjfX2KZl.js} +1 -1
  5. package/web/dist/assets/{ChannelForms-DfG4vYvk.js → ChannelForms-BYrcUldk.js} +1 -1
  6. package/web/dist/assets/ChannelView-GJ_DusLQ.js +2 -0
  7. package/web/dist/assets/CitizenView-Cx7IYSRr.js +2 -0
  8. package/web/dist/assets/{CitizensView-bKbUaCab.js → CitizensView-IKH7_yNQ.js} +1 -1
  9. package/web/dist/assets/{CliIcon-DTVk0pGU.js → CliIcon-vSuwLv2K.js} +1 -1
  10. package/web/dist/assets/CronsView-DJbi8Qhx.js +1 -0
  11. package/web/dist/assets/{KnowledgeView-Dt7wE3FT.js → KnowledgeView-CoYOG9tI.js} +1 -1
  12. package/web/dist/assets/{Markdown-DeF9PKNm.js → Markdown-YC-fbHiE.js} +4 -4
  13. package/web/dist/assets/MessageItem-D7MUCA_T.js +1 -0
  14. package/web/dist/assets/{MetricsView-Bw7nL6Y9.js → MetricsView-CE94VW0I.js} +1 -1
  15. package/web/dist/assets/{NeedsYouView-DbESNtQT.js → NeedsYouView-Otnm3lcG.js} +1 -1
  16. package/web/dist/assets/{Overview-Dv7SS4yW.js → Overview-B8f6rLb2.js} +1 -1
  17. package/web/dist/assets/{Pane-v044FaP5.js → Pane-_D7n5uKH.js} +1 -1
  18. package/web/dist/assets/{Picker-wWsO86nu.js → Picker-DUiiQMFN.js} +1 -1
  19. package/web/dist/assets/ProjectView-D_ACOmEd.js +1 -0
  20. package/web/dist/assets/ProposalView-4d0U3nEb.js +2 -0
  21. package/web/dist/assets/{ProposalsView-dVA50yG1.js → ProposalsView-BovF6CMD.js} +1 -1
  22. package/web/dist/assets/{RunnersView-C-l5QYIa.js → RunnersView-B2_uCVHC.js} +1 -1
  23. package/web/dist/assets/{SocietyView--mWRXRj_.js → SocietyView-Bf0iX8AY.js} +1 -1
  24. package/web/dist/assets/SubjectThread-B_Vmgs3d.js +1 -0
  25. package/web/dist/assets/TaskFileView-BzO0gjE6.js +4 -0
  26. package/web/dist/assets/{TaskView-BgJcOmGj.js → TaskView-DB_Nji99.js} +2 -2
  27. package/web/dist/assets/{TasksView-f8KIsif_.js → TasksView-Bz36YYWq.js} +1 -1
  28. package/web/dist/assets/{ThreadForms-CXsunS-N.js → ThreadForms-1mbMZsbv.js} +1 -1
  29. package/web/dist/assets/ThreadItem-QJSA92C4.js +1 -0
  30. package/web/dist/assets/ThreadView-BECLqt5B.js +1 -0
  31. package/web/dist/assets/{citizen-VtLNxjtf.js → citizen-DVFr5T6R.js} +1 -1
  32. package/web/dist/assets/{compose-D-SO-HE-.js → compose-_6feqP1t.js} +1 -1
  33. package/web/dist/assets/{format-CkySAdH1.js → format-DxXtbdNM.js} +1 -1
  34. package/web/dist/assets/governance-C9xH4QTm.js +1 -0
  35. package/web/dist/assets/index-BEVCPm4C.js +23 -0
  36. package/web/dist/assets/index-BkOq4PSU.css +2 -0
  37. package/web/dist/assets/link-CTjFMNoY.js +1 -0
  38. package/web/dist/assets/{tasks-CD70eybR.js → tasks-B6I9DJ8O.js} +1 -1
  39. package/web/dist/assets/useMatch-B3RzAe8y.js +1 -0
  40. package/web/dist/assets/useStickyScroll-Ca7sWbGH.js +1 -0
  41. package/web/dist/index.html +9 -9
  42. package/web/dist/assets/ChannelView-DTLhLXy4.js +0 -2
  43. package/web/dist/assets/CitizenView-q24CExsI.js +0 -2
  44. package/web/dist/assets/CronsView-DaG1xhQu.js +0 -1
  45. package/web/dist/assets/MessageItem-DDs7m3-4.js +0 -1
  46. package/web/dist/assets/ProjectView--nzlqSDA.js +0 -1
  47. package/web/dist/assets/ProposalView-Cdw_xzG1.js +0 -1
  48. package/web/dist/assets/SubjectThread-DvvsxHN9.js +0 -1
  49. package/web/dist/assets/TaskFileView-DYuPrCdk.js +0 -4
  50. package/web/dist/assets/ThreadCard-BRLvjdz3.js +0 -1
  51. package/web/dist/assets/ThreadView-DzpaXkik.js +0 -1
  52. package/web/dist/assets/governance-1UeTD2K_.js +0 -1
  53. package/web/dist/assets/index-CwkK8XtJ.js +0 -23
  54. package/web/dist/assets/index-DaES0xVj.css +0 -2
  55. package/web/dist/assets/link-BN04dcBX.js +0 -1
  56. package/web/dist/assets/useMatch-i33QT_GI.js +0 -1
  57. package/web/dist/assets/useStickyScroll-WSVpAMrA.js +0 -1
package/dist/cli/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // apps/cli/dist/index.js
4
- import { readFile as readFile6 } from "node:fs/promises";
4
+ import { readFile as readFile5 } from "node:fs/promises";
5
5
  import path7 from "node:path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // packages/board-core/dist/board.js
9
- import { mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rename as rename3, stat as stat2 } from "node:fs/promises";
9
+ import { chmod as chmod2, cp, mkdir as mkdir4, readdir as readdir3, readFile as readFile4, rename as rename3, rm as rm2, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
10
10
  import path6 from "node:path";
11
11
  import { decodeTime, monotonicFactory } from "ulid";
12
12
  import { z as z13 } from "zod";
@@ -422,6 +422,13 @@ var MessageFrontmatterSchema = z4.object({
422
422
  ts: IsoDateTimeSchema,
423
423
  mentions: z4.array(NameSchema)
424
424
  });
425
+ var MessageRefSchema = MessageFrontmatterSchema.pick({
426
+ id: true,
427
+ author: true,
428
+ channel: true,
429
+ thread: true,
430
+ ts: true
431
+ });
425
432
  var TaskStatusSchema = z4.enum(["open", "claimed", "done", "abandoned"]);
426
433
  var ThreadStateSchema = z4.enum(["open", "closed"]);
427
434
  var ThreadSubjectSchema = z4.object({
@@ -622,7 +629,7 @@ var ArchiveProposalSchema = z4.object({
622
629
  var SkillProposalSchema = z4.object({
623
630
  name: NameSchema,
624
631
  summary: z4.string().min(1).max(200),
625
- body: z4.string().min(1)
632
+ body: z4.string().min(1).optional()
626
633
  });
627
634
  var ProposalCharterSchemas = {
628
635
  role: RoleCharterSchema,
@@ -749,6 +756,7 @@ var HomeFileDiffSchema = HomeFileChangeSchema.pick({ path: true, status: true })
749
756
  var KnowledgeSchema = z4.object({
750
757
  topic: NameSchema,
751
758
  project: NameSchema.nullable(),
759
+ revision: z4.number().int().min(0),
752
760
  updatedBy: NameSchema,
753
761
  updatedAt: IsoDateTimeSchema
754
762
  });
@@ -771,6 +779,24 @@ var ProposalOriginSchema = z4.object({
771
779
  thread: UlidSchema.optional(),
772
780
  channel: NameSchema.optional()
773
781
  });
782
+ var SKILL_FILES_LIMIT = 100;
783
+ var SKILL_BYTES_LIMIT = 10 * 1024 * 1024;
784
+ var SkillFileSchema = z4.object({
785
+ path: z4.string().min(1),
786
+ size: z4.number().int().nonnegative(),
787
+ executable: z4.boolean()
788
+ });
789
+ var SkillFilesSchema = z4.object({
790
+ /** The commit of the proposer's home the files came from. */
791
+ commit: CommitIdSchema,
792
+ takenAt: IsoDateTimeSchema,
793
+ files: z4.array(SkillFileSchema)
794
+ });
795
+ var SkillFileContentSchema = SkillFileSchema.extend({
796
+ text: z4.string().nullable(),
797
+ /** The text was cut at its size limit. */
798
+ truncated: z4.boolean()
799
+ });
774
800
  var ProposalFrontmatterSchema = z4.object({
775
801
  id: UlidSchema,
776
802
  kind: ProposalKindSchema,
@@ -779,6 +805,8 @@ var ProposalFrontmatterSchema = z4.object({
779
805
  createdAt: IsoDateTimeSchema,
780
806
  /** Absent for a proposal filed outside a turn, or before proposals recorded it. */
781
807
  filedFrom: ProposalOriginSchema.optional(),
808
+ /** A skill proposal's folder, once taken; absent for one proposed as text alone. */
809
+ skillFiles: SkillFilesSchema.optional(),
782
810
  decidedBy: NameSchema.optional(),
783
811
  decidedAt: IsoDateTimeSchema.optional(),
784
812
  reason: z4.string().optional(),
@@ -881,7 +909,9 @@ var VerbInputs = {
881
909
  /** A project you belong to, or null for society knowledge, which the steward and the user curate. */
882
910
  project: NameSchema.nullable().default(null),
883
911
  topic: NameSchema,
884
- body: z5.string().min(1)
912
+ body: z5.string().min(1),
913
+ /** The `revision` in the frontmatter of the topic you read, 0 when it names none or for a new topic. */
914
+ revision: z5.number().int().min(0)
885
915
  }),
886
916
  remove_knowledge: z5.object({
887
917
  /** As for write_knowledge: a project you belong to, or null for society knowledge. */
@@ -982,6 +1012,7 @@ var BOARD_EVENT_TYPES = [
982
1012
  "proposal.created",
983
1013
  "proposal.decided",
984
1014
  "proposal.provisioned",
1015
+ "proposal.files_taken",
985
1016
  "channel.added",
986
1017
  "channel.archived",
987
1018
  "role.added",
@@ -1388,6 +1419,9 @@ var RunnerAnswerSchema = z7.discriminatedUnion("ok", [
1388
1419
  ]);
1389
1420
  var WarmSessionsSchema = z7.object({ warm: z7.array(z7.string()) });
1390
1421
  var FileManifestSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
1422
+ function fileVersion(sha, executable) {
1423
+ return executable ? `${sha}+x` : sha;
1424
+ }
1391
1425
  var FileReadSchema = z7.object({ paths: z7.array(z7.string()) });
1392
1426
  var FileContentsSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
1393
1427
  function isSafeRelativePath(file) {
@@ -1706,7 +1740,7 @@ var RunnerConfigSchema = z11.object({
1706
1740
  });
1707
1741
 
1708
1742
  // packages/shared/dist/version.js
1709
- var STELLARIS_VERSION = "0.3.18";
1743
+ var STELLARIS_VERSION = "0.3.20";
1710
1744
 
1711
1745
  // packages/board-core/dist/errors.js
1712
1746
  var BoardError = class extends Error {
@@ -1719,7 +1753,7 @@ var BoardError = class extends Error {
1719
1753
  };
1720
1754
 
1721
1755
  // packages/board-core/dist/events.js
1722
- import { appendFile, readFile as readFile2 } from "node:fs/promises";
1756
+ import { appendFile, open, stat } from "node:fs/promises";
1723
1757
  import path2 from "node:path";
1724
1758
 
1725
1759
  // packages/board-core/dist/fs.js
@@ -1760,7 +1794,9 @@ async function readMarkdown(file, schema) {
1760
1794
  return { data: schema.parse(parsed.data), body: parsed.content.replace(/^\n/, "") };
1761
1795
  }
1762
1796
  async function readLooseMarkdown(file) {
1763
- const raw = await readFile(file, "utf8");
1797
+ return parseLooseMarkdown(await readFile(file, "utf8"));
1798
+ }
1799
+ function parseLooseMarkdown(raw) {
1764
1800
  try {
1765
1801
  const parsed = parseMatter(raw);
1766
1802
  return { data: parsed.data, body: parsed.content.replace(/^\n/, ""), error: null };
@@ -1802,10 +1838,19 @@ async function listDirs(dir) {
1802
1838
 
1803
1839
  // packages/board-core/dist/events.js
1804
1840
  var LEADING_ID = /^\{"id":"([0-9A-HJKMNP-TV-Z]{26})"/;
1841
+ var ID_PREFIX_BYTES = 64;
1842
+ var NEWLINE = 10;
1805
1843
  var EventLog = class {
1806
1844
  file;
1807
1845
  newId;
1808
1846
  now;
1847
+ lines = [];
1848
+ /** Bytes the index covers: always the end of a complete line. */
1849
+ indexed = 0;
1850
+ /** The indexed file, to tell a replaced log from a grown one. */
1851
+ inode = null;
1852
+ scanning = Promise.resolve();
1853
+ listeners = /* @__PURE__ */ new Set();
1809
1854
  constructor(file, newId, now) {
1810
1855
  this.file = file;
1811
1856
  this.newId = newId;
@@ -1822,48 +1867,97 @@ var EventLog = class {
1822
1867
  await ensureDir(path2.dirname(this.file));
1823
1868
  await appendFile(this.file, `${JSON.stringify(event)}
1824
1869
  `, "utf8");
1870
+ for (const listener of this.listeners) {
1871
+ try {
1872
+ listener(event);
1873
+ } catch {
1874
+ }
1875
+ }
1825
1876
  return event;
1826
1877
  }
1878
+ /**
1879
+ * Calls `listener` after each event this process appends, until the returned function is
1880
+ * called. Another process's appends, such as the admin CLI's, are seen only by the next read.
1881
+ */
1882
+ onAppend(listener) {
1883
+ this.listeners.add(listener);
1884
+ return () => {
1885
+ this.listeners.delete(listener);
1886
+ };
1887
+ }
1827
1888
  /** Events with an id greater than `since`, oldest first. `null` reads from the beginning. */
1828
1889
  async readSince(since, limit = 1e3) {
1829
- if (!await exists(this.file)) {
1830
- return [];
1831
- }
1832
- const raw = await readFile2(this.file, "utf8");
1833
- const events = [];
1834
- for (const line of raw.split("\n")) {
1835
- if (line.trim().length === 0) {
1836
- continue;
1837
- }
1838
- const leading = LEADING_ID.exec(line)?.[1];
1839
- if (since !== null && leading !== void 0 && leading <= since) {
1840
- continue;
1841
- }
1842
- const event = BoardEventSchema.parse(JSON.parse(line));
1843
- if (since !== null && event.id <= since) {
1890
+ await this.refresh();
1891
+ const chosen = [];
1892
+ for (const line of this.lines) {
1893
+ if (since !== null && line.id <= since) {
1844
1894
  continue;
1845
1895
  }
1846
- events.push(event);
1847
- if (events.length >= limit) {
1896
+ chosen.push(line);
1897
+ if (chosen.length >= limit) {
1848
1898
  break;
1849
1899
  }
1850
1900
  }
1851
- return events;
1901
+ const first = chosen[0];
1902
+ const last = chosen.at(-1);
1903
+ if (first === void 0 || last === void 0) {
1904
+ return [];
1905
+ }
1906
+ const span = await this.readBytes(first.start, last.end);
1907
+ return chosen.map((line) => BoardEventSchema.parse(JSON.parse(span.toString("utf8", line.start - first.start, line.end - first.start))));
1852
1908
  }
1853
1909
  /** The id of the newest event, or null for an empty log. */
1854
1910
  async lastId() {
1855
- if (!await exists(this.file)) {
1856
- return null;
1911
+ await this.refresh();
1912
+ return this.lines.at(-1)?.id ?? null;
1913
+ }
1914
+ /** Brings the index up to the file's end, one scan at a time. */
1915
+ refresh() {
1916
+ this.scanning = this.scanning.then(() => this.scan(), () => this.scan());
1917
+ return this.scanning;
1918
+ }
1919
+ async scan() {
1920
+ const info = await stat(this.file).catch((error) => {
1921
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1922
+ return null;
1923
+ }
1924
+ throw error;
1925
+ });
1926
+ if (info === null || info.ino !== this.inode || info.size < this.indexed) {
1927
+ this.lines = [];
1928
+ this.indexed = 0;
1929
+ this.inode = info?.ino ?? null;
1930
+ }
1931
+ if (info === null || info.size === this.indexed) {
1932
+ return;
1933
+ }
1934
+ const added = await this.readBytes(this.indexed, info.size);
1935
+ let start = 0;
1936
+ for (let at = added.indexOf(NEWLINE); at >= 0; at = added.indexOf(NEWLINE, start)) {
1937
+ const prefix = added.toString("utf8", start, Math.min(at, start + ID_PREFIX_BYTES));
1938
+ if (prefix.trim().length > 0) {
1939
+ const id = LEADING_ID.exec(prefix)?.[1] ?? BoardEventSchema.parse(JSON.parse(added.toString("utf8", start, at))).id;
1940
+ this.lines.push({ id, start: this.indexed + start, end: this.indexed + at });
1941
+ }
1942
+ start = at + 1;
1943
+ }
1944
+ this.indexed += start;
1945
+ }
1946
+ async readBytes(from, to) {
1947
+ const handle = await open(this.file, "r");
1948
+ try {
1949
+ const buffer = Buffer.alloc(to - from);
1950
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, from);
1951
+ return buffer.subarray(0, bytesRead);
1952
+ } finally {
1953
+ await handle.close();
1857
1954
  }
1858
- const lines = (await readFile2(this.file, "utf8")).trimEnd().split("\n");
1859
- const last = lines.at(-1) ?? "";
1860
- return last.length === 0 ? null : BoardEventSchema.parse(JSON.parse(last)).id;
1861
1955
  }
1862
1956
  };
1863
1957
 
1864
1958
  // packages/board-core/dist/files.js
1865
1959
  import { createHash } from "node:crypto";
1866
- import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rename as rename2, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
1960
+ import { mkdir as mkdir2, readdir as readdir2, readFile as readFile2, rename as rename2, rm, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
1867
1961
  import path3 from "node:path";
1868
1962
  var MAX_FILE_BYTES = 8 * 1024 * 1024;
1869
1963
  function isMissing2(error) {
@@ -1885,9 +1979,9 @@ var FileTree = class {
1885
1979
  }
1886
1980
  try {
1887
1981
  const absolute = path3.join(root, ...relative.split("/"));
1888
- const info = await stat(absolute);
1982
+ const info = await stat2(absolute);
1889
1983
  if (info.isFile() && info.size <= MAX_FILE_BYTES) {
1890
- files[relative] = (await readFile3(absolute)).toString("base64");
1984
+ files[relative] = (await readFile2(absolute)).toString("base64");
1891
1985
  }
1892
1986
  } catch (error) {
1893
1987
  if (!isMissing2(error)) {
@@ -1943,17 +2037,18 @@ var FileTree = class {
1943
2037
  }
1944
2038
  async hash(absolute) {
1945
2039
  try {
1946
- const info = await stat(absolute);
2040
+ const info = await stat2(absolute);
1947
2041
  if (info.size > MAX_FILE_BYTES) {
1948
2042
  return null;
1949
2043
  }
2044
+ const executable = (info.mode & 73) !== 0;
1950
2045
  const known = this.hashes.get(absolute);
1951
2046
  if (known !== void 0 && known.mtimeMs === info.mtimeMs && known.size === info.size) {
1952
- return known.sha;
2047
+ return fileVersion(known.sha, executable);
1953
2048
  }
1954
- const sha = createHash("sha256").update(await readFile3(absolute)).digest("hex");
2049
+ const sha = createHash("sha256").update(await readFile2(absolute)).digest("hex");
1955
2050
  this.hashes.set(absolute, { mtimeMs: info.mtimeMs, size: info.size, sha });
1956
- return sha;
2051
+ return fileVersion(sha, executable);
1957
2052
  } catch (error) {
1958
2053
  if (isMissing2(error)) {
1959
2054
  return null;
@@ -1964,7 +2059,7 @@ var FileTree = class {
1964
2059
  };
1965
2060
 
1966
2061
  // packages/board-core/dist/homes.js
1967
- import { chmod, mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
2062
+ import { chmod, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
1968
2063
  import path4 from "node:path";
1969
2064
  import { execa } from "execa";
1970
2065
  var HOME_GITIGNORE = [
@@ -2108,7 +2203,7 @@ var HomeRepos = class {
2108
2203
  await writeFile3(ignore, IGNORE_FILE, "utf8");
2109
2204
  await git(["add", "--all"], home);
2110
2205
  await git(["commit", "--quiet", "--allow-empty", "-m", "home: created by the board"], home);
2111
- } else if (await readFile4(ignore, "utf8").catch(() => "") !== IGNORE_FILE) {
2206
+ } else if (await readFile3(ignore, "utf8").catch(() => "") !== IGNORE_FILE) {
2112
2207
  await writeFile3(ignore, IGNORE_FILE, "utf8");
2113
2208
  await git(["add", ".gitignore"], home);
2114
2209
  await git(["commit", "--quiet", "-m", "home: the board's ignore file"], home);
@@ -2204,6 +2299,55 @@ var HomeRepos = class {
2204
2299
  }
2205
2300
  return files;
2206
2301
  }
2302
+ /**
2303
+ * The files of a folder in a home's newest commit, by path within the folder, with the commit;
2304
+ * conflict copies and anything but a regular file are left out. Null when the home is not a
2305
+ * repository yet.
2306
+ */
2307
+ async folder(home, folder) {
2308
+ if (!await exists(path4.join(home, ".git"))) {
2309
+ return null;
2310
+ }
2311
+ const commit = (await read(home, ["rev-parse", "--verify", "HEAD^{commit}"])).trim();
2312
+ const prefix = `${folder}/`;
2313
+ const listed = await read(home, [
2314
+ "ls-tree",
2315
+ "-r",
2316
+ "-z",
2317
+ "--long",
2318
+ "--full-tree",
2319
+ commit,
2320
+ prefix
2321
+ ]);
2322
+ const files = [];
2323
+ for (const entry of listed.split("\0")) {
2324
+ const match = /^(100644|100755) blob ([0-9a-f]+) +(\d+)\t(.+)$/.exec(entry);
2325
+ const [, mode = "", object = "", size = "0", file = ""] = match ?? [];
2326
+ if (match === null || !file.startsWith(prefix) || !isSafeRelativePath(file) || conflictCopyOf(file) !== null) {
2327
+ continue;
2328
+ }
2329
+ files.push({
2330
+ path: file.slice(prefix.length),
2331
+ object,
2332
+ size: Number.parseInt(size, 10),
2333
+ executable: mode === "100755"
2334
+ });
2335
+ }
2336
+ return { commit, files };
2337
+ }
2338
+ /** A blob of a home's repository, as `folder` names it. */
2339
+ async blob(home, object) {
2340
+ const result = await execa("git", ["cat-file", "blob", object], {
2341
+ cwd: home,
2342
+ encoding: "buffer",
2343
+ stripFinalNewline: false,
2344
+ reject: false
2345
+ });
2346
+ if (result.exitCode !== 0) {
2347
+ throw new Error(`git cat-file failed: ${Buffer.from(result.stderr).toString().trim()}`);
2348
+ }
2349
+ return Buffer.from(result.stdout);
2350
+ }
2207
2351
  /** The conflict copies a merge added, against its first parent. */
2208
2352
  async keptCopies(home, parent, commit) {
2209
2353
  const added = await read(home, [
@@ -2619,6 +2763,10 @@ var BoardPaths = class {
2619
2763
  proposal(id) {
2620
2764
  return path5.join(this.proposals(), `${id}.md`);
2621
2765
  }
2766
+ /** A skill proposal's folder, as the board took it from its proposer's home. */
2767
+ proposalFiles(id) {
2768
+ return path5.join(this.proposals(), id);
2769
+ }
2622
2770
  decisions() {
2623
2771
  return path5.join(this.society, "decisions");
2624
2772
  }
@@ -2870,6 +3018,20 @@ var MENTION_PATTERN = /(^|[^\w@])@([a-z0-9][a-z0-9-]{0,31})(?![\w-])/g;
2870
3018
  function withoutMentions(text) {
2871
3019
  return text.replaceAll(MENTION_PATTERN, "$1$2");
2872
3020
  }
3021
+ var SKILL_TEXT_LIMIT_CHARS = 1e5;
3022
+ function skillFolderOf(proposal2) {
3023
+ if (proposal2.kind !== "skill") {
3024
+ return null;
3025
+ }
3026
+ const parsed = SkillProposalSchema.safeParse(proposal2.charter);
3027
+ return parsed.success && parsed.data.body === void 0 ? parsed.data.name : null;
3028
+ }
3029
+ function proposedFrom(origin, where) {
3030
+ return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel;
3031
+ }
3032
+ function megabytes(bytes) {
3033
+ return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "");
3034
+ }
2873
3035
  var CURATING_ROLES = [USER_ROLE, "steward"];
2874
3036
  var SEED_INSTRUCTIONS_HEADING = "## Seed instructions";
2875
3037
  var OPS_WAKE_TRIGGER = "ops_event";
@@ -2960,8 +3122,8 @@ function restage(existing, edit) {
2960
3122
  };
2961
3123
  }
2962
3124
  function reopenStage(stage) {
2963
- const { completedBy: _by, completedAt: _at, ...open2 } = stage;
2964
- return open2;
3125
+ const { completedBy: _by, completedAt: _at, ...open3 } = stage;
3126
+ return open3;
2965
3127
  }
2966
3128
  function gateChanges(before, after) {
2967
3129
  const changes = [];
@@ -3356,7 +3518,7 @@ var Board = class _Board {
3356
3518
  const task2 = { ...TaskFrontmatterSchema.parse(raw.data), body: raw.body };
3357
3519
  if ((legacy === "open" || legacy === "closed") && !await exists(this.paths.threadFile(threads, task2.id))) {
3358
3520
  const ended = task2.status === "done" || task2.status === "abandoned";
3359
- const open2 = legacy === "open" && !ended;
3521
+ const open3 = legacy === "open" && !ended;
3360
3522
  const openedBy = opened.get(task2.id);
3361
3523
  const closedBy = closed.get(task2.id);
3362
3524
  await this.writeThread(threads, {
@@ -3364,10 +3526,10 @@ var Board = class _Board {
3364
3526
  channel: channelRef(slug, "general"),
3365
3527
  title: task2.title,
3366
3528
  subject: { kind: "task", id: task2.id },
3367
- state: open2 ? "open" : "closed",
3529
+ state: open3 ? "open" : "closed",
3368
3530
  openedBy: openedBy?.actor ?? task2.createdBy,
3369
3531
  openedAt: openedBy?.ts ?? task2.createdAt,
3370
- ...open2 ? {} : {
3532
+ ...open3 ? {} : {
3371
3533
  closedBy: closedBy?.actor ?? SYSTEM_ACTOR.name,
3372
3534
  closedAt: closedBy?.ts ?? this.now().toISOString()
3373
3535
  },
@@ -3430,7 +3592,7 @@ var Board = class _Board {
3430
3592
  for (const agent2 of await this.listAgents()) {
3431
3593
  const file = this.paths.agentCursors(agent2.name);
3432
3594
  if (await exists(file)) {
3433
- const legacy = Legacy.safeParse(JSON.parse(await readFile5(file, "utf8")));
3595
+ const legacy = Legacy.safeParse(JSON.parse(await readFile4(file, "utf8")));
3434
3596
  if (legacy.success) {
3435
3597
  await writeJson(file, { digest: legacy.data.inbox });
3436
3598
  }
@@ -3769,6 +3931,10 @@ ${charter.purpose}
3769
3931
  async latestEventId() {
3770
3932
  return this.events.lastId();
3771
3933
  }
3934
+ /** Calls `listener` after each event the board appends, until the returned function is called. */
3935
+ onEventAppended(listener) {
3936
+ return this.events.onAppend(listener);
3937
+ }
3772
3938
  /** The roster as projected: every citizen with identity, reach, availability, and profile. */
3773
3939
  async listMembers() {
3774
3940
  const members = [];
@@ -3792,22 +3958,41 @@ ${charter.purpose}
3792
3958
  const dir = project2 === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project2);
3793
3959
  const topics = [];
3794
3960
  for (const file of await listFiles(dir)) {
3795
- const doc = await readMarkdown(path6.join(dir, file), z13.record(z13.string(), z13.unknown()));
3796
- const parsed = KnowledgeSchema.safeParse({
3797
- ...doc.data,
3798
- project: project2,
3799
- topic: file.replace(/\.md$/, "")
3800
- });
3801
- const data = parsed.success ? parsed.data : {
3802
- topic: file.replace(/\.md$/, ""),
3803
- project: project2,
3804
- updatedBy: SYSTEM_ACTOR.name,
3805
- updatedAt: (await stat2(path6.join(dir, file))).mtime.toISOString()
3806
- };
3807
- topics.push({ ...data, body: doc.body });
3961
+ topics.push(await this.knowledgeAt(path6.join(dir, file), project2, file.replace(/\.md$/, "")));
3808
3962
  }
3809
3963
  return topics;
3810
3964
  }
3965
+ knowledgeFile(project2, topic) {
3966
+ return project2 === null ? this.paths.societyKnowledgeFile(topic) : this.paths.projectKnowledgeFile(project2, topic);
3967
+ }
3968
+ /**
3969
+ * Read loosely, as a dashboard is: a topic written before revisions counts as revision 0, and an
3970
+ * author or a time that does not parse is the board's at the file's mtime.
3971
+ */
3972
+ async knowledgeAt(file, project2, topic) {
3973
+ const { data, body } = await readLooseMarkdown(file);
3974
+ const revision = z13.number().int().min(0).safeParse(data["revision"]);
3975
+ const updatedBy = NameSchema.safeParse(data["updatedBy"]);
3976
+ const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
3977
+ return {
3978
+ topic,
3979
+ project: project2,
3980
+ revision: revision.success ? revision.data : 0,
3981
+ updatedBy: updatedBy.success ? updatedBy.data : SYSTEM_ACTOR.name,
3982
+ updatedAt: updatedAt.success ? updatedAt.data : (await stat3(file)).mtime.toISOString(),
3983
+ body
3984
+ };
3985
+ }
3986
+ /** The highest revision among a topic's removed copies, 0 when it has none. */
3987
+ async removedRevision(project2, topic) {
3988
+ const aside = this.paths.removedKnowledge(project2 ?? SOCIETY_SCOPE);
3989
+ let highest = 0;
3990
+ for (const file of (await listFiles(aside)).filter((name) => name.startsWith(`${topic}.`))) {
3991
+ const copy = await this.knowledgeAt(path6.join(aside, file), project2, topic);
3992
+ highest = Math.max(highest, copy.revision);
3993
+ }
3994
+ return highest;
3995
+ }
3811
3996
  /** Skills promoted to the society: every citizen may read them. */
3812
3997
  async listSocietySkills() {
3813
3998
  return this.listSkillsIn(this.paths.societySkills(), "society");
@@ -3896,6 +4081,28 @@ ${charter.purpose}
3896
4081
  async readThread(id) {
3897
4082
  return (await this.findThread(id)).thread;
3898
4083
  }
4084
+ /**
4085
+ * Where each of the messages named is and who wrote it, for text that names them by id; an id
4086
+ * that names no message is left out. Message files start with their ids, so only the files
4087
+ * asked for are read.
4088
+ */
4089
+ async findMessages(ids) {
4090
+ const wanted = new Set(ids);
4091
+ const found = [];
4092
+ for await (const dir of this.messageDirs()) {
4093
+ for (const file of await listFiles(dir)) {
4094
+ if (!wanted.delete(file.slice(0, 26))) {
4095
+ continue;
4096
+ }
4097
+ const { data } = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
4098
+ found.push(MessageRefSchema.parse(data));
4099
+ }
4100
+ if (wanted.size === 0) {
4101
+ break;
4102
+ }
4103
+ }
4104
+ return found;
4105
+ }
3899
4106
  /**
3900
4107
  * What citizens have asked the user and the user has not answered, oldest first: every message
3901
4108
  * by someone else that mentions the user, in an open thread or a channel the society or an
@@ -4397,12 +4604,12 @@ ${DASHBOARD_LINE}
4397
4604
  continue;
4398
4605
  }
4399
4606
  const named = thread2 === null ? channelConversation(filedIn, message.channel) : void 0;
4400
- let open2 = named === void 0 ? void 0 : openChannels.get(message.channel);
4401
- if (named !== void 0 && open2 === void 0) {
4402
- open2 = await this.channelOpen(message.channel);
4403
- openChannels.set(message.channel, open2);
4607
+ let open3 = named === void 0 ? void 0 : openChannels.get(message.channel);
4608
+ if (named !== void 0 && open3 === void 0) {
4609
+ open3 = await this.channelOpen(message.channel);
4610
+ openChannels.set(message.channel, open3);
4404
4611
  }
4405
- const part = thread2 ?? (open2 === true ? `#${named}` : null);
4612
+ const part = thread2 ?? (open3 === true ? `#${named}` : null);
4406
4613
  if (conversation !== void 0 && (filedIn !== conversation.scope || part !== conversation.part)) {
4407
4614
  continue;
4408
4615
  }
@@ -4467,7 +4674,7 @@ ${task2.body}`;
4467
4674
  }
4468
4675
  }
4469
4676
  for (const file of await listFiles(this.paths.projectKnowledge(project2))) {
4470
- const content = await readFile5(path6.join(this.paths.projectKnowledge(project2), file), "utf8");
4677
+ const content = await readFile4(path6.join(this.paths.projectKnowledge(project2), file), "utf8");
4471
4678
  if (content.toLowerCase().includes(needle)) {
4472
4679
  hits.push({
4473
4680
  kind: "knowledge",
@@ -4505,7 +4712,7 @@ ${task2.body}`;
4505
4712
  });
4506
4713
  }
4507
4714
  for (const entry of shared) {
4508
- const content = await readFile5(entry.file, "utf8");
4715
+ const content = await readFile4(entry.file, "utf8");
4509
4716
  if (content.toLowerCase().includes(needle)) {
4510
4717
  hits.push({
4511
4718
  kind: entry.kind,
@@ -4521,26 +4728,42 @@ ${task2.body}`;
4521
4728
  }
4522
4729
  /**
4523
4730
  * Writes a topic: a project's by its members, the society's by the curating roles. The note in
4524
- * the matching general channel carries no mention, so nobody is woken.
4731
+ * the matching general channel carries no mention, so nobody is woken. A turn reads a topic from
4732
+ * the copy of the projection pulled when it started, so a revision other than the current one, 0
4733
+ * for a topic not there, means someone wrote or removed it since; the refusal carries the current
4734
+ * text, which that copy lacks.
4525
4735
  */
4526
4736
  async writeKnowledge(actor, input) {
4527
4737
  const args = VerbInputs.write_knowledge.parse(input);
4528
4738
  await this.authorize(actor, "write_knowledge");
4529
4739
  return this.mutex.run(async () => {
4530
4740
  await this.assertMayCurate(actor, args.project);
4741
+ const file = this.knowledgeFile(args.project, args.topic);
4742
+ const current = await exists(file) ? await this.knowledgeAt(file, args.project, args.topic) : null;
4743
+ const place = args.project ?? "the society";
4744
+ if (current === null && args.revision !== 0) {
4745
+ throw new BoardError("INVALID_STATE", `${place} has no knowledge topic ${args.topic}, so it has no revision ${args.revision}: it was removed since you read it, or never written. Write it with revision 0 to start it anew.`);
4746
+ }
4747
+ if (current !== null && args.revision !== current.revision) {
4748
+ throw new BoardError("INVALID_STATE", `the knowledge topic ${args.topic} of ${place} is at revision ${current.revision}, updated by ${current.updatedBy} at ${current.updatedAt}, not ${args.revision}. Merge your changes into its current text, below, and write again with revision ${current.revision}.
4749
+
4750
+ ${current.body}`);
4751
+ }
4752
+ const base = current?.revision ?? await this.removedRevision(args.project, args.topic);
4531
4753
  const data = KnowledgeSchema.parse({
4532
4754
  topic: args.topic,
4533
4755
  project: args.project,
4756
+ revision: base + 1,
4534
4757
  updatedBy: actor.name,
4535
4758
  updatedAt: this.now().toISOString()
4536
4759
  });
4537
- const file = args.project === null ? this.paths.societyKnowledgeFile(args.topic) : this.paths.projectKnowledgeFile(args.project, args.topic);
4538
- const replaced = await exists(file);
4760
+ const replaced = current !== null;
4539
4761
  await writeMarkdown(file, data, args.body);
4540
4762
  await this.events.append("knowledge.written", actor.name, {
4541
4763
  topic: args.topic,
4542
4764
  project: args.project,
4543
- replaced
4765
+ replaced,
4766
+ revision: data.revision
4544
4767
  });
4545
4768
  const where = args.project === null ? "general" : channelRef(args.project, "general");
4546
4769
  await this.appendMessage(actor.name, where, `Knowledge ${replaced ? "updated" : "written"}: ${args.topic}. It is under knowledge/${args.topic}.md${args.project === null ? " of the society" : ""}.`, {
@@ -4563,7 +4786,7 @@ ${task2.body}`;
4563
4786
  await this.authorize(actor, "remove_knowledge");
4564
4787
  return this.mutex.run(async () => {
4565
4788
  await this.assertMayCurate(actor, args.project);
4566
- const file = args.project === null ? this.paths.societyKnowledgeFile(args.topic) : this.paths.projectKnowledgeFile(args.project, args.topic);
4789
+ const file = this.knowledgeFile(args.project, args.topic);
4567
4790
  if (!await exists(file)) {
4568
4791
  throw new BoardError("NOT_FOUND", `${args.project ?? "the society"} has no knowledge topic ${args.topic}`);
4569
4792
  }
@@ -5614,7 +5837,7 @@ ${args.summary}`);
5614
5837
  throw new BoardError("VALIDATION", `invalid ${args.kind} charter: ${parsed.error.message}`);
5615
5838
  }
5616
5839
  return this.mutex.run(async () => {
5617
- const proposal2 = {
5840
+ let proposal2 = {
5618
5841
  ...ProposalFrontmatterSchema.parse({
5619
5842
  id: this.newId(),
5620
5843
  kind: args.kind,
@@ -5633,6 +5856,14 @@ ${args.summary}`);
5633
5856
  body: args.rationale
5634
5857
  };
5635
5858
  await this.validateProvision(proposal2.kind, proposal2.charter);
5859
+ const skill2 = skillFolderOf(proposal2);
5860
+ if (skill2 !== null && proposal2.filedFrom === void 0) {
5861
+ const taken = await this.takeSkillFolderUnlocked(proposal2, skill2);
5862
+ if (typeof taken === "string") {
5863
+ throw new BoardError("VALIDATION", taken);
5864
+ }
5865
+ proposal2 = { ...proposal2, skillFiles: taken };
5866
+ }
5636
5867
  await this.writeProposal(proposal2);
5637
5868
  await this.events.append("proposal.created", actor.name, {
5638
5869
  proposalId: proposal2.id,
@@ -5970,7 +6201,7 @@ ${rationale}`}`);
5970
6201
  throw new BoardError("NOT_FOUND", `no transcript of turn ${turnId} for ${agent2}`);
5971
6202
  }
5972
6203
  const entries = [];
5973
- for (const line of (await readFile5(file, "utf8")).split("\n")) {
6204
+ for (const line of (await readFile4(file, "utf8")).split("\n")) {
5974
6205
  if (line.trim() === "")
5975
6206
  continue;
5976
6207
  const parsed = TranscriptEntrySchema.safeParse(JSON.parse(line));
@@ -6449,7 +6680,7 @@ ${rationale}`}`);
6449
6680
  }
6450
6681
  const file = entry.isFile() ? conflictCopyOf(relative) : null;
6451
6682
  if (file !== null) {
6452
- const since = await this.homes.changedAt(home, relative) ?? (await stat2(path6.join(home, relative)).catch(() => null))?.mtime.toISOString();
6683
+ const since = await this.homes.changedAt(home, relative) ?? (await stat3(path6.join(home, relative)).catch(() => null))?.mtime.toISOString();
6453
6684
  if (since !== void 0) {
6454
6685
  found.push({ path: relative, file, since });
6455
6686
  }
@@ -6849,7 +7080,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
6849
7080
  }
6850
7081
  case "skill": {
6851
7082
  const skill2 = SkillProposalSchema.parse(proposal2.charter);
6852
- const replaced = await this.promoteSkillUnlocked(by, proposal2.proposedBy, skill2, meta);
7083
+ const replaced = await this.promoteSkillUnlocked(by, proposal2, skill2, meta);
6853
7084
  return { skill: skill2.name, replaced };
6854
7085
  }
6855
7086
  case "archive": {
@@ -6863,26 +7094,180 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
6863
7094
  return void 0;
6864
7095
  }
6865
7096
  }
6866
- /** Writes a promoted skill under the society's skills, where every citizen's skills index lists it. */
6867
- async promoteSkillUnlocked(by, proposedBy, skill2, meta) {
7097
+ /**
7098
+ * Publishes a promoted skill under the society's skills, where every citizen's skills index lists
7099
+ * it: the folder the board took from its proposer's home, or the text proposed alone. A
7100
+ * replacement is the whole skill, so files the new version dropped go with the old one.
7101
+ */
7102
+ async promoteSkillUnlocked(by, proposal2, skill2, meta) {
7103
+ const dir = this.paths.societySkill(skill2.name);
6868
7104
  const file = this.paths.societySkillFile(skill2.name);
6869
7105
  const replaced = await exists(file);
6870
- await ensureDir(this.paths.societySkill(skill2.name));
7106
+ const taken = proposal2.skillFiles;
7107
+ const staged = path6.join(this.paths.proposalFiles(proposal2.id), "SKILL.md");
7108
+ if (skill2.body === void 0 && (taken === void 0 || !await exists(staged))) {
7109
+ throw new BoardError("INVALID_STATE", `proposal ${proposal2.id} holds no skill files`);
7110
+ }
7111
+ await rm2(dir, { recursive: true, force: true });
7112
+ if (skill2.body === void 0) {
7113
+ await cp(this.paths.proposalFiles(proposal2.id), dir, { recursive: true });
7114
+ } else {
7115
+ await ensureDir(dir);
7116
+ }
7117
+ const body = skill2.body ?? (await readLooseMarkdown(file)).body;
6871
7118
  await writeMarkdown(file, {
6872
7119
  name: skill2.name,
6873
7120
  description: skill2.summary,
6874
- metadata: { proposedBy, promotedBy: by, promotedAt: this.now().toISOString() }
6875
- }, skill2.body);
7121
+ metadata: {
7122
+ proposedBy: proposal2.proposedBy,
7123
+ promotedBy: by,
7124
+ promotedAt: this.now().toISOString(),
7125
+ ...taken === void 0 ? {} : { commit: taken.commit }
7126
+ }
7127
+ }, body);
6876
7128
  await this.events.append("skill.promoted", by, {
6877
7129
  name: skill2.name,
6878
7130
  summary: skill2.summary,
6879
- proposedBy,
7131
+ proposedBy: proposal2.proposedBy,
6880
7132
  replaced,
7133
+ ...taken === void 0 ? {} : { files: taken.files.length },
6881
7134
  ...meta
6882
7135
  });
6883
7136
  await this.appendMessage(SYSTEM_ACTOR.name, "general", `Skill ${skill2.name} ${replaced ? "updated in" : "promoted to"} the society: ${sentence(skill2.summary)}. Every citizen's skills index now lists it.`);
6884
7137
  return replaced;
6885
7138
  }
7139
+ /**
7140
+ * Copies a skill's folder from its proposer's home, as the home's newest commit holds it, into the
7141
+ * proposal's folder in the projection, where its deciders read it and from where approval
7142
+ * publishes it. Returns what was taken, or why the home could not give it.
7143
+ */
7144
+ async takeSkillFolderUnlocked(proposal2, name) {
7145
+ const home = this.paths.agent(proposal2.proposedBy);
7146
+ const folder = `skills/${name}`;
7147
+ const where = `${folder} in ${proposal2.proposedBy}'s home`;
7148
+ const taken = await this.homes.folder(home, folder);
7149
+ if (taken === null || !taken.files.some((entry) => entry.path === "SKILL.md")) {
7150
+ return `no ${folder}/SKILL.md in ${proposal2.proposedBy}'s home`;
7151
+ }
7152
+ if (taken.files.length > SKILL_FILES_LIMIT) {
7153
+ return `${where} holds ${taken.files.length} files, more than the ${SKILL_FILES_LIMIT} a society skill may`;
7154
+ }
7155
+ const bytes = taken.files.reduce((sum, entry) => sum + entry.size, 0);
7156
+ if (bytes > SKILL_BYTES_LIMIT) {
7157
+ return `${where} holds ${megabytes(bytes)} MB, more than the ${megabytes(SKILL_BYTES_LIMIT)} MB a society skill may`;
7158
+ }
7159
+ const contents = /* @__PURE__ */ new Map();
7160
+ for (const entry of taken.files) {
7161
+ contents.set(entry.path, await this.homes.blob(home, entry.object));
7162
+ }
7163
+ const manual = parseLooseMarkdown(contents.get("SKILL.md")?.toString("utf8") ?? "");
7164
+ if (manual.error !== null) {
7165
+ return `the frontmatter of ${folder}/SKILL.md in ${proposal2.proposedBy}'s home does not parse (${manual.error})`;
7166
+ }
7167
+ const dir = this.paths.proposalFiles(proposal2.id);
7168
+ await rm2(dir, { recursive: true, force: true });
7169
+ for (const entry of taken.files) {
7170
+ const target = path6.join(dir, ...entry.path.split("/"));
7171
+ await ensureDir(path6.dirname(target));
7172
+ await writeFile4(target, contents.get(entry.path) ?? Buffer.alloc(0));
7173
+ await chmod2(target, entry.executable ? 493 : 420);
7174
+ }
7175
+ return {
7176
+ commit: taken.commit,
7177
+ takenAt: this.now().toISOString(),
7178
+ files: taken.files.map((entry) => ({
7179
+ path: entry.path,
7180
+ size: entry.size,
7181
+ executable: entry.executable
7182
+ }))
7183
+ };
7184
+ }
7185
+ /** Keeps what was taken of a skill proposal's folder on the proposal, and logs it. */
7186
+ async recordSkillFiles(proposal2, taken) {
7187
+ const next = { ...proposal2, skillFiles: taken };
7188
+ await this.writeProposal(next);
7189
+ await this.events.append("proposal.files_taken", SYSTEM_ACTOR.name, {
7190
+ proposalId: proposal2.id,
7191
+ kind: proposal2.kind,
7192
+ proposedBy: proposal2.proposedBy,
7193
+ commit: taken.commit,
7194
+ files: taken.files.length
7195
+ });
7196
+ return next;
7197
+ }
7198
+ /**
7199
+ * A skill proposal with its folder, for approval: taken now when the turn that filed it ended
7200
+ * without it, as when the server restarted before the turn's end was recorded, and refused while
7201
+ * that turn is still in flight, since the folder reaches the board only when it ends.
7202
+ */
7203
+ async skillFilesForApproval(proposal2) {
7204
+ const name = skillFolderOf(proposal2);
7205
+ if (name === null || proposal2.skillFiles !== void 0) {
7206
+ return proposal2;
7207
+ }
7208
+ const cannot = `proposal ${proposal2.id} cannot be approved`;
7209
+ const filing = this.turnsInFlight().some((turn2) => turn2.agent === proposal2.proposedBy && proposedFrom(proposal2.filedFrom, turn2));
7210
+ if (filing) {
7211
+ throw new BoardError("INVALID_STATE", `${cannot} until ${proposal2.proposedBy}'s turn that proposed it ends, when the board takes the skill's folder from its home`);
7212
+ }
7213
+ const taken = await this.takeSkillFolderUnlocked(proposal2, name);
7214
+ if (typeof taken === "string") {
7215
+ throw new BoardError("INVALID_STATE", `${cannot}: ${taken}; reject it with that reason`);
7216
+ }
7217
+ return this.recordSkillFiles(proposal2, taken);
7218
+ }
7219
+ /**
7220
+ * Takes the folders of the skills a citizen proposed from a conversation, once the turn there has
7221
+ * ended and its runner has pushed the citizen's home. A proposal whose folder the home does not
7222
+ * give is rejected by the board with the reason, which wakes the proposer to put it right; while
7223
+ * someone is in a turn in the proposal's thread, it stays as it is, and approval meets the reason.
7224
+ */
7225
+ async takeSkillFiles(turn2) {
7226
+ const where = {
7227
+ scope: turn2.project,
7228
+ ...turn2.thread === void 0 ? {} : { thread: turn2.thread },
7229
+ ...turn2.channel === void 0 ? {} : { channel: turn2.channel }
7230
+ };
7231
+ await this.mutex.run(async () => {
7232
+ for (const proposal2 of await this.listProposals()) {
7233
+ const name = skillFolderOf(proposal2);
7234
+ if (name === null || proposal2.status !== "proposed" || proposal2.skillFiles !== void 0 || proposal2.proposedBy !== turn2.agent || !proposedFrom(proposal2.filedFrom, where)) {
7235
+ continue;
7236
+ }
7237
+ const taken = await this.takeSkillFolderUnlocked(proposal2, name);
7238
+ if (typeof taken !== "string") {
7239
+ await this.recordSkillFiles(proposal2, taken);
7240
+ continue;
7241
+ }
7242
+ try {
7243
+ await this.decideUnlocked(SYSTEM_ACTOR, proposal2.id, "rejected", `${taken} when the turn that proposed it ended; propose it again once the folder is there`);
7244
+ } catch (error) {
7245
+ if (!(error instanceof BoardError)) {
7246
+ throw error;
7247
+ }
7248
+ }
7249
+ }
7250
+ });
7251
+ }
7252
+ /** A skill proposal's files with their text, as the board took them; none before then. */
7253
+ async readSkillFiles(proposalId) {
7254
+ const proposal2 = await this.readProposal(proposalId);
7255
+ const dir = this.paths.proposalFiles(proposal2.id);
7256
+ const files = [];
7257
+ for (const entry of proposal2.skillFiles?.files ?? []) {
7258
+ if (!isSafeRelativePath(entry.path)) {
7259
+ continue;
7260
+ }
7261
+ const bytes = await readFile4(path6.join(dir, ...entry.path.split("/"))).catch(() => null);
7262
+ const text = bytes === null || bytes.includes(0) ? null : bytes.toString("utf8");
7263
+ files.push({
7264
+ ...entry,
7265
+ text: text === null ? null : text.slice(0, SKILL_TEXT_LIMIT_CHARS),
7266
+ truncated: text !== null && text.length > SKILL_TEXT_LIMIT_CHARS
7267
+ });
7268
+ }
7269
+ return files;
7270
+ }
6886
7271
  async validateAddAgent(input) {
6887
7272
  if (await exists(this.paths.agentFile(input.name))) {
6888
7273
  throw new BoardError("ALREADY_EXISTS", `agent ${input.name} already exists`);
@@ -7394,67 +7779,70 @@ ${charter.purpose}
7394
7779
  * retired. Validation runs before any write, so an impossible provision fails the decision.
7395
7780
  */
7396
7781
  async decide(actor, proposalId, outcome, reason) {
7397
- return this.mutex.run(async () => {
7398
- const proposal2 = await this.readProposal(proposalId);
7399
- if (proposal2.status !== "proposed") {
7400
- throw new BoardError("INVALID_STATE", `proposal ${proposalId} is already ${proposal2.status}`);
7401
- }
7402
- if (proposal2.proposedBy === actor.name) {
7403
- throw new BoardError("FORBIDDEN", "a proposer never decides its own proposal");
7404
- }
7405
- const approvers = ROLE_KIND_APPROVERS[proposal2.kind];
7406
- if (!approvers.includes(actor.role)) {
7407
- throw new BoardError("FORBIDDEN", `a ${proposal2.kind} proposal requires one of: ${approvers.join(", ")}`);
7408
- }
7409
- if (outcome === "approved") {
7410
- await this.validateProvision(proposal2.kind, proposal2.charter);
7411
- }
7412
- this.assertNoTurnCut(actor, `proposal ${proposalId} cannot be decided`, (turn2) => turn2.thread === proposalId);
7413
- if (outcome === "approved" && proposal2.kind === "archive") {
7414
- await this.assertProjectQuiet(actor, ArchiveProposalSchema.parse(proposal2.charter).project);
7415
- }
7416
- const ts = this.now().toISOString();
7417
- const decision = DecisionSchema.parse({
7418
- id: this.newId(),
7419
- proposalId,
7420
- decidedBy: actor.name,
7421
- outcome,
7422
- ...reason === void 0 ? {} : { reason },
7423
- ts
7424
- });
7425
- await writeMarkdown(this.paths.decision(decision.id), decision, reason ?? "");
7426
- const provision = outcome === "approved" ? await this.executeProvision(actor.name, proposal2) : void 0;
7427
- const status = outcome === "rejected" ? "rejected" : provision === void 0 ? "approved" : "provisioned";
7428
- await this.writeProposal({
7429
- ...proposal2,
7430
- status,
7431
- decidedBy: actor.name,
7432
- decidedAt: ts,
7433
- ...reason === void 0 ? {} : { reason },
7434
- ...provision === void 0 ? {} : { provisionedAt: ts, provision }
7435
- });
7436
- await this.events.append("proposal.decided", actor.name, {
7782
+ return this.mutex.run(() => this.decideUnlocked(actor, proposalId, outcome, reason));
7783
+ }
7784
+ async decideUnlocked(actor, proposalId, outcome, reason) {
7785
+ const filed = await this.readProposal(proposalId);
7786
+ if (filed.status !== "proposed") {
7787
+ throw new BoardError("INVALID_STATE", `proposal ${proposalId} is already ${filed.status}`);
7788
+ }
7789
+ if (filed.proposedBy === actor.name) {
7790
+ throw new BoardError("FORBIDDEN", "a proposer never decides its own proposal");
7791
+ }
7792
+ const approvers = ROLE_KIND_APPROVERS[filed.kind];
7793
+ if (!approvers.includes(actor.role)) {
7794
+ throw new BoardError("FORBIDDEN", `a ${filed.kind} proposal requires one of: ${approvers.join(", ")}`);
7795
+ }
7796
+ if (outcome === "approved") {
7797
+ await this.validateProvision(filed.kind, filed.charter);
7798
+ }
7799
+ this.assertNoTurnCut(actor, `proposal ${proposalId} cannot be decided`, (turn2) => turn2.thread === proposalId);
7800
+ if (outcome === "approved" && filed.kind === "archive") {
7801
+ await this.assertProjectQuiet(actor, ArchiveProposalSchema.parse(filed.charter).project);
7802
+ }
7803
+ const proposal2 = outcome === "approved" ? await this.skillFilesForApproval(filed) : filed;
7804
+ const ts = this.now().toISOString();
7805
+ const decision = DecisionSchema.parse({
7806
+ id: this.newId(),
7807
+ proposalId,
7808
+ decidedBy: actor.name,
7809
+ outcome,
7810
+ ...reason === void 0 ? {} : { reason },
7811
+ ts
7812
+ });
7813
+ await writeMarkdown(this.paths.decision(decision.id), decision, reason ?? "");
7814
+ const provision = outcome === "approved" ? await this.executeProvision(actor.name, proposal2) : void 0;
7815
+ const status = outcome === "rejected" ? "rejected" : provision === void 0 ? "approved" : "provisioned";
7816
+ await this.writeProposal({
7817
+ ...proposal2,
7818
+ status,
7819
+ decidedBy: actor.name,
7820
+ decidedAt: ts,
7821
+ ...reason === void 0 ? {} : { reason },
7822
+ ...provision === void 0 ? {} : { provisionedAt: ts, provision }
7823
+ });
7824
+ await this.events.append("proposal.decided", actor.name, {
7825
+ proposalId,
7826
+ outcome,
7827
+ kind: proposal2.kind,
7828
+ proposedBy: proposal2.proposedBy,
7829
+ ...proposal2.filedFrom === void 0 ? {} : { filedFrom: proposal2.filedFrom }
7830
+ });
7831
+ if (provision !== void 0) {
7832
+ await this.events.append("proposal.provisioned", actor.name, {
7437
7833
  proposalId,
7438
- outcome,
7439
7834
  kind: proposal2.kind,
7440
- proposedBy: proposal2.proposedBy,
7441
- ...proposal2.filedFrom === void 0 ? {} : { filedFrom: proposal2.filedFrom }
7835
+ ...provision
7442
7836
  });
7443
- if (provision !== void 0) {
7444
- await this.events.append("proposal.provisioned", actor.name, {
7445
- proposalId,
7446
- kind: proposal2.kind,
7447
- ...provision
7448
- });
7449
- }
7450
- const verdict = outcome === "approved" ? "Approved" : "Rejected";
7451
- const why = reason === void 0 ? "" : ` Reason: ${reason}.`;
7452
- const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
7453
- const thread2 = await this.proposalThreadUnlocked(actor.name, proposal2);
7454
- await this.appendThreadMessage(actor.name, thread2.id, `${verdict}: ${describeCharter(proposal2.kind, proposal2.charter)}.${why}${result}`, { step: { action: outcome } });
7455
- await this.endThreadOf(proposalId, actor.name, outcome);
7456
- return decision;
7457
- });
7837
+ }
7838
+ const verdict = outcome === "approved" ? "Approved" : "Rejected";
7839
+ const why = reason === void 0 ? "" : ` Reason: ${reason}.`;
7840
+ const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
7841
+ const thread2 = await this.proposalThreadUnlocked(actor.name, proposal2);
7842
+ const post = `${verdict}: ${describeCharter(proposal2.kind, proposal2.charter)}.${why}${result}`;
7843
+ await this.appendThreadMessage(actor.name, thread2.id, actor.name === SYSTEM_ACTOR.name ? withoutMentions(post) : post, { step: { action: outcome } });
7844
+ await this.endThreadOf(proposalId, actor.name, outcome);
7845
+ return decision;
7458
7846
  }
7459
7847
  /** The opener, the people of its subject, and anyone who posted in it; mentions reach the rest. */
7460
7848
  async participatesInThread(actor, thread2, threads) {
@@ -7569,6 +7957,23 @@ ${charter.purpose}
7569
7957
  yield* this.threadMessagesIn(this.paths.threads(slug), since);
7570
7958
  }
7571
7959
  }
7960
+ /** Every folder that holds messages: each channel's and each thread's, in the society and every project. */
7961
+ async *messageDirs() {
7962
+ for (const channel of await listDirs(this.paths.societyChannels())) {
7963
+ yield this.paths.societyChannel(channel);
7964
+ }
7965
+ for (const id of await listDirs(this.paths.societyThreads())) {
7966
+ yield this.paths.threadMessages(this.paths.societyThreads(), id);
7967
+ }
7968
+ for (const slug of await listDirs(this.paths.projects())) {
7969
+ for (const channel of await listDirs(this.paths.projectChannels(slug))) {
7970
+ yield this.paths.projectChannel(slug, channel);
7971
+ }
7972
+ for (const id of await listDirs(this.paths.threads(slug))) {
7973
+ yield this.paths.threadMessages(this.paths.threads(slug), id);
7974
+ }
7975
+ }
7976
+ }
7572
7977
  async *threadMessagesIn(threads, since) {
7573
7978
  for (const id of await listDirs(threads)) {
7574
7979
  yield* await this.readMessagesIn(this.paths.threadMessages(threads, id), since);
@@ -7585,7 +7990,7 @@ function globals() {
7585
7990
  function print(value, human) {
7586
7991
  console.log(globals().json ? JSON.stringify(value, null, 2) : human());
7587
7992
  }
7588
- async function open() {
7993
+ async function open2() {
7589
7994
  return Board.open(globals().data);
7590
7995
  }
7591
7996
  async function actorFor(board, as) {
@@ -7631,7 +8036,7 @@ user.command("token").description("Issue the user a new token and print it once;
7631
8036
  if (!opts.rotate) {
7632
8037
  throw new BoardError("VALIDATION", "the board keeps only a hash of the user token, so it cannot show it again; `user token --rotate` issues a new one");
7633
8038
  }
7634
- const board = await open();
8039
+ const board = await open2();
7635
8040
  const userToken = await board.rotateUserToken();
7636
8041
  print({ userToken }, () => [
7637
8042
  `User token (shown once): ${userToken}`,
@@ -7640,7 +8045,7 @@ user.command("token").description("Issue the user a new token and print it once;
7640
8045
  });
7641
8046
  var project = program.command("project").description("Manage projects");
7642
8047
  project.command("add <slug>").description("Add a project with its default channels").option("--name <name>", "display name").option("--repo <url>", "git remote").option("--on-done <effect>", "what finishing a task does: none, or merge its branch").action(async (slug, opts) => {
7643
- const board = await open();
8048
+ const board = await open2();
7644
8049
  const added = await board.addProject(board.userActor(), {
7645
8050
  slug,
7646
8051
  name: opts.name,
@@ -7650,7 +8055,7 @@ project.command("add <slug>").description("Add a project with its default channe
7650
8055
  print(added, () => `Project ${added.slug} added with channels ${added.channels.join(", ")}`);
7651
8056
  });
7652
8057
  project.command("configure <slug>").description("Set a project's completion effect").requiredOption("--on-done <effect>", "none, or merge").option("--as <agent>", "act as this agent instead of the user").action(async (slug, opts) => {
7653
- const board = await open();
8058
+ const board = await open2();
7654
8059
  const configured = await board.configureProject(await actorFor(board, opts.as), {
7655
8060
  project: slug,
7656
8061
  on_done: CompletionEffectSchema.parse(opts.onDone)
@@ -7658,7 +8063,7 @@ project.command("configure <slug>").description("Set a project's completion effe
7658
8063
  print(configured, () => `Project ${configured.slug}: on done ${configured.onDone}`);
7659
8064
  });
7660
8065
  project.command("archive <slug>").description("Archive a project with no task in play: its members leave and its threads close").requiredOption("--reason <text>", "why the project is archived").action(async (slug, opts) => {
7661
- const board = await open();
8066
+ const board = await open2();
7662
8067
  const archived = await board.archiveProject(board.userActor(), {
7663
8068
  project: slug,
7664
8069
  reason: opts.reason
@@ -7666,13 +8071,13 @@ project.command("archive <slug>").description("Archive a project with no task in
7666
8071
  print(archived, () => `Project ${archived.slug} archived`);
7667
8072
  });
7668
8073
  project.command("list").description("List projects").action(async () => {
7669
- const board = await open();
8074
+ const board = await open2();
7670
8075
  const projects = await board.listProjects();
7671
8076
  print(projects, () => projects.map((p) => p.archived === void 0 ? `${p.slug} ${p.name} members: ${p.members.join(", ") || "none"}` : `${p.slug} ${p.name} archived ${p.archived.at} by ${p.archived.by}`).join("\n"));
7672
8077
  });
7673
8078
  var runner = program.command("runner").description("Manage runners");
7674
8079
  runner.command("add <name>").description("Register a runner and print its token once").action(async (name) => {
7675
- const board = await open();
8080
+ const board = await open2();
7676
8081
  const { runner: added, token } = await board.addRunner(board.userActor(), name);
7677
8082
  print({ runner: added, token }, () => [
7678
8083
  `Runner ${added.name} added`,
@@ -7681,13 +8086,13 @@ runner.command("add <name>").description("Register a runner and print its token
7681
8086
  ].join("\n"));
7682
8087
  });
7683
8088
  runner.command("list").description("List runners and what they offer").action(async () => {
7684
- const board = await open();
8089
+ const board = await open2();
7685
8090
  const runners = await board.listRunners();
7686
8091
  print(runners, () => runners.length === 0 ? "No runners yet; add one with `runner add <name>`." : runners.map((each) => `${each.name} ${each.status} ${each.os}${each.version === void 0 ? "" : ` v${each.version}`} ${each.clis.join(",") || "no CLIs yet"}${each.capabilities.length === 0 ? "" : ` ${each.capabilities.join(",")}`}`).join("\n"));
7687
8092
  });
7688
8093
  var agent = program.command("agent").description("Manage agents");
7689
8094
  agent.command("add <name>").description("Add an agent and print its board token once").requiredOption("--role <role>", "role charter name").option("--cli <cli>", "claude or codex").option("--model <model>", "model to run the CLI with; the CLI's own default otherwise").option("--runner <name>", "runner preferred for its turns; any with its CLI otherwise").option("-p, --project <slug...>", "project memberships").action(async (name, opts) => {
7690
- const board = await open();
8095
+ const board = await open2();
7691
8096
  const cli = opts.cli === void 0 ? null : CliKindSchema.parse(opts.cli);
7692
8097
  const { agent: added, token } = await board.addAgent(board.userActor(), {
7693
8098
  name,
@@ -7700,7 +8105,7 @@ agent.command("add <name>").description("Add an agent and print its board token
7700
8105
  print({ agent: added, token }, () => [`Agent ${added.name} added as ${added.role}`, `Token (shown once): ${token}`].join("\n"));
7701
8106
  });
7702
8107
  agent.command("runner <name> [runner]").description("Show or set the runner a citizen's work outside any project runs on; `none` pins it again on its next turn").action(async (name, chosen) => {
7703
- const board = await open();
8108
+ const board = await open2();
7704
8109
  if (chosen === void 0) {
7705
8110
  const current = await board.readAgent(name);
7706
8111
  print({ agent: name, homeRunner: current.homeRunner ?? null }, () => current.homeRunner === void 0 ? `${name} is not pinned; its next turn outside a project pins it` : `${name} runs on ${current.homeRunner}`);
@@ -7710,7 +8115,7 @@ agent.command("runner <name> [runner]").description("Show or set the runner a ci
7710
8115
  print({ agent: name, homeRunner: updated.homeRunner ?? null }, () => updated.homeRunner === void 0 ? `${name} will be pinned on its next turn` : `${name} runs on ${updated.homeRunner} from its next turn`);
7711
8116
  });
7712
8117
  agent.command("list").description("List agents").action(async () => {
7713
- const board = await open();
8118
+ const board = await open2();
7714
8119
  const agents = await board.listAgents();
7715
8120
  const members = await board.listMembers();
7716
8121
  print(agents, () => agents.map((a) => {
@@ -7719,28 +8124,28 @@ agent.command("list").description("List agents").action(async () => {
7719
8124
  }).join("\n"));
7720
8125
  });
7721
8126
  agent.command("model <name> [model]").description("Set the model a member's turns run with, from its next turn; no model means the CLI's default").action(async (name, model) => {
7722
- const board = await open();
8127
+ const board = await open2();
7723
8128
  const updated = await board.setAgentModel(board.userActor(), name, model ?? null);
7724
8129
  print(updated, () => `${updated.name} runs with ${updated.model ?? "its CLI's default model"} from its next turn`);
7725
8130
  });
7726
8131
  agent.command("effort <name> [effort]").description("Set the reasoning effort a member's turns run with, from its next turn, such as high; no effort means the model's default").action(async (name, effort) => {
7727
- const board = await open();
8132
+ const board = await open2();
7728
8133
  const updated = await board.setAgentEffort(board.userActor(), name, effort ?? null);
7729
8134
  print(updated, () => `${updated.name} runs with ${updated.effort === void 0 ? "its model's default effort" : `${updated.effort} effort`} from its next turn`);
7730
8135
  });
7731
8136
  agent.command("retire <name>").description("Retire a member: no more wakes, claims released, token revoked, sessions archived").requiredOption("--reason <text>", "why").action(async (name, opts) => {
7732
- const board = await open();
8137
+ const board = await open2();
7733
8138
  const retired = await board.retireAgent(board.userActor(), { name, reason: opts.reason });
7734
8139
  print(retired, () => `Agent ${retired.name} retired: ${opts.reason}`);
7735
8140
  });
7736
8141
  var proposal = program.command("proposal").description("Governance proposals");
7737
8142
  proposal.command("list").description("List proposals").action(async () => {
7738
- const board = await open();
8143
+ const board = await open2();
7739
8144
  const proposals = await board.listProposals();
7740
8145
  print(proposals, () => proposals.length === 0 ? "No proposals." : proposals.map((p) => `${p.id} ${p.kind} ${p.status} by ${p.proposedBy}`).join("\n"));
7741
8146
  });
7742
8147
  proposal.command("show <id>").description("Show a proposal with its charter and rationale").action(async (id) => {
7743
- const board = await open();
8148
+ const board = await open2();
7744
8149
  const found = await board.readProposal(id);
7745
8150
  print(found, () => [
7746
8151
  `${found.id} ${found.kind} ${found.status} by ${found.proposedBy}`,
@@ -7750,7 +8155,7 @@ proposal.command("show <id>").description("Show a proposal with its charter and
7750
8155
  ].filter((line) => line.length > 0).join("\n"));
7751
8156
  });
7752
8157
  proposal.command("create").description("Propose a member, role, channel, reallocation, or retirement").requiredOption("--kind <kind>", "member, role, channel, reallocation, or retirement").requiredOption("--charter <json>", "the charter as a JSON object").option("--rationale <text>", "why", "").option("--as <agent>", "act as this agent instead of the user").action(async (opts) => {
7753
- const board = await open();
8158
+ const board = await open2();
7754
8159
  const created = await board.propose(await actorFor(board, opts.as), {
7755
8160
  kind: ProposalKindSchema.parse(opts.kind),
7756
8161
  charter: parseCharter(opts.charter),
@@ -7759,7 +8164,7 @@ proposal.command("create").description("Propose a member, role, channel, realloc
7759
8164
  print(created, () => `Proposal ${created.id} (${created.kind}) is ${created.status}`);
7760
8165
  });
7761
8166
  proposal.command("approve <id>").description("Approve a proposal; the board provisions it").option("--reason <text>", "a note for the record").option("--as <agent>", "act as this agent instead of the user").action(async (id, opts) => {
7762
- const board = await open();
8167
+ const board = await open2();
7763
8168
  const decision = await board.approve(await actorFor(board, opts.as), {
7764
8169
  proposal_id: id,
7765
8170
  ...opts.reason === void 0 ? {} : { reason: opts.reason }
@@ -7768,7 +8173,7 @@ proposal.command("approve <id>").description("Approve a proposal; the board prov
7768
8173
  print({ decision, proposal: after }, () => `Proposal ${id} approved; it is now ${after.status}`);
7769
8174
  });
7770
8175
  proposal.command("reject <id>").description("Reject a proposal with a reason").requiredOption("--reason <text>", "why").option("--as <agent>", "act as this agent instead of the user").action(async (id, opts) => {
7771
- const board = await open();
8176
+ const board = await open2();
7772
8177
  const decision = await board.reject(await actorFor(board, opts.as), {
7773
8178
  proposal_id: id,
7774
8179
  reason: opts.reason
@@ -7777,14 +8182,14 @@ proposal.command("reject <id>").description("Reject a proposal with a reason").r
7777
8182
  });
7778
8183
  var role = program.command("role").description("Role charters");
7779
8184
  role.command("list").description("List role charters").action(async () => {
7780
- const board = await open();
8185
+ const board = await open2();
7781
8186
  const roles = await board.listRoles();
7782
8187
  print(roles, () => roles.map((r) => `${r.name} replicas up to ${r.maxReplicas} backlog threshold ${r.backlogThreshold} wakes on ${r.wakeTriggers.join(", ") || "direct wakes only"}`).join("\n"));
7783
8188
  });
7784
8189
  role.command("add <name>").description("Write a new role charter directly as the user").requiredOption("--purpose <text>", "what the role is for; its members read this every turn").option("--verbs <verbs...>", "board verbs granted; the member verbs by default").option("--triggers <triggers...>", "wakes it opts into: user_post, ops_event, heartbeat", [
7785
8190
  "heartbeat"
7786
8191
  ]).action(async (name, opts) => {
7787
- const board = await open();
8192
+ const board = await open2();
7788
8193
  const added = await board.setRoleCharter(board.userActor(), {
7789
8194
  name,
7790
8195
  purpose: opts.purpose,
@@ -7794,7 +8199,7 @@ role.command("add <name>").description("Write a new role charter directly as the
7794
8199
  print(added, () => `Role ${added.name} written with ${added.verbs.length} verbs`);
7795
8200
  });
7796
8201
  role.command("set <name>").description("Change a charter directly as the user: scaling cap, threshold, or purpose").option("--max-replicas <n>", "active members of the role per project the scheduler may reach").option("--backlog-threshold <n>", "load per member that adds a replica").option("--purpose <text>", "the charter's purpose").action(async (name, opts) => {
7797
- const board = await open();
8202
+ const board = await open2();
7798
8203
  const current = await board.readRole(name);
7799
8204
  const updated = await board.setRoleCharter(board.userActor(), {
7800
8205
  ...current,
@@ -7806,7 +8211,7 @@ role.command("set <name>").description("Change a charter directly as the user: s
7806
8211
  });
7807
8212
  var channelCommand = program.command("channel").description("Channels");
7808
8213
  channelCommand.command("add <ref>").description("Open a channel: <name> for the society or <project>/<name>").requiredOption("--purpose <text>", "what the channel is for").option("--as <agent>", "act as this agent instead of the user").action(async (ref, opts) => {
7809
- const board = await open();
8214
+ const board = await open2();
7810
8215
  const parsed = parseChannelRef(ref);
7811
8216
  const added = await board.addChannel(await actorFor(board, opts.as), {
7812
8217
  project: parsed.project,
@@ -7816,7 +8221,7 @@ channelCommand.command("add <ref>").description("Open a channel: <name> for the
7816
8221
  print({ channel: added }, () => `Channel ${added} opened`);
7817
8222
  });
7818
8223
  channelCommand.command("archive <ref>").description("Archive a channel whose workstream is done, once no task filed there is in play").requiredOption("--reason <text>", "why its work is done").option("--as <agent>", "act as this agent instead of the user").action(async (ref, opts) => {
7819
- const board = await open();
8224
+ const board = await open2();
7820
8225
  const archived = await board.archiveChannel(await actorFor(board, opts.as), {
7821
8226
  channel: ref,
7822
8227
  reason: opts.reason
@@ -7824,12 +8229,12 @@ channelCommand.command("archive <ref>").description("Archive a channel whose wor
7824
8229
  print(archived, () => `Channel ${ref} archived: ${archived.reason}`);
7825
8230
  });
7826
8231
  program.command("signals").description("Recent operations signals, oldest first").option("--limit <n>", "how many", "50").action(async (opts) => {
7827
- const board = await open();
8232
+ const board = await open2();
7828
8233
  const signals = await board.listSignals(Number(opts.limit));
7829
8234
  print(signals, () => signals.length === 0 ? "No signals yet." : signals.map((s) => `[${s.ts}] ${s.signal.kind} ${s.signal.summary}`).join("\n"));
7830
8235
  });
7831
8236
  program.command("post <channel> <body>").description("Post a message to a channel, or into one of its threads").option("--as <agent>", "act as this agent instead of the user").option("--thread <threadId>", "post into the thread, which must hang off the channel").action(async (channel, body, opts) => {
7832
- const board = await open();
8237
+ const board = await open2();
7833
8238
  const who = await actorFor(board, opts.as);
7834
8239
  const message = await board.postMessage(who, {
7835
8240
  channel,
@@ -7839,13 +8244,13 @@ program.command("post <channel> <body>").description("Post a message to a channe
7839
8244
  print(message, () => `Posted ${message.id} to ${message.channel}${message.thread === void 0 ? "" : ` (thread ${message.thread})`}`);
7840
8245
  });
7841
8246
  program.command("ask <text>").description("Post to the society's general channel as the user; the front desk routes it").action(async (text) => {
7842
- const board = await open();
8247
+ const board = await open2();
7843
8248
  const message = await board.postMessage(board.userActor(), { channel: "general", body: text });
7844
8249
  print(message, () => `Posted ${message.id} to general; the concierge wakes on it`);
7845
8250
  });
7846
8251
  var task = program.command("task").description("Manage tasks");
7847
8252
  task.command("create <project> <title>").description("Create a task with a plan, or with one stage anyone in the project may take").option("--body <text>", "task body", "").option("--parent <id>", "parent task id").option("--plan <json>", `stages, for example '[{"name":"analysis","role":"analyst"}]'`).option("--channel <name>", "the project's channel to file it in, such as a release's", "general").option("--as <agent>", "act as this agent instead of the user").action(async (projectSlug, title, opts) => {
7848
- const board = await open();
8253
+ const board = await open2();
7849
8254
  const who = await actorFor(board, opts.as);
7850
8255
  const created = await board.createTask(who, {
7851
8256
  project: projectSlug,
@@ -7861,17 +8266,17 @@ task.command("create <project> <title>").description("Create a task with a plan,
7861
8266
  ].join("\n"));
7862
8267
  });
7863
8268
  task.command("claim <id>").description("Hold the task's current stage").option("--as <agent>", "act as this agent instead of the user").action(async (id, opts) => {
7864
- const board = await open();
8269
+ const board = await open2();
7865
8270
  const claimed = await board.claimTask(await actorFor(board, opts.as), { task_id: id });
7866
8271
  print(claimed, () => `Stage ${claimed.stage} of task ${claimed.id} held by ${claimed.claimedBy ?? "?"} until ${claimed.leaseExpiresAt ?? "?"}`);
7867
8272
  });
7868
8273
  task.command("release <id>").description("Let go of the stage held on a task").option("--as <agent>", "act as this agent instead of the user").action(async (id, opts) => {
7869
- const board = await open();
8274
+ const board = await open2();
7870
8275
  const released = await board.releaseTask(await actorFor(board, opts.as), { task_id: id });
7871
8276
  print(released, () => `Stage ${released.stage} of task ${released.id} released`);
7872
8277
  });
7873
8278
  task.command("advance <id>").description("Finish the current stage; past the last one the task is done").option("--note <text>", "what was done, posted to the task's thread").option("--as <agent>", "act as this agent instead of the user").action(async (id, opts) => {
7874
- const board = await open();
8279
+ const board = await open2();
7875
8280
  const advanced = await board.advanceTask(await actorFor(board, opts.as), {
7876
8281
  task_id: id,
7877
8282
  ...opts.note === void 0 ? {} : { note: opts.note }
@@ -7880,7 +8285,7 @@ task.command("advance <id>").description("Finish the current stage; past the las
7880
8285
  ${describePlan(advanced)}`);
7881
8286
  });
7882
8287
  task.command("plan <id> <stages>").description("Reshape the stages ahead: pass existing stages with their id to keep them, new ones without").option("--on-done <effect>", "override the completion effect for this task").option("--as <agent>", "act as this agent instead of the user").action(async (id, stages, opts) => {
7883
- const board = await open();
8288
+ const board = await open2();
7884
8289
  const planned = await board.planTask(await actorFor(board, opts.as), {
7885
8290
  task_id: id,
7886
8291
  stages: PlanEditSchema.parse(JSON.parse(stages)),
@@ -7889,7 +8294,7 @@ task.command("plan <id> <stages>").description("Reshape the stages ahead: pass e
7889
8294
  print(planned, () => describePlan(planned));
7890
8295
  });
7891
8296
  task.command("update <id>").description("Send a task back to an earlier stage, abandon it, or set blockers, with a note for its thread").option("--stage <stageId>", "move back to this earlier stage").option("--abandon", "abandon the task", false).option("--note <text>", "a note, posted to the task's thread").option("--blocked-by <ids...>", "task ids this task waits on").option("--as <agent>", "act as this agent instead of the user").action(async (id, opts) => {
7892
- const board = await open();
8297
+ const board = await open2();
7893
8298
  const updated = await board.updateTask(await actorFor(board, opts.as), {
7894
8299
  task_id: id,
7895
8300
  ...opts.stage === void 0 ? {} : { stage: opts.stage },
@@ -7900,7 +8305,7 @@ task.command("update <id>").description("Send a task back to an earlier stage, a
7900
8305
  print(updated, () => `Task ${updated.id} is ${updated.status} at ${updated.stage}`);
7901
8306
  });
7902
8307
  task.command("show <id>").description("Show a task with its plan and its thread").action(async (id) => {
7903
- const board = await open();
8308
+ const board = await open2();
7904
8309
  const found = await board.getTask(board.userActor(), { task_id: id });
7905
8310
  const threadState = await board.readThread(id).then((t) => t.state).catch(() => "none");
7906
8311
  const said = found.messages.map((message) => {
@@ -7920,7 +8325,7 @@ ${message.body.trim()}
7920
8325
  ].join("\n"));
7921
8326
  });
7922
8327
  task.command("list <project>").description("List a project's tasks with their current stage").action(async (projectSlug) => {
7923
- const board = await open();
8328
+ const board = await open2();
7924
8329
  const tasks = await board.listTasks(projectSlug);
7925
8330
  print(tasks, () => tasks.length === 0 ? "No tasks." : tasks.map((t) => {
7926
8331
  const stage = t.stages[stageIndex(t, t.stage)];
@@ -7929,7 +8334,7 @@ task.command("list <project>").description("List a project's tasks with their cu
7929
8334
  });
7930
8335
  var thread = program.command("thread").description("Manage threads");
7931
8336
  thread.command("open [taskId]").description("Open a thread on a task, on a proposal, or on a channel with a title").option("--proposal <id>", "open the proposal's thread").option("--channel <ref>", "the channel it hangs off; required for a topic").option("--title <text>", "the thread's title; required for a topic").option("--as <agent>", "act as this agent instead of the user").action(async (taskId, opts) => {
7932
- const board = await open();
8337
+ const board = await open2();
7933
8338
  const opened = await board.openThread(await actorFor(board, opts.as), {
7934
8339
  ...taskId === void 0 ? {} : { task_id: taskId },
7935
8340
  ...opts.proposal === void 0 ? {} : { proposal_id: opts.proposal },
@@ -7939,7 +8344,7 @@ thread.command("open [taskId]").description("Open a thread on a task, on a propo
7939
8344
  print(opened, () => `Thread ${opened.id} opened on ${opened.channel}: ${opened.title}`);
7940
8345
  });
7941
8346
  thread.command("close <threadId>").description("Close a thread with a summary posted to its channel").requiredOption("--summary <text>", "closure summary").option("--as <agent>", "act as this agent instead of the user").action(async (threadId, opts) => {
7942
- const board = await open();
8347
+ const board = await open2();
7943
8348
  const summary = await board.closeThread(await actorFor(board, opts.as), {
7944
8349
  thread_id: threadId,
7945
8350
  summary: opts.summary
@@ -7947,7 +8352,7 @@ thread.command("close <threadId>").description("Close a thread with a summary po
7947
8352
  print(summary, () => `Thread closed; summary posted as ${summary.id} to ${summary.channel}`);
7948
8353
  });
7949
8354
  thread.command("list").description("List threads, the society's first, then each project's").option("--open", "only open threads", false).action(async (opts) => {
7950
- const board = await open();
8355
+ const board = await open2();
7951
8356
  const threads = (await board.listThreads()).filter((t) => !opts.open || t.state === "open");
7952
8357
  print(threads, () => threads.length === 0 ? "No threads." : threads.map((t) => {
7953
8358
  const about = t.subject === void 0 ? "topic" : t.subject.kind;
@@ -7955,7 +8360,7 @@ thread.command("list").description("List threads, the society's first, then each
7955
8360
  }).join("\n"));
7956
8361
  });
7957
8362
  thread.command("show <threadId>").description("Show a thread and its messages").action(async (threadId) => {
7958
- const board = await open();
8363
+ const board = await open2();
7959
8364
  const found = await board.readThread(threadId);
7960
8365
  const messages = await board.listThread(threadId);
7961
8366
  print({ thread: found, messages }, () => [
@@ -7966,17 +8371,17 @@ thread.command("show <threadId>").description("Show a thread and its messages").
7966
8371
  ].join("\n"));
7967
8372
  });
7968
8373
  program.command("pause").description("Stop all wakeups; turns in flight finish").action(async () => {
7969
- const board = await open();
8374
+ const board = await open2();
7970
8375
  await board.setPaused(board.userActor(), true);
7971
8376
  print({ paused: true }, () => "Society paused");
7972
8377
  });
7973
8378
  program.command("resume").description("Allow wakeups again").action(async () => {
7974
- const board = await open();
8379
+ const board = await open2();
7975
8380
  await board.setPaused(board.userActor(), false);
7976
8381
  print({ paused: false }, () => "Society resumed");
7977
8382
  });
7978
8383
  program.command("society").description("The society's settings").command("timezone [zone]").description("Show the society's time zone, or set it: crons that name none are read in it").action(async (zone) => {
7979
- const board = await open();
8384
+ const board = await open2();
7980
8385
  const society = zone === void 0 ? await board.society() : await board.setTimezone(board.userActor(), zone);
7981
8386
  print({ timezone: society.timezone }, () => `Time zone: ${society.timezone}`);
7982
8387
  });
@@ -7992,13 +8397,13 @@ function describeCron(cron2, timeZone) {
7992
8397
  }
7993
8398
  var cron = program.command("cron").description("Crons that wake citizens on a clock");
7994
8399
  cron.command("list").description("List crons, ended ones too with --all").option("--all", "include ended crons", false).action(async (opts) => {
7995
- const board = await open();
8400
+ const board = await open2();
7996
8401
  const { timezone } = await board.society();
7997
8402
  const crons = (await board.listCrons()).filter((each) => opts.all || each.ended === void 0);
7998
8403
  print(crons, () => crons.length === 0 ? "No crons." : crons.map((each) => describeCron(each, timezone)).join("\n"));
7999
8404
  });
8000
8405
  cron.command("add <agent> <title>").description("Set a cron that wakes a citizen, with a note on what to do then").requiredOption("--note <text>", "what the citizen does when it fires").option("--cron <expression>", "five fields: minute hour day-of-month month day-of-week").option("--timezone <zone>", "the zone the expression is read in; the society's otherwise").option("--at <time>", "one time to fire, with its offset, instead of --cron").option("--project <slug>", "where it fires: a project the citizen is in, or society").option("--thread <id>", "an open thread there whose conversation it fires in").option("--channel <name>", "an open channel there, other than general, whose conversation it fires in").option("--as <agent>", "act as this agent instead of the user").action(async (agentName, title, opts) => {
8001
- const board = await open();
8406
+ const board = await open2();
8002
8407
  const created = await board.createCron(await actorFor(board, opts.as), {
8003
8408
  title,
8004
8409
  note: opts.note,
@@ -8018,7 +8423,7 @@ for (const [name, paused] of [
8018
8423
  ["resume", false]
8019
8424
  ]) {
8020
8425
  cron.command(`${name} <id>`).description(paused ? "Pause a cron" : "Resume a paused cron from its next time").option("--as <agent>", "act as this agent instead of the user").action(async (id, opts) => {
8021
- const board = await open();
8426
+ const board = await open2();
8022
8427
  const updated = await board.updateCron(await actorFor(board, opts.as), {
8023
8428
  cron_id: id,
8024
8429
  paused
@@ -8028,7 +8433,7 @@ for (const [name, paused] of [
8028
8433
  });
8029
8434
  }
8030
8435
  cron.command("remove <id>").description("End a cron for good").requiredOption("--reason <text>", "why it ends").option("--as <agent>", "act as this agent instead of the user").action(async (id, opts) => {
8031
- const board = await open();
8436
+ const board = await open2();
8032
8437
  const removed = await board.removeCron(await actorFor(board, opts.as), {
8033
8438
  cron_id: id,
8034
8439
  reason: opts.reason
@@ -8037,31 +8442,32 @@ cron.command("remove <id>").description("End a cron for good").requiredOption("-
8037
8442
  });
8038
8443
  var knowledge = program.command("knowledge").description("Shared knowledge, by project or society");
8039
8444
  knowledge.command("list [project]").description("List knowledge topics of a project, or of the society when no project is given").action(async (projectSlug) => {
8040
- const board = await open();
8445
+ const board = await open2();
8041
8446
  const topics = await board.listKnowledge(projectSlug ?? null);
8042
- print(topics, () => topics.length === 0 ? "No knowledge yet." : topics.map((t) => `${t.topic} updated by ${t.updatedBy} at ${t.updatedAt}`).join("\n"));
8447
+ print(topics, () => topics.length === 0 ? "No knowledge yet." : topics.map((t) => `${t.topic} revision ${t.revision}, updated by ${t.updatedBy} at ${t.updatedAt}`).join("\n"));
8043
8448
  });
8044
8449
  knowledge.command("show <topic>").description("Print a knowledge topic").option("--project <slug>", "the project's topic instead of the society's").action(async (topic, opts) => {
8045
- const board = await open();
8450
+ const board = await open2();
8046
8451
  const found = (await board.listKnowledge(opts.project ?? null)).find((t) => t.topic === topic);
8047
8452
  if (found === void 0) {
8048
8453
  throw new BoardError("NOT_FOUND", `no knowledge topic ${topic}`);
8049
8454
  }
8050
8455
  print(found, () => found.body);
8051
8456
  });
8052
- knowledge.command("write <topic>").description("Write a knowledge topic as the user, from a file or standard input").option("--project <slug>", "the project's topic instead of the society's").option("--file <path>", "read the body from this file instead of standard input").option("--as <agent>", "act as this agent instead of the user").action(async (topic, opts) => {
8053
- const board = await open();
8054
- const body = opts.file === void 0 ? await readStdin() : await readFile6(path7.resolve(opts.file), "utf8");
8457
+ knowledge.command("write <topic>").description("Write a knowledge topic as the user, from a file or standard input").requiredOption("--revision <n>", "the revision it replaces, as knowledge list shows it; 0 for a new topic").option("--project <slug>", "the project's topic instead of the society's").option("--file <path>", "read the body from this file instead of standard input").option("--as <agent>", "act as this agent instead of the user").action(async (topic, opts) => {
8458
+ const board = await open2();
8459
+ const body = opts.file === void 0 ? await readStdin() : await readFile5(path7.resolve(opts.file), "utf8");
8055
8460
  const written = await board.writeKnowledge(await actorFor(board, opts.as), {
8056
8461
  project: opts.project ?? null,
8057
8462
  topic,
8058
- body
8463
+ body,
8464
+ revision: Number(opts.revision)
8059
8465
  });
8060
- print(written, () => `Wrote ${written.topic} for ${written.project ?? "the society"}`);
8466
+ print(written, () => `Wrote ${written.topic} for ${written.project ?? "the society"} at revision ${written.revision}`);
8061
8467
  });
8062
8468
  var skill = program.command("skill").description("Skills promoted to the society");
8063
8469
  skill.command("list").description("List the society's skills, and each member's own").action(async () => {
8064
- const board = await open();
8470
+ const board = await open2();
8065
8471
  const society = await board.listSocietySkills();
8066
8472
  const own = [];
8067
8473
  for (const member of await board.listAgents()) {
@@ -8076,7 +8482,7 @@ skill.command("list").description("List the society's skills, and each member's
8076
8482
  ].join("\n") || "No skills yet.");
8077
8483
  });
8078
8484
  skill.command("remove <name>").description("Remove a skill from the society's skills; its text is set aside, not destroyed").requiredOption("--reason <text>", "why it is wrong, superseded, or no longer used").option("--as <agent>", "act as this agent instead of the user").action(async (name, opts) => {
8079
- const board = await open();
8485
+ const board = await open2();
8080
8486
  const removed = await board.removeSkill(await actorFor(board, opts.as), {
8081
8487
  name,
8082
8488
  reason: opts.reason
@@ -8085,13 +8491,13 @@ skill.command("remove <name>").description("Remove a skill from the society's sk
8085
8491
  });
8086
8492
  var turn = program.command("turn").description("Turn operations");
8087
8493
  turn.command("digest <agent>").description("Show the unread messages the agent's next turn opens with, in one scope or all; the cursors stay put").option("--limit <n>", "maximum messages", "50").option("--project <slug>", "the scope whose turn to show: a project, or society").action(async (agentName, opts) => {
8088
- const board = await open();
8494
+ const board = await open2();
8089
8495
  const actor = await board.actorFor(agentName);
8090
8496
  const digest = await board.readDigest(opts.project === void 0 ? actor : { ...actor, scope: opts.project }, { advance: false, limit: Number(opts.limit) });
8091
8497
  print(digest, () => digest.messages.length === 0 ? "No unread messages." : digest.messages.map((m) => `[${m.ts}] ${m.channel}${m.thread === void 0 ? "" : ` (thread ${m.thread})`} @${m.author}: ${m.body.trim()}`).join("\n"));
8092
8498
  });
8093
8499
  turn.command("run <agent>").description("Enqueue a manual wake for an agent on a project, or a reflection turn").requiredOption("--project <slug>", "project to act on, or society for a society-scope turn").option("--reason <text>", "why", "manual wake from the admin CLI").option("--reflect", "a reflection turn instead of a working turn", false).action(async (agentName, opts) => {
8094
- const board = await open();
8500
+ const board = await open2();
8095
8501
  const event = await board.requestWake(board.userActor(), {
8096
8502
  agent: agentName,
8097
8503
  project: opts.project,