@kubitnodes/stellaris 0.3.23 → 0.3.25

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 (77) hide show
  1. package/dist/cli/index.js +543 -682
  2. package/dist/server/index.js +866 -735
  3. package/package.json +4 -4
  4. package/web/dist/assets/BackLink-UTiLF1J4.js +1 -0
  5. package/web/dist/assets/{Button-f4jgBZY1.js → Button-Cp4cNRUX.js} +1 -1
  6. package/web/dist/assets/ChannelForms-DD2B0tey.js +1 -0
  7. package/web/dist/assets/ChannelView-CY3oG9hZ.js +2 -0
  8. package/web/dist/assets/CitizenView-BNK5ro-6.js +2 -0
  9. package/web/dist/assets/CitizensView-WPQ6Jhl3.js +1 -0
  10. package/web/dist/assets/{CliIcon-CLHaA65A.js → CliIcon-C7bwChMP.js} +1 -1
  11. package/web/dist/assets/CronsView-_pdgwX1I.js +1 -0
  12. package/web/dist/assets/FileTree-jyDUSu7g.js +1 -0
  13. package/web/dist/assets/FileView-DcQvlc8V.js +4 -0
  14. package/web/dist/assets/KnowledgeView-DDl0HQPG.js +1 -0
  15. package/web/dist/assets/Markdown-DT3V-EVe.js +30 -0
  16. package/web/dist/assets/{MessageItem-Bk9eBp1U.js → MessageItem-agtEugfG.js} +1 -1
  17. package/web/dist/assets/MetricsView-DiXyYeWP.js +1 -0
  18. package/web/dist/assets/NeedsYouView-CKmuYan_.js +1 -0
  19. package/web/dist/assets/Overview-CKcTqsMM.js +1 -0
  20. package/web/dist/assets/{Pane-CJYRVkwh.js → Pane-BaM0TdE6.js} +1 -1
  21. package/web/dist/assets/{Picker-DoxCN6C7.js → Picker-C2ig-0dT.js} +1 -1
  22. package/web/dist/assets/ProjectView-T2OHTh5j.js +1 -0
  23. package/web/dist/assets/ProposalView-B5yjTAcn.js +1 -0
  24. package/web/dist/assets/ProposalsView-p4KjwV9p.js +1 -0
  25. package/web/dist/assets/Quota-CfFGIql9.js +1 -0
  26. package/web/dist/assets/RunnersView-DdvwoOur.js +1 -0
  27. package/web/dist/assets/SkillFileList-CgrCnuaC.js +2 -0
  28. package/web/dist/assets/SkillView-Di5Z9h2L.js +1 -0
  29. package/web/dist/assets/SocietyView-BDiVxUC5.js +1 -0
  30. package/web/dist/assets/StickyList-BaV_KZ8x.js +1 -0
  31. package/web/dist/assets/SubjectThread-DD_ZCCnr.js +1 -0
  32. package/web/dist/assets/TaskView-CKcs-MOC.js +3 -0
  33. package/web/dist/assets/TasksView-oZMjncOU.js +1 -0
  34. package/web/dist/assets/{ThreadForms-C846TvtS.js → ThreadForms-CG5Y926b.js} +1 -1
  35. package/web/dist/assets/ThreadItem-D-n2arFA.js +1 -0
  36. package/web/dist/assets/ThreadView-CT9vd0P4.js +1 -0
  37. package/web/dist/assets/citizen-BiHfph2A.js +1 -0
  38. package/web/dist/assets/{compose-BmqQmcZH.js → compose-DY1WL8zB.js} +1 -1
  39. package/web/dist/assets/format-CENiwkJC.js +55 -0
  40. package/web/dist/assets/governance-C6ez5MLh.js +1 -0
  41. package/web/dist/assets/index-CPXALUV6.js +23 -0
  42. package/web/dist/assets/index-DIa8Dlav.css +2 -0
  43. package/web/dist/assets/mentions-BEL5U6ra.js +3 -0
  44. package/web/dist/assets/{tasks-R3YgpYTe.js → tasks-CAI8Eci7.js} +1 -1
  45. package/web/dist/assets/useMatch-CINzyDWa.js +1 -0
  46. package/web/dist/index.html +9 -9
  47. package/web/dist/assets/BackLink-DGYs80bb.js +0 -1
  48. package/web/dist/assets/ChannelForms-q9zdj4p2.js +0 -1
  49. package/web/dist/assets/ChannelView-CGs_aWL3.js +0 -2
  50. package/web/dist/assets/CitizenView-gcIo_sKt.js +0 -2
  51. package/web/dist/assets/CitizensView-DZ7S8vnJ.js +0 -1
  52. package/web/dist/assets/CronsView-1wns6Uwv.js +0 -1
  53. package/web/dist/assets/KnowledgeView-Cxfi2-Pt.js +0 -1
  54. package/web/dist/assets/Markdown-BhtIFcWc.js +0 -30
  55. package/web/dist/assets/MetricsView-CTEH6rJn.js +0 -1
  56. package/web/dist/assets/NeedsYouView-2VNqH2Sb.js +0 -1
  57. package/web/dist/assets/Overview-CS9HCSmm.js +0 -1
  58. package/web/dist/assets/ProjectView-CJ3r4UdN.js +0 -1
  59. package/web/dist/assets/ProposalView-DLn9dNPp.js +0 -2
  60. package/web/dist/assets/ProposalsView-B-cOxTsQ.js +0 -1
  61. package/web/dist/assets/Quota-Bo7UdWTV.js +0 -1
  62. package/web/dist/assets/RunnersView-DlQruhwp.js +0 -1
  63. package/web/dist/assets/SocietyView-Bl4ErnV8.js +0 -1
  64. package/web/dist/assets/StickyList-MVP34ytV.js +0 -1
  65. package/web/dist/assets/SubjectThread-BFFnHxIF.js +0 -1
  66. package/web/dist/assets/TaskFileView-GZ0eQFUb.js +0 -4
  67. package/web/dist/assets/TaskView-DppFelLW.js +0 -3
  68. package/web/dist/assets/TasksView-DDNXzeG4.js +0 -1
  69. package/web/dist/assets/ThreadItem-C8cou6wD.js +0 -1
  70. package/web/dist/assets/ThreadView-DvSGDYoD.js +0 -1
  71. package/web/dist/assets/citizen-B2Dlbcpe.js +0 -1
  72. package/web/dist/assets/format-DT52SbbR.js +0 -55
  73. package/web/dist/assets/governance-Cr5OlQnw.js +0 -1
  74. package/web/dist/assets/index-BTVy_xFF.js +0 -23
  75. package/web/dist/assets/index-DzV6CCnG.css +0 -2
  76. package/web/dist/assets/mentions-D5hNgtg6.js +0 -3
  77. package/web/dist/assets/useMatch-pEF6RJ6Q.js +0 -1
package/dist/cli/index.js CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  // apps/cli/dist/index.js
4
4
  import { readFile as readFile5 } from "node:fs/promises";
5
- import path7 from "node:path";
5
+ import path8 from "node:path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // packages/board-core/dist/board.js
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
- import path6 from "node:path";
9
+ import { chmod as chmod2, cp, mkdir as mkdir4, readdir as readdir4, readFile as readFile4, rename as rename3, rm as rm2, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
10
+ import path7 from "node:path";
11
11
  import { decodeTime, monotonicFactory } from "ulid";
12
12
  import { z as z16 } from "zod";
13
13
 
@@ -234,6 +234,7 @@ var CronFrontmatterSchema = z3.object({
234
234
  scope: NameSchema,
235
235
  thread: UlidSchema.optional(),
236
236
  channel: NameSchema.optional(),
237
+ place: NameSchema.optional(),
237
238
  schedule: CronScheduleSchema,
238
239
  createdBy: NameSchema,
239
240
  createdAt: IsoDateTimeSchema,
@@ -714,7 +715,6 @@ var ReallocationProposalSchema = z5.object({
714
715
  changes: RoleChangesSchema,
715
716
  description: z5.string().min(1)
716
717
  });
717
- var LegacyReallocationSchema = z5.object({ description: z5.string().min(1) });
718
718
  function applyRoleChanges(role2, changes) {
719
719
  return {
720
720
  ...role2,
@@ -759,8 +759,7 @@ var ArchiveProposalSchema = z5.object({
759
759
  });
760
760
  var SkillProposalSchema = z5.object({
761
761
  name: NameSchema,
762
- summary: z5.string().min(1).max(200),
763
- body: z5.string().min(1).optional()
762
+ summary: z5.string().min(1).max(200)
764
763
  });
765
764
  var ProposalCharterSchemas = {
766
765
  role: RoleCharterSchema,
@@ -837,14 +836,10 @@ function describeCharter(kind, charter) {
837
836
  }
838
837
  case "reallocation": {
839
838
  const parsed = ReallocationProposalSchema.safeParse(charter);
840
- if (parsed.success) {
841
- const { role: role2, changes, description } = parsed.data;
842
- return `reallocation of role ${role2} (${describeRoleChanges(changes)}): ${description}`;
843
- }
844
- const legacy = LegacyReallocationSchema.safeParse(charter);
845
- if (!legacy.success)
839
+ if (!parsed.success)
846
840
  break;
847
- return `reallocation: ${legacy.data.description}`;
841
+ const { role: role2, changes, description } = parsed.data;
842
+ return `reallocation of role ${role2} (${describeRoleChanges(changes)}): ${description}`;
848
843
  }
849
844
  case "skill": {
850
845
  const parsed = SkillProposalSchema.safeParse(charter);
@@ -939,7 +934,8 @@ var DashboardSchema = z5.object({
939
934
  var ProposalOriginSchema = z5.object({
940
935
  scope: NameSchema,
941
936
  thread: UlidSchema.optional(),
942
- channel: NameSchema.optional()
937
+ channel: NameSchema.optional(),
938
+ place: NameSchema.optional()
943
939
  });
944
940
  var SKILL_FILES_LIMIT = 100;
945
941
  var SKILL_BYTES_LIMIT = 10 * 1024 * 1024;
@@ -1048,12 +1044,10 @@ var VerbInputs = {
1048
1044
  channel: ChannelRefSchema.optional(),
1049
1045
  limit: z7.number().int().positive().max(100).default(20)
1050
1046
  }),
1051
- /** On a task or a proposal, whose id the thread takes, or on a channel with a title. */
1047
+ /** On a channel with a title; a task's and a proposal's open with them. */
1052
1048
  open_thread: z7.object({
1053
- task_id: UlidSchema.optional(),
1054
- proposal_id: UlidSchema.optional(),
1055
- channel: ChannelRefSchema.optional(),
1056
- title: z7.string().min(1).max(200).optional()
1049
+ channel: ChannelRefSchema,
1050
+ title: z7.string().min(1).max(200)
1057
1051
  }),
1058
1052
  close_thread: z7.object({ thread_id: UlidSchema, summary: z7.string().min(1) }),
1059
1053
  create_task: z7.object({
@@ -1293,6 +1287,7 @@ var TurnExitReasonSchema = z8.enum([
1293
1287
  "blocked",
1294
1288
  "stopped"
1295
1289
  ]);
1290
+ var ResumeFailureSchema = z8.enum(["lost", "retry"]);
1296
1291
  var AgentEventSchema = z8.discriminatedUnion("type", [
1297
1292
  z8.object({
1298
1293
  type: z8.literal("turn_started"),
@@ -1344,6 +1339,8 @@ var LiveTurnEventSchema = z8.object({
1344
1339
  thread: UlidSchema.optional(),
1345
1340
  /** The channel, never general, whose conversation the turn is in. */
1346
1341
  channel: NameSchema.optional(),
1342
+ /** The project whose channels' conversation the turn is in, for a turn in the society scope. */
1343
+ place: NameSchema.optional(),
1347
1344
  event: AgentEventSchema
1348
1345
  });
1349
1346
 
@@ -1430,7 +1427,8 @@ var TurnWorkspaceSchema = z9.discriminatedUnion("kind", [
1430
1427
  z9.object({
1431
1428
  kind: z9.literal("home"),
1432
1429
  thread: UlidSchema.optional(),
1433
- channel: NameSchema.optional()
1430
+ channel: NameSchema.optional(),
1431
+ place: NameSchema.optional()
1434
1432
  }),
1435
1433
  z9.object({
1436
1434
  kind: z9.literal("project"),
@@ -1439,7 +1437,16 @@ var TurnWorkspaceSchema = z9.discriminatedUnion("kind", [
1439
1437
  thread: UlidSchema.optional(),
1440
1438
  channel: NameSchema.optional()
1441
1439
  }),
1442
- z9.object({ kind: z9.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
1440
+ z9.object({
1441
+ kind: z9.literal("task"),
1442
+ repo: ProjectRepoSchema,
1443
+ taskId: UlidSchema,
1444
+ /**
1445
+ * The citizen holding the task's current stage, when someone does: a turn of anyone else
1446
+ * works detached at the branch's tip, so the holder's turns find the branch free.
1447
+ */
1448
+ holder: NameSchema.optional()
1449
+ })
1443
1450
  ]);
1444
1451
  var TurnJobSchema = z9.object({
1445
1452
  turnId: UlidSchema,
@@ -1455,6 +1462,8 @@ var TurnJobSchema = z9.object({
1455
1462
  thread: UlidSchema.optional(),
1456
1463
  /** The channel, never general, whose conversation the turn is in. */
1457
1464
  channel: NameSchema.optional(),
1465
+ /** The project whose channels' conversation the turn is in, for a turn in the society scope. */
1466
+ place: NameSchema.optional(),
1458
1467
  /**
1459
1468
  * The session to resume, or null to start one: the CLI's adapter picks a fresh session's id, and
1460
1469
  * the outcome reports it.
@@ -1475,6 +1484,12 @@ var TurnJobSchema = z9.object({
1475
1484
  workspace: TurnWorkspaceSchema
1476
1485
  });
1477
1486
  var TurnWorkSchema = z9.object({ branch: z9.string().min(1), head: z9.string().min(1) });
1487
+ var StuckWorkSchema = z9.object({
1488
+ branch: z9.string().min(1),
1489
+ agent: NameSchema,
1490
+ commits: z9.array(z9.string()),
1491
+ total: z9.number().int().positive()
1492
+ });
1478
1493
  var TurnEventsSchema = z9.object({ entries: z9.array(TranscriptEntrySchema) });
1479
1494
  var TurnOutcomeSchema = z9.object({
1480
1495
  exitReason: TurnExitReasonSchema,
@@ -1484,8 +1499,24 @@ var TurnOutcomeSchema = z9.object({
1484
1499
  costUsd: z9.number().nonnegative(),
1485
1500
  /** The session's running total after this turn, for CLIs that report one. */
1486
1501
  sessionCostUsd: z9.number().nonnegative().optional(),
1487
- /** The session the CLI used, which differs from the job's for CLIs that assign their own ids. */
1488
- session: z9.string().min(1),
1502
+ /**
1503
+ * The session the CLI used, which differs from the job's for CLIs that assign their own ids. A
1504
+ * turn whose CLI never started, because it failed or was stopped first, reports the job's own,
1505
+ * null in a conversation's first turn, so a session never started is not recorded.
1506
+ */
1507
+ session: z9.string().min(1).nullable(),
1508
+ /**
1509
+ * Set when the CLI could not resume the job's session, which failed the turn before any model
1510
+ * call; the server then drops a `lost` session, and one that failed `retry` too often in a row. A
1511
+ * runner from before never sends it.
1512
+ */
1513
+ resumeFailed: ResumeFailureSchema.optional(),
1514
+ /**
1515
+ * Set when the model service turned the turn away for now, as when it was overloaded or failed
1516
+ * on its side, which the scheduler answers by sending the same wake again within minutes. A
1517
+ * runner from before never sends it.
1518
+ */
1519
+ transient: z9.boolean().optional(),
1489
1520
  /** The model the CLI reported, when it reported one. */
1490
1521
  model: z9.string().nullable(),
1491
1522
  work: TurnWorkSchema.nullable(),
@@ -1494,6 +1525,11 @@ var TurnOutcomeSchema = z9.object({
1494
1525
  * branch after the hand-back: uncommitted changes, or commits no branch contains.
1495
1526
  */
1496
1527
  leftovers: z9.boolean().default(false),
1528
+ /**
1529
+ * Commits the hand-back found its task's worktrees holding that no fast-forward of the task's
1530
+ * branch can take, each told once; a runner from before never sends it.
1531
+ */
1532
+ stuck: z9.array(StuckWorkSchema).optional(),
1497
1533
  /**
1498
1534
  * The CLI's plan quota as the turn's stream last reported it, when it reported any; a runner from
1499
1535
  * before never sends it.
@@ -1612,7 +1648,8 @@ var TurnImageSchema = z9.discriminatedUnion("kind", [
1612
1648
  ]);
1613
1649
  var WorkspaceConversationSchema = z9.discriminatedUnion("kind", [
1614
1650
  z9.object({ kind: z9.literal("thread"), id: UlidSchema }),
1615
- z9.object({ kind: z9.literal("channel"), scope: NameSchema, name: NameSchema })
1651
+ z9.object({ kind: z9.literal("channel"), scope: NameSchema, name: NameSchema }),
1652
+ z9.object({ kind: z9.literal("place"), project: NameSchema })
1616
1653
  ]);
1617
1654
  var HeldWorkspaceSchema = z9.object({
1618
1655
  agent: NameSchema,
@@ -1718,6 +1755,7 @@ var TurnDispatchSchema = z10.object({
1718
1755
  project: NameSchema,
1719
1756
  thread: TurnThreadSchema.optional(),
1720
1757
  channel: NameSchema.optional(),
1758
+ place: NameSchema.optional(),
1721
1759
  trigger: TriggerSchema,
1722
1760
  priority: z10.number().int().min(0).max(2),
1723
1761
  onboarding: z10.boolean().default(false),
@@ -1725,7 +1763,13 @@ var TurnDispatchSchema = z10.object({
1725
1763
  crons: z10.array(UlidSchema).optional()
1726
1764
  });
1727
1765
  function conversationPart(conversation) {
1728
- return conversation.thread ?? (conversation.channel === void 0 ? void 0 : `#${conversation.channel}`);
1766
+ if (conversation.thread !== void 0) {
1767
+ return conversation.thread;
1768
+ }
1769
+ if (conversation.channel !== void 0) {
1770
+ return `#${conversation.channel}`;
1771
+ }
1772
+ return conversation.place === void 0 ? void 0 : `~${conversation.place}`;
1729
1773
  }
1730
1774
  function channelConversation(scope, channel) {
1731
1775
  const at = channel.indexOf("/");
@@ -1733,6 +1777,10 @@ function channelConversation(scope, channel) {
1733
1777
  const name = at < 0 ? channel : channel.slice(at + 1);
1734
1778
  return place === scope && name !== "general" ? name : void 0;
1735
1779
  }
1780
+ function placeConversation(scope, channel) {
1781
+ const at = channel.indexOf("/");
1782
+ return scope === SOCIETY_SCOPE && at >= 0 ? channel.slice(0, at) : void 0;
1783
+ }
1736
1784
  var TurnRecordSchema = z10.object({
1737
1785
  /** Assigned by the board when the turn begins; the turn's transcript is filed under it. */
1738
1786
  id: UlidSchema.optional(),
@@ -1742,6 +1790,8 @@ var TurnRecordSchema = z10.object({
1742
1790
  thread: UlidSchema.optional(),
1743
1791
  /** The channel, never general, whose conversation the turn was in. */
1744
1792
  channel: NameSchema.optional(),
1793
+ /** The project whose channels' conversation the turn was in, for a turn in the society scope. */
1794
+ place: NameSchema.optional(),
1745
1795
  runner: NameSchema,
1746
1796
  cli: CliKindSchema.nullable(),
1747
1797
  session: z10.string().nullable(),
@@ -1765,6 +1815,14 @@ var TurnRecordSchema = z10.object({
1765
1815
  work: TurnWorkSchema.optional(),
1766
1816
  /** Who stopped the turn, for one that ended `stopped`. */
1767
1817
  stoppedBy: NameSchema.optional(),
1818
+ /** Why the CLI could not resume the turn's session, for a turn that failed so. */
1819
+ resumeFailed: ResumeFailureSchema.optional(),
1820
+ /** Whether the model service turned the turn away for now: overloaded, or failing on its side. */
1821
+ transient: z10.boolean().optional(),
1822
+ /** How many turns in a row, this one included, could not resume the session for a `retry`. */
1823
+ resumeRetries: z10.number().int().positive().optional(),
1824
+ /** Whether the server dropped the conversation's session after this turn, which the next starts anew. */
1825
+ sessionDropped: z10.boolean().optional(),
1768
1826
  /** The crons whose fire started the turn, which record how it ended. */
1769
1827
  crons: z10.array(UlidSchema).optional()
1770
1828
  });
@@ -1774,6 +1832,7 @@ var RunningTurnSchema = z10.object({
1774
1832
  scope: NameSchema,
1775
1833
  thread: UlidSchema.optional(),
1776
1834
  channel: NameSchema.optional(),
1835
+ place: NameSchema.optional(),
1777
1836
  cli: CliKindSchema,
1778
1837
  askedIn: ChannelRefSchema.optional(),
1779
1838
  steerable: z10.boolean(),
@@ -1804,6 +1863,8 @@ var TurnHistoryEntrySchema = z10.object({
1804
1863
  thread: UlidSchema.optional(),
1805
1864
  /** The channel, never general, whose conversation the turn was in. */
1806
1865
  channel: NameSchema.optional(),
1866
+ /** The project whose channels' conversation the turn was in, for a turn in the society scope. */
1867
+ place: NameSchema.optional(),
1807
1868
  trigger: z10.string(),
1808
1869
  exitReason: z10.string().nullable(),
1809
1870
  costUsd: z10.number(),
@@ -1832,9 +1893,19 @@ var OPS_SIGNAL_KINDS = [
1832
1893
  "runner",
1833
1894
  "home_conflict",
1834
1895
  "stuck_workspace",
1835
- "cron_failing"
1896
+ "cron_failing",
1897
+ "turn_refused"
1836
1898
  ];
1837
1899
  var OpsSignalKindSchema = z11.enum(OPS_SIGNAL_KINDS);
1900
+ var WAKING_SIGNAL_KINDS = [
1901
+ "backlog",
1902
+ "role_gap",
1903
+ "churn",
1904
+ "stale_thread",
1905
+ "idle_member",
1906
+ "blocked_capability",
1907
+ "scaled"
1908
+ ];
1838
1909
  var OpsSignalSchema = z11.object({
1839
1910
  kind: OpsSignalKindSchema,
1840
1911
  /** Identifies the condition, for example `backlog:demo:engineer`, so a persisting one is not logged again every pass. */
@@ -1847,8 +1918,15 @@ var OpsSignalSchema = z11.object({
1847
1918
  project: NameSchema.optional(),
1848
1919
  agent: NameSchema.optional(),
1849
1920
  role: NameSchema.optional(),
1850
- taskId: UlidSchema.optional()
1921
+ taskId: UlidSchema.optional(),
1922
+ /**
1923
+ * Whether this log of the condition wakes the roles charted for `ops_event`, as its publisher
1924
+ * decided: the operations pass wakes them when a condition is new, and again only at the steps
1925
+ * it names, not at every repeat. Absent, the kind decides.
1926
+ */
1927
+ wakes: z11.boolean().optional()
1851
1928
  });
1929
+ var WAKING = new Set(WAKING_SIGNAL_KINDS);
1852
1930
 
1853
1931
  // packages/shared/dist/metrics.js
1854
1932
  import { z as z12 } from "zod";
@@ -2006,7 +2084,7 @@ var RunnerConfigSchema = z13.object({
2006
2084
  });
2007
2085
 
2008
2086
  // packages/shared/dist/version.js
2009
- var STELLARIS_VERSION = "0.3.23";
2087
+ var STELLARIS_VERSION = "0.3.25";
2010
2088
 
2011
2089
  // packages/shared/dist/mentions.js
2012
2090
  import { parse, postprocess, preprocess } from "micromark";
@@ -2387,6 +2465,7 @@ var HOME_GITIGNORE = [
2387
2465
  "/projects/*/last-turn.json",
2388
2466
  "/projects/*/threads/",
2389
2467
  "/projects/*/channels/",
2468
+ "/projects/*/places/",
2390
2469
  `/${HOME_SCRATCH}/`,
2391
2470
  "*.tmp",
2392
2471
  "node_modules/",
@@ -2404,7 +2483,7 @@ var HOME_GITIGNORE = [
2404
2483
  var IGNORE_FILE = `${HOME_GITIGNORE.join("\n")}
2405
2484
  `;
2406
2485
  var PRE_RECEIVE = `#!/bin/sh
2407
- forbidden='^(agent\\.json|cursors\\.json|role\\.md|turns/|\\.claude/|\\.codex/|projects/[^/]+/(sessions[^/]*\\.json|last-turn\\.json|threads/|channels/))'
2486
+ forbidden='^(agent\\.json|cursors\\.json|role\\.md|turns/|\\.claude/|\\.codex/|projects/[^/]+/(sessions[^/]*\\.json|last-turn\\.json|threads/|channels/|places/))'
2408
2487
  while read old new ref; do
2409
2488
  if [ "$ref" != "refs/heads/main" ]; then
2410
2489
  echo "a home takes pushes to main only" >&2
@@ -2730,6 +2809,7 @@ var TurnPayload = z14.object({
2730
2809
  project: z14.string(),
2731
2810
  thread: z14.string().optional(),
2732
2811
  channel: z14.string().optional(),
2812
+ place: z14.string().optional(),
2733
2813
  trigger: z14.string()
2734
2814
  });
2735
2815
  var TaskPayload = z14.object({
@@ -2775,11 +2855,16 @@ function answers(mention, turn2) {
2775
2855
  if (!mention.scopes.includes(turn2.project)) {
2776
2856
  return false;
2777
2857
  }
2778
- const own = mention.thread === null ? channelConversation(turn2.project, mention.channel) : void 0;
2858
+ const topLevel = mention.thread === null;
2859
+ const own = topLevel ? channelConversation(turn2.project, mention.channel) : void 0;
2860
+ const place = topLevel ? placeConversation(turn2.project, mention.channel) : void 0;
2779
2861
  if (turn2.channel !== null) {
2780
- return mention.thread === null && own === turn2.channel;
2862
+ return topLevel && own === turn2.channel;
2863
+ }
2864
+ if (turn2.place !== null) {
2865
+ return topLevel && place === turn2.place;
2781
2866
  }
2782
- return mention.thread === null && own === void 0 || turn2.trigger === "mention";
2867
+ return topLevel && own === void 0 && place === void 0 || turn2.trigger === "mention";
2783
2868
  }
2784
2869
  function computeMetrics(input) {
2785
2870
  const { events, now, window } = input;
@@ -2845,6 +2930,7 @@ function computeMetrics(input) {
2845
2930
  at,
2846
2931
  thread: turn2.data.thread ?? null,
2847
2932
  channel: turn2.data.channel ?? null,
2933
+ place: turn2.data.place ?? null,
2848
2934
  project: turn2.data.project,
2849
2935
  trigger: turn2.data.trigger
2850
2936
  };
@@ -3044,6 +3130,7 @@ var TurnHistoryPayloadSchema = z15.object({
3044
3130
  project: NameSchema,
3045
3131
  thread: UlidSchema.optional(),
3046
3132
  channel: NameSchema.optional(),
3133
+ place: NameSchema.optional(),
3047
3134
  trigger: z15.string().default("manual"),
3048
3135
  exitReason: z15.string().nullable().default(null),
3049
3136
  costUsd: z15.number().default(0),
@@ -3282,11 +3369,14 @@ var BoardPaths = class {
3282
3369
  return path5.join(this.agentProjects(name), slug);
3283
3370
  }
3284
3371
  /**
3285
- * A thread's or a channel's conversation's session and last turn, beside the scope's home
3286
- * conversation; `part` is a thread's id or `#channel`.
3372
+ * A thread's, a channel's, or, in the society scope, a project's conversation's session and last
3373
+ * turn, beside the scope's home conversation; `part` is a thread's id, `#channel`, or `~project`.
3287
3374
  */
3288
3375
  agentConversation(name, slug, part) {
3289
- return part.startsWith("#") ? path5.join(this.agentProject(name, slug), "channels", part.slice(1)) : path5.join(this.agentProject(name, slug), "threads", part);
3376
+ if (part.startsWith("#")) {
3377
+ return path5.join(this.agentProject(name, slug), "channels", part.slice(1));
3378
+ }
3379
+ return part.startsWith("~") ? path5.join(this.agentProject(name, slug), "places", part.slice(1)) : path5.join(this.agentProject(name, slug), "threads", part);
3290
3380
  }
3291
3381
  agentCursors(name) {
3292
3382
  return path5.join(this.agent(name), "cursors.json");
@@ -3320,6 +3410,111 @@ var BoardPaths = class {
3320
3410
  }
3321
3411
  };
3322
3412
 
3413
+ // packages/board-core/dist/skill-folder.js
3414
+ import { constants } from "node:fs";
3415
+ import { lstat, open as open2, readdir as readdir3, readlink, realpath } from "node:fs/promises";
3416
+ import path6 from "node:path";
3417
+ var SKILL_TEXT_LIMIT_CHARS = 1e5;
3418
+ var SKILL_TEXT_LIMIT_BYTES = SKILL_TEXT_LIMIT_CHARS * 4;
3419
+ function skillFileText(bytes) {
3420
+ const text = bytes.includes(0) ? null : bytes.toString("utf8");
3421
+ return {
3422
+ text: text === null ? null : text.slice(0, SKILL_TEXT_LIMIT_CHARS),
3423
+ truncated: text !== null && text.length > SKILL_TEXT_LIMIT_CHARS
3424
+ };
3425
+ }
3426
+ function inside(root, file) {
3427
+ const relative = path6.relative(root, file);
3428
+ return relative !== ".." && !relative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(relative);
3429
+ }
3430
+ async function handleInside(root, handle) {
3431
+ if (process.platform !== "linux") {
3432
+ return true;
3433
+ }
3434
+ const file = await readlink(`/proc/self/fd/${handle.fd}`).catch(() => null);
3435
+ return file !== null && inside(root, file);
3436
+ }
3437
+ async function walk(root, prefix, found) {
3438
+ const entries = await readdir3(path6.join(root, ...prefix === "" ? [] : prefix.split("/")), {
3439
+ withFileTypes: true
3440
+ }).catch(() => []);
3441
+ for (const entry of entries.toSorted((a, b) => a.name < b.name ? -1 : 1)) {
3442
+ if (found.length >= SKILL_FILES_LIMIT) {
3443
+ return;
3444
+ }
3445
+ const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
3446
+ if (!isSafeRelativePath(relative)) {
3447
+ continue;
3448
+ }
3449
+ if (entry.isDirectory()) {
3450
+ await walk(root, relative, found);
3451
+ } else if (entry.isFile() && !entry.name.endsWith(".tmp")) {
3452
+ found.push(relative);
3453
+ }
3454
+ }
3455
+ }
3456
+ async function readEntry(root, relative) {
3457
+ let real;
3458
+ try {
3459
+ real = await realpath(path6.join(root, ...relative.split("/")));
3460
+ } catch {
3461
+ return null;
3462
+ }
3463
+ if (!inside(root, real)) {
3464
+ return null;
3465
+ }
3466
+ const handle = await open2(real, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK).catch(() => null);
3467
+ if (handle === null) {
3468
+ return null;
3469
+ }
3470
+ try {
3471
+ if (!await handleInside(root, handle)) {
3472
+ return null;
3473
+ }
3474
+ const stats = await handle.stat();
3475
+ if (!stats.isFile()) {
3476
+ return null;
3477
+ }
3478
+ const buffer = Buffer.alloc(Math.min(stats.size, SKILL_TEXT_LIMIT_BYTES));
3479
+ let size = 0;
3480
+ while (size < buffer.length) {
3481
+ const { bytesRead } = await handle.read(buffer, size, buffer.length - size, size);
3482
+ if (bytesRead === 0) {
3483
+ break;
3484
+ }
3485
+ size += bytesRead;
3486
+ }
3487
+ return {
3488
+ path: relative,
3489
+ size: stats.size,
3490
+ executable: (stats.mode & 73) !== 0,
3491
+ ...skillFileText(buffer.subarray(0, size))
3492
+ };
3493
+ } finally {
3494
+ await handle.close();
3495
+ }
3496
+ }
3497
+ async function readSkillFolder(dir) {
3498
+ const info = await lstat(dir).catch(() => null);
3499
+ if (info === null || !info.isDirectory()) {
3500
+ return null;
3501
+ }
3502
+ const root = await realpath(dir).catch(() => null);
3503
+ if (root === null) {
3504
+ return null;
3505
+ }
3506
+ const found = [];
3507
+ await walk(root, "", found);
3508
+ const files = [];
3509
+ for (const relative of found) {
3510
+ const file = await readEntry(root, relative);
3511
+ if (file !== null) {
3512
+ files.push(file);
3513
+ }
3514
+ }
3515
+ return files.some((file) => file.path === "SKILL.md") ? files : null;
3516
+ }
3517
+
3323
3518
  // packages/board-core/dist/tokens.js
3324
3519
  import { createHash as createHash2, randomBytes } from "node:crypto";
3325
3520
  function mintToken() {
@@ -3333,7 +3528,7 @@ function hashToken(token) {
3333
3528
  var SYSTEM_ACTOR = { name: "board", role: USER_ROLE };
3334
3529
  var CLOSING_ATTEMPTS = 3;
3335
3530
  function ownTurn(actor, turn2) {
3336
- return turn2.agent === actor.name && turn2.scope === actor.scope && turn2.thread === actor.thread && turn2.channel === actor.channel;
3531
+ return turn2.agent === actor.name && turn2.scope === actor.scope && turn2.thread === actor.thread && turn2.channel === actor.channel && turn2.place === actor.place;
3337
3532
  }
3338
3533
  function namesList(names) {
3339
3534
  return names.length <= 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}`;
@@ -3346,37 +3541,45 @@ var SignInsSchema = z16.object({
3346
3541
  signIns: z16.array(z16.object({
3347
3542
  tokenHash: z16.string().min(1),
3348
3543
  login: z16.string().min(1),
3349
- /** The account's picture at the identity provider; sign-ins from before it have none. */
3544
+ /** The account's picture, when GitHub gave one on its own avatar host. */
3350
3545
  avatarUrl: z16.url().optional(),
3351
3546
  createdAt: IsoDateTimeSchema,
3352
3547
  expiresAt: IsoDateTimeSchema
3353
3548
  })).default([])
3354
3549
  });
3355
- var TurnStartPayloadSchema = z16.object({ project: NameSchema, turnId: UlidSchema.optional() });
3550
+ var TurnStartPayloadSchema = z16.object({ turnId: UlidSchema });
3356
3551
  var CursorsSchema = z16.object({
3357
3552
  digest: z16.string().nullable(),
3358
3553
  scopes: z16.record(z16.string(), z16.string()).default({}),
3359
- threadsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3360
3554
  channelsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3555
+ placesFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3361
3556
  /** How far a work role has read news, which goes to whichever of its general conversations reads first. */
3362
3557
  news: z16.string().optional()
3363
3558
  });
3364
- var NO_CURSORS = { digest: null, scopes: {}, threadsFrom: {}, channelsFrom: {} };
3559
+ var NO_CURSORS = {
3560
+ digest: null,
3561
+ scopes: {},
3562
+ channelsFrom: {},
3563
+ placesFrom: {}
3564
+ };
3365
3565
  function conversationKey(scope, part) {
3366
3566
  return part === null ? scope : `${scope}/${part}`;
3367
3567
  }
3368
3568
  function channelOfPart(part) {
3369
3569
  return part?.startsWith("#") === true ? part.slice(1) : void 0;
3370
3570
  }
3571
+ function placeOfPart(part) {
3572
+ return part?.startsWith("~") === true ? part.slice(1) : void 0;
3573
+ }
3371
3574
  function threadOfPart(part) {
3372
- return part === null || part === void 0 || part.startsWith("#") ? void 0 : part;
3575
+ return part === null || part === void 0 || part.startsWith("#") || part.startsWith("~") ? void 0 : part;
3373
3576
  }
3374
3577
  function channelStart(cursors, scope) {
3375
3578
  const from = cursors.channelsFrom[scope];
3376
3579
  return from === void 0 ? cursors.digest : from;
3377
3580
  }
3378
- function threadStart(cursors, scope) {
3379
- const from = cursors.threadsFrom[scope];
3581
+ function placeStart(cursors, scope) {
3582
+ const from = cursors.placesFrom[scope];
3380
3583
  return from === void 0 ? cursors.digest : from;
3381
3584
  }
3382
3585
  function newsStart(cursors) {
@@ -3390,7 +3593,7 @@ function cursorOf(cursors, scope, part = null) {
3390
3593
  if (part === null) {
3391
3594
  return cursors.scopes[scope] ?? cursors.digest;
3392
3595
  }
3393
- return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : threadStart(cursors, scope));
3596
+ return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : part.startsWith("~") ? placeStart(cursors, scope) : cursors.digest);
3394
3597
  }
3395
3598
  function byId(a, b) {
3396
3599
  return a.message.id < b.message.id ? -1 : a.message.id > b.message.id ? 1 : 0;
@@ -3398,21 +3601,23 @@ function byId(a, b) {
3398
3601
  function turnTime(record) {
3399
3602
  return record.endedAt ?? record.startedAt;
3400
3603
  }
3604
+ function turnConversation(record) {
3605
+ return record.thread !== void 0 ? ` in thread ${record.thread}` : record.channel !== void 0 ? ` in #${record.channel}` : record.place !== void 0 ? ` in ${record.place}'s channels` : "";
3606
+ }
3401
3607
  var PausedSchema = z16.object({ paused: z16.boolean() });
3402
3608
  var DEFAULT_LEASE_MS = 30 * 60 * 1e3;
3403
3609
  function withoutMentions(text) {
3404
3610
  return text.replaceAll(MENTION_PATTERN, "$1$2");
3405
3611
  }
3406
- var SKILL_TEXT_LIMIT_CHARS = 1e5;
3407
3612
  function skillFolderOf(proposal2) {
3408
3613
  if (proposal2.kind !== "skill") {
3409
3614
  return null;
3410
3615
  }
3411
3616
  const parsed = SkillProposalSchema.safeParse(proposal2.charter);
3412
- return parsed.success && parsed.data.body === void 0 ? parsed.data.name : null;
3617
+ return parsed.success ? parsed.data.name : null;
3413
3618
  }
3414
3619
  function proposedFrom(origin, where) {
3415
- return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel;
3620
+ return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel && origin.place === where.place;
3416
3621
  }
3417
3622
  function megabytes(bytes) {
3418
3623
  return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "");
@@ -3424,27 +3629,6 @@ var FRONT_DESK_TRIGGER = "user_post";
3424
3629
  var OPS_CHANNELS = ["governance"];
3425
3630
  var FRONT_DESK_CHANNELS = [ASK_CHANNEL];
3426
3631
  var AlignmentsSchema = z16.object({ applied: z16.array(z16.string()).default([]) });
3427
- var FOLLOW_DECISIONS = "ops-readers-follow-decisions";
3428
- var THREAD_RECORDS = "threads-as-records";
3429
- var DIGEST_CURSORS = "digest-replaces-inbox";
3430
- var TASK_RETURNS = "task-returns-recorded";
3431
- var OPS_CHANNEL_RETIRED = "ops-channel-retired";
3432
- var DECISIONS_CHANNEL_RETIRED = "decisions-channel-retired";
3433
- var THREAD_CONVERSATIONS = "thread-conversations";
3434
- var ASKS_CHANNEL_OPENED = "asks-channel-opened";
3435
- var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
3436
- var CHANNEL_CONVERSATIONS = "channel-conversations";
3437
- var DASHBOARD_UPDATES = "dashboard-updates-granted";
3438
- var CRON_VERBS = "cron-verbs-granted";
3439
- var KNOWLEDGE_NOTICES = "knowledge-notices";
3440
- var IMAGE_UPLOADS = "image-uploads-granted";
3441
- var KnowledgeEventSchema = z16.object({
3442
- topic: NameSchema,
3443
- project: NameSchema.nullable(),
3444
- replaced: z16.boolean().optional()
3445
- });
3446
- var CRON_VERB_NAMES = ["create_cron", "update_cron", "remove_cron"];
3447
- var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
3448
3632
  var DASHBOARD_LINE = "The project's members keep this page current with update_dashboard.";
3449
3633
  var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
3450
3634
  var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
@@ -3464,9 +3648,9 @@ function sentence(summary) {
3464
3648
  return summary.trim().replace(/\.+$/, "");
3465
3649
  }
3466
3650
  function skillSummary(data, body) {
3467
- const fromFrontmatter = data["description"] ?? data["summary"];
3468
- if (typeof fromFrontmatter === "string" && fromFrontmatter.trim().length > 0) {
3469
- return fromFrontmatter.trim();
3651
+ const description = data["description"];
3652
+ if (typeof description === "string" && description.trim().length > 0) {
3653
+ return description.trim();
3470
3654
  }
3471
3655
  return body.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#")) ?? "";
3472
3656
  }
@@ -3508,8 +3692,8 @@ function restage(existing, edit) {
3508
3692
  };
3509
3693
  }
3510
3694
  function reopenStage(stage) {
3511
- const { completedBy: _by, completedAt: _at, ...open3 } = stage;
3512
- return open3;
3695
+ const { completedBy: _by, completedAt: _at, ...open4 } = stage;
3696
+ return open4;
3513
3697
  }
3514
3698
  function gateChanges(before, after) {
3515
3699
  const changes = [];
@@ -3590,7 +3774,7 @@ var Board = class _Board {
3590
3774
  homes;
3591
3775
  constructor(dataDir, options) {
3592
3776
  this.paths = new BoardPaths(dataDir);
3593
- this.homes = new HomeRepos(path6.join(this.paths.state(), "home-hooks"));
3777
+ this.homes = new HomeRepos(path7.join(this.paths.state(), "home-hooks"));
3594
3778
  this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
3595
3779
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
3596
3780
  this.events = new EventLog(this.paths.eventLog(), () => this.newId(), this.now);
@@ -3619,368 +3803,22 @@ var Board = class _Board {
3619
3803
  return board;
3620
3804
  }
3621
3805
  /**
3622
- * Applies each one-time alignment an existing society has not had yet. Once applied it is not
3623
- * repeated, so a member that later chooses otherwise keeps its choice.
3806
+ * Applies each one-time alignment an existing society has not had yet, and remembers it, so a
3807
+ * member that later chooses otherwise keeps its choice. A new alignment is a name and a method in
3808
+ * this list; once every board has applied it, both are deleted. None is pending.
3624
3809
  */
3625
3810
  async applyAlignments() {
3626
- const file = path6.join(this.paths.state(), "alignments.json");
3811
+ const alignments = [];
3812
+ const file = path7.join(this.paths.state(), "alignments.json");
3627
3813
  const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
3628
- const pending = [
3629
- FOLLOW_DECISIONS,
3630
- THREAD_RECORDS,
3631
- DIGEST_CURSORS,
3632
- TASK_RETURNS,
3633
- OPS_CHANNEL_RETIRED,
3634
- DECISIONS_CHANNEL_RETIRED,
3635
- THREAD_CONVERSATIONS,
3636
- ASKS_CHANNEL_OPENED,
3637
- KNOWLEDGE_REMOVAL,
3638
- CHANNEL_CONVERSATIONS,
3639
- DASHBOARD_UPDATES,
3640
- CRON_VERBS,
3641
- KNOWLEDGE_NOTICES,
3642
- IMAGE_UPLOADS
3643
- ].filter((name) => !applied.includes(name));
3814
+ const pending = alignments.filter(({ name }) => !applied.includes(name));
3644
3815
  if (pending.length === 0) {
3645
3816
  return;
3646
3817
  }
3647
- if (pending.includes(FOLLOW_DECISIONS)) {
3648
- await this.followDecisions();
3649
- }
3650
- if (pending.includes(THREAD_RECORDS)) {
3651
- await this.recordTaskThreads();
3652
- }
3653
- if (pending.includes(DIGEST_CURSORS)) {
3654
- await this.renameInboxCursors();
3655
- }
3656
- if (pending.includes(TASK_RETURNS)) {
3657
- await this.recordTaskReturns();
3818
+ for (const { apply } of pending) {
3819
+ await apply();
3658
3820
  }
3659
- if (pending.includes(OPS_CHANNEL_RETIRED)) {
3660
- await this.retireSocietyChannel("ops");
3661
- }
3662
- if (pending.includes(DECISIONS_CHANNEL_RETIRED)) {
3663
- await this.retireSocietyChannel("decisions");
3664
- }
3665
- if (pending.includes(THREAD_CONVERSATIONS)) {
3666
- await this.startThreadCursors();
3667
- }
3668
- if (pending.includes(ASKS_CHANNEL_OPENED)) {
3669
- await this.openAsksChannel();
3670
- }
3671
- if (pending.includes(KNOWLEDGE_REMOVAL)) {
3672
- await this.grantBeside("write_knowledge", "remove_knowledge");
3673
- }
3674
- if (pending.includes(CHANNEL_CONVERSATIONS)) {
3675
- await this.startChannelCursors();
3676
- }
3677
- if (pending.includes(DASHBOARD_UPDATES)) {
3678
- await this.grantBeside("write_knowledge", "update_dashboard");
3679
- await this.rewordDashboards();
3680
- }
3681
- if (pending.includes(CRON_VERBS)) {
3682
- await this.grantToEveryRole(CRON_VERB_NAMES);
3683
- }
3684
- if (pending.includes(KNOWLEDGE_NOTICES)) {
3685
- await this.noticeKnowledgeNotes();
3686
- }
3687
- if (pending.includes(IMAGE_UPLOADS)) {
3688
- await this.grantBeside("post_message", "upload_image");
3689
- }
3690
- await writeJson(file, { applied: [...applied, ...pending] });
3691
- }
3692
- /**
3693
- * Gives each knowledge note from before notes carried their notice the notice it would carry
3694
- * now. The event log, not the note's words, says which post is one: a knowledge verb appends
3695
- * its event and then its note under the mutex, so a note is the post right after such an event,
3696
- * by the same actor, at the top of the general channel of the topic's place. A post written to
3697
- * look like a note follows no such event and is left as it is.
3698
- */
3699
- async noticeKnowledgeNotes() {
3700
- let before;
3701
- for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
3702
- const verb = before;
3703
- before = event;
3704
- if (event.type !== "message.posted" || verb === void 0 || verb.actor !== event.actor || verb.type !== "knowledge.written" && verb.type !== "knowledge.removed") {
3705
- continue;
3706
- }
3707
- const knowledge2 = KnowledgeEventSchema.safeParse(verb.payload);
3708
- const id = UlidSchema.safeParse(event.payload["id"]);
3709
- if (!knowledge2.success || !id.success || (event.payload["thread"] ?? null) !== null || event.payload["channel"] !== channelRef(knowledge2.data.project, "general")) {
3710
- continue;
3711
- }
3712
- const { topic, project: project2, replaced } = knowledge2.data;
3713
- const file = this.paths.messageFile(this.paths.channelDir(channelRef(project2, "general")), id.data, event.actor);
3714
- if (!await exists(file)) {
3715
- continue;
3716
- }
3717
- const { data, body, error } = await readLooseMarkdown(file);
3718
- if (error !== null || data["notice"] !== void 0) {
3719
- continue;
3720
- }
3721
- const notice = {
3722
- kind: "knowledge",
3723
- action: verb.type === "knowledge.removed" ? "removed" : replaced === true ? "updated" : "written",
3724
- topic,
3725
- project: project2
3726
- };
3727
- await writeMarkdown(file, { ...data, notice }, body);
3728
- }
3729
- }
3730
- /**
3731
- * Grants `verb` once to every charter granting `beside`, the work roles' charters in practice;
3732
- * seed roles have it from their seed.
3733
- */
3734
- async grantBeside(beside, verb) {
3735
- for (const charter of await this.listRoles()) {
3736
- if (charter.verbs.includes(beside) && !charter.verbs.includes(verb)) {
3737
- await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
3738
- }
3739
- }
3740
- }
3741
- /** Grants verbs once to every charter but the user's, which holds every verb already. */
3742
- async grantToEveryRole(verbs) {
3743
- for (const charter of await this.listRoles()) {
3744
- const missing = verbs.filter((verb) => !charter.verbs.includes(verb));
3745
- if (charter.name !== USER_ROLE && missing.length > 0) {
3746
- await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, ...missing] }, { verbsAdded: missing });
3747
- }
3748
- }
3749
- }
3750
- /** A dashboard still saying agents may edit its file says how it changes now. */
3751
- async rewordDashboards() {
3752
- for (const slug of await listDirs(this.paths.projects())) {
3753
- const file = this.paths.dashboard(slug);
3754
- if (!await exists(file)) {
3755
- continue;
3756
- }
3757
- const { data, body } = await readLooseMarkdown(file);
3758
- if (body.includes(OLD_DASHBOARD_LINE)) {
3759
- await writeMarkdown(file, data, body.replace(OLD_DASHBOARD_LINE, DASHBOARD_LINE));
3760
- }
3761
- }
3762
- }
3763
- /**
3764
- * A task sent back before tasks recorded it kept no trace outside the event log. A task still
3765
- * short of the stage its latest `task.moved` came from is in that rework, and records it.
3766
- */
3767
- async recordTaskReturns() {
3768
- const moves = /* @__PURE__ */ new Map();
3769
- for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
3770
- const taskId = event.payload["taskId"];
3771
- if (event.type === "task.moved" && typeof taskId === "string") {
3772
- moves.set(taskId, event);
3773
- }
3774
- }
3775
- for (const slug of await listDirs(this.paths.projects())) {
3776
- for (const task2 of await this.listTasks(slug)) {
3777
- const move = moves.get(task2.id);
3778
- const from = move?.payload["from"];
3779
- if (move === void 0 || typeof from !== "string" || task2.returned !== void 0 || task2.completing || task2.status !== "open" && task2.status !== "claimed") {
3780
- continue;
3781
- }
3782
- const back = stageIndex(task2, from);
3783
- if (back > stageIndex(task2, task2.stage)) {
3784
- await this.writeTask(slug, {
3785
- ...task2,
3786
- returned: { from, by: move.actor, at: move.ts }
3787
- });
3788
- }
3789
- }
3790
- }
3791
- }
3792
- /**
3793
- * A society channel the board no longer uses leaves the society's list and every member's
3794
- * subscriptions; its old posts stay on disk. `ops` carried operations signals, which are only
3795
- * logged now; `decisions` carried proposals' outcomes, which are posts in their threads now, and
3796
- * requests to the user, which are mentions of the user wherever the question belongs.
3797
- */
3798
- async retireSocietyChannel(name) {
3799
- const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
3800
- if (doc.data.channels.includes(name)) {
3801
- await writeMarkdown(this.paths.societyFile(), { ...doc.data, channels: doc.data.channels.filter((channel) => channel !== name) }, doc.body);
3802
- }
3803
- for (const agent2 of await this.listAgents()) {
3804
- if (agent2.subscriptions.includes(name)) {
3805
- await this.updateAgent(agent2.name, (a) => ({
3806
- ...a,
3807
- subscriptions: a.subscriptions.filter((ref) => ref !== name)
3808
- }));
3809
- await this.refreshMember(agent2.name);
3810
- }
3811
- }
3812
- }
3813
- /**
3814
- * Asks once opened their threads in general, where closing one posted its summary to every
3815
- * citizen and woke the steward's heartbeat for it. The society gains the asks channel, and the
3816
- * front desk follows it.
3817
- */
3818
- async openAsksChannel() {
3819
- const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
3820
- if (!doc.data.channels.includes(ASK_CHANNEL)) {
3821
- await writeMarkdown(this.paths.societyFile(), { ...doc.data, channels: [...doc.data.channels, ASK_CHANNEL] }, doc.body);
3822
- }
3823
- await ensureDir(this.paths.societyChannel(ASK_CHANNEL));
3824
- for (const agent2 of await this.listAgents()) {
3825
- if (agent2.status !== "active" || agent2.subscriptions.includes(ASK_CHANNEL)) {
3826
- continue;
3827
- }
3828
- if (!(await this.readRole(agent2.role)).wakeTriggers.includes(FRONT_DESK_TRIGGER)) {
3829
- continue;
3830
- }
3831
- await this.updateAgent(agent2.name, (a) => ({
3832
- ...a,
3833
- subscriptions: [...a.subscriptions, ASK_CHANNEL]
3834
- }));
3835
- await this.refreshMember(agent2.name);
3836
- await this.events.append("subscription.changed", agent2.name, {
3837
- channel: ASK_CHANNEL,
3838
- subscribed: true,
3839
- aligned: true
3840
- });
3841
- }
3842
- }
3843
- /** Members that predate `decisions` among the ops channels read proposals without their outcome. */
3844
- async followDecisions() {
3845
- if (!(await this.society()).channels.includes("decisions")) {
3846
- return;
3847
- }
3848
- for (const agent2 of await this.listAgents()) {
3849
- if (agent2.status !== "active" || agent2.subscriptions.includes("decisions")) {
3850
- continue;
3851
- }
3852
- if (!(await this.readRole(agent2.role)).wakeTriggers.includes(OPS_WAKE_TRIGGER)) {
3853
- continue;
3854
- }
3855
- await this.updateAgent(agent2.name, (a) => ({
3856
- ...a,
3857
- subscriptions: [...a.subscriptions, "decisions"]
3858
- }));
3859
- await this.refreshMember(agent2.name);
3860
- await this.events.append("subscription.changed", agent2.name, {
3861
- channel: "decisions",
3862
- subscribed: true,
3863
- aligned: true
3864
- });
3865
- }
3866
- }
3867
- /**
3868
- * Threads were once a state field on their task. Each becomes a record under the task's project,
3869
- * on its general channel, closed if its task has ended, and the field leaves the task file.
3870
- */
3871
- async recordTaskThreads() {
3872
- const opened = /* @__PURE__ */ new Map();
3873
- const closed = /* @__PURE__ */ new Map();
3874
- for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
3875
- const taskId = event.payload["taskId"];
3876
- if (typeof taskId !== "string") {
3877
- continue;
3878
- }
3879
- if (event.type === "thread.opened") {
3880
- opened.set(taskId, event);
3881
- } else if (event.type === "thread.closed") {
3882
- closed.set(taskId, event);
3883
- }
3884
- }
3885
- for (const slug of await listDirs(this.paths.projects())) {
3886
- const threads = this.paths.threads(slug);
3887
- const summaries = /* @__PURE__ */ new Map();
3888
- for (const message of await this.readMessagesIn(this.paths.projectChannel(slug, "general"), null)) {
3889
- if (message.task !== void 0) {
3890
- summaries.set(message.task, message.body.replace(/^Thread closed[^\n]*\n\n/, ""));
3891
- }
3892
- }
3893
- for (const file of await listFiles(this.paths.tasks(slug))) {
3894
- const raw = await readMarkdown(path6.join(this.paths.tasks(slug), file), z16.record(z16.string(), z16.unknown()));
3895
- const legacy = raw.data["thread"];
3896
- if (legacy === void 0) {
3897
- continue;
3898
- }
3899
- const task2 = { ...TaskFrontmatterSchema.parse(raw.data), body: raw.body };
3900
- if ((legacy === "open" || legacy === "closed") && !await exists(this.paths.threadFile(threads, task2.id))) {
3901
- const ended = task2.status === "done" || task2.status === "abandoned";
3902
- const open3 = legacy === "open" && !ended;
3903
- const openedBy = opened.get(task2.id);
3904
- const closedBy = closed.get(task2.id);
3905
- await this.writeThread(threads, {
3906
- id: task2.id,
3907
- channel: channelRef(slug, "general"),
3908
- title: task2.title,
3909
- subject: { kind: "task", id: task2.id },
3910
- state: open3 ? "open" : "closed",
3911
- openedBy: openedBy?.actor ?? task2.createdBy,
3912
- openedAt: openedBy?.ts ?? task2.createdAt,
3913
- ...open3 ? {} : {
3914
- closedBy: closedBy?.actor ?? SYSTEM_ACTOR.name,
3915
- closedAt: closedBy?.ts ?? this.now().toISOString()
3916
- },
3917
- body: summaries.get(task2.id) ?? ""
3918
- });
3919
- }
3920
- await this.writeTask(slug, task2);
3921
- }
3922
- }
3923
- }
3924
- /**
3925
- * Every channel of a scope was once read in its home conversation, so a member had read each up
3926
- * to its scope's cursor. Each channel beside general starts from that cursor now, as each takes a
3927
- * conversation of its own.
3928
- */
3929
- async startChannelCursors() {
3930
- for (const agent2 of await this.listAgents()) {
3931
- const file = this.paths.agentCursors(agent2.name);
3932
- if (!await exists(file)) {
3933
- continue;
3934
- }
3935
- const cursors = await readJson(file, CursorsSchema);
3936
- const scopes = /* @__PURE__ */ new Set([
3937
- ...Object.keys(cursors.scopes).filter((key) => !key.includes("/")),
3938
- ...agent2.memberships,
3939
- SOCIETY_SCOPE
3940
- ]);
3941
- const channelsFrom = Object.fromEntries([...scopes].map((scope) => [scope, cursorOf(cursors, scope)]));
3942
- await writeJson(file, { ...cursors, channelsFrom });
3943
- }
3944
- }
3945
- /**
3946
- * Threads were once read in the scope's single conversation, so a member had read every thread
3947
- * up to its scope's cursor. Each scope's threads start from that cursor now, as each thread's
3948
- * conversation takes a cursor of its own.
3949
- */
3950
- async startThreadCursors() {
3951
- for (const agent2 of await this.listAgents()) {
3952
- const file = this.paths.agentCursors(agent2.name);
3953
- if (!await exists(file)) {
3954
- continue;
3955
- }
3956
- const cursors = await readJson(file, CursorsSchema);
3957
- const scopes = /* @__PURE__ */ new Set([
3958
- ...Object.keys(cursors.scopes).filter((key) => !key.includes("/")),
3959
- ...agent2.memberships,
3960
- SOCIETY_SCOPE
3961
- ]);
3962
- const threadsFrom = Object.fromEntries([...scopes].map((scope) => [scope, cursorOf(cursors, scope)]));
3963
- await writeJson(file, { ...cursors, threadsFrom });
3964
- }
3965
- }
3966
- /**
3967
- * The digest was once called the inbox: each cursor file keyed its cursor `inbox`, and the user
3968
- * followed every society channel and project general channel to read one. The user takes no
3969
- * turns, so it follows nothing now.
3970
- */
3971
- async renameInboxCursors() {
3972
- const Legacy = z16.object({ inbox: z16.string().nullable() });
3973
- for (const agent2 of await this.listAgents()) {
3974
- const file = this.paths.agentCursors(agent2.name);
3975
- if (await exists(file)) {
3976
- const legacy = Legacy.safeParse(JSON.parse(await readFile4(file, "utf8")));
3977
- if (legacy.success) {
3978
- await writeJson(file, { digest: legacy.data.inbox });
3979
- }
3980
- }
3981
- }
3982
- await this.updateAgent(USER_NAME, (user2) => ({ ...user2, subscriptions: [] }));
3983
- await this.refreshMember(USER_NAME);
3821
+ await writeJson(file, { applied: [...applied, ...pending.map(({ name }) => name)] });
3984
3822
  }
3985
3823
  /** Writes every missing seed charter and aligns the existing ones with their seed. */
3986
3824
  async ensureSeedRoles() {
@@ -4082,13 +3920,15 @@ ${charter.purpose}
4082
3920
  const token = mintToken();
4083
3921
  const thread2 = threadOfPart(part);
4084
3922
  const channel = channelOfPart(part);
3923
+ const place = placeOfPart(part);
4085
3924
  this.turnTokens.set(hashToken(token), {
4086
3925
  actor: {
4087
3926
  name: agent2,
4088
3927
  role: role2,
4089
3928
  ...scope === void 0 ? {} : { scope },
4090
3929
  ...thread2 === void 0 ? {} : { thread: thread2 },
4091
- ...channel === void 0 ? {} : { channel }
3930
+ ...channel === void 0 ? {} : { channel },
3931
+ ...place === void 0 ? {} : { place }
4092
3932
  },
4093
3933
  expiresAt: this.now().getTime() + ttlMs
4094
3934
  });
@@ -4177,7 +4017,7 @@ ${charter.purpose}
4177
4017
  });
4178
4018
  }
4179
4019
  signInsFile() {
4180
- return path6.join(this.paths.state(), "sign-ins.json");
4020
+ return path7.join(this.paths.state(), "sign-ins.json");
4181
4021
  }
4182
4022
  async readSignIns() {
4183
4023
  const file = this.signInsFile();
@@ -4274,7 +4114,7 @@ ${charter.purpose}
4274
4114
  const files = await listFiles(this.paths.tasks(project2));
4275
4115
  const tasks = [];
4276
4116
  for (const file of files) {
4277
- const doc = await readMarkdown(path6.join(this.paths.tasks(project2), file), TaskFrontmatterSchema);
4117
+ const doc = await readMarkdown(path7.join(this.paths.tasks(project2), file), TaskFrontmatterSchema);
4278
4118
  tasks.push({ ...doc.data, body: doc.body });
4279
4119
  }
4280
4120
  return tasks;
@@ -4293,7 +4133,7 @@ ${charter.purpose}
4293
4133
  const files = await listFiles(this.paths.proposals());
4294
4134
  const proposals = [];
4295
4135
  for (const file of files) {
4296
- const doc = await readMarkdown(path6.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
4136
+ const doc = await readMarkdown(path7.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
4297
4137
  proposals.push({ ...doc.data, body: doc.body });
4298
4138
  }
4299
4139
  return proposals;
@@ -4320,7 +4160,7 @@ ${charter.purpose}
4320
4160
  async listMembers() {
4321
4161
  const members = [];
4322
4162
  for (const file of await listFiles(this.paths.members())) {
4323
- const doc = await readMarkdown(path6.join(this.paths.members(), file), MemberSchema);
4163
+ const doc = await readMarkdown(path7.join(this.paths.members(), file), MemberSchema);
4324
4164
  members.push({ ...doc.data, profile: doc.body });
4325
4165
  }
4326
4166
  return members;
@@ -4339,7 +4179,7 @@ ${charter.purpose}
4339
4179
  const dir = project2 === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project2);
4340
4180
  const topics = [];
4341
4181
  for (const file of await listFiles(dir)) {
4342
- topics.push(await this.knowledgeAt(path6.join(dir, file), project2, file.replace(/\.md$/, "")));
4182
+ topics.push(await this.knowledgeAt(path7.join(dir, file), project2, file.replace(/\.md$/, "")));
4343
4183
  }
4344
4184
  return topics;
4345
4185
  }
@@ -4369,7 +4209,7 @@ ${charter.purpose}
4369
4209
  const aside = this.paths.removedKnowledge(project2 ?? SOCIETY_SCOPE);
4370
4210
  let highest = 0;
4371
4211
  for (const file of (await listFiles(aside)).filter((name) => name.startsWith(`${topic}.`))) {
4372
- const copy = await this.knowledgeAt(path6.join(aside, file), project2, topic);
4212
+ const copy = await this.knowledgeAt(path7.join(aside, file), project2, topic);
4373
4213
  highest = Math.max(highest, copy.revision);
4374
4214
  }
4375
4215
  return highest;
@@ -4385,7 +4225,7 @@ ${charter.purpose}
4385
4225
  async listSkillsIn(dir, scope) {
4386
4226
  const skills = [];
4387
4227
  for (const entry of await listDirs(dir)) {
4388
- const file = path6.join(dir, entry, "SKILL.md");
4228
+ const file = path7.join(dir, entry, "SKILL.md");
4389
4229
  if (!await exists(file)) {
4390
4230
  continue;
4391
4231
  }
@@ -4475,7 +4315,7 @@ ${charter.purpose}
4475
4315
  if (!wanted.delete(file.slice(0, 26))) {
4476
4316
  continue;
4477
4317
  }
4478
- const { data } = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
4318
+ const { data } = await readMarkdown(path7.join(dir, file), MessageFrontmatterSchema);
4479
4319
  found.push(MessageRefSchema.parse(data));
4480
4320
  }
4481
4321
  if (wanted.size === 0) {
@@ -4507,7 +4347,7 @@ ${charter.purpose}
4507
4347
  if (!file.endsWith(".md")) {
4508
4348
  continue;
4509
4349
  }
4510
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
4350
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
4511
4351
  if (doc.data.state !== "open") {
4512
4352
  continue;
4513
4353
  }
@@ -4534,7 +4374,7 @@ ${charter.purpose}
4534
4374
  const all = [];
4535
4375
  for (const threads of await this.threadScopes()) {
4536
4376
  for (const file of await listFiles(threads)) {
4537
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
4377
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
4538
4378
  const messages = await listFiles(this.paths.threadMessages(threads, doc.data.id));
4539
4379
  const last = messages.at(-1);
4540
4380
  all.push({
@@ -4782,7 +4622,7 @@ ${DASHBOARD_LINE}
4782
4622
  if (event.type === "turn.started") {
4783
4623
  const start = TurnStartPayloadSchema.safeParse(event.payload);
4784
4624
  if (start.success)
4785
- starts.set(start.data.turnId ?? start.data.project, event.ts);
4625
+ starts.set(start.data.turnId, event.ts);
4786
4626
  continue;
4787
4627
  }
4788
4628
  if (event.type !== "turn.completed" && event.type !== "turn.failed") {
@@ -4790,9 +4630,11 @@ ${DASHBOARD_LINE}
4790
4630
  }
4791
4631
  const parsed = TurnHistoryPayloadSchema.safeParse(event.payload);
4792
4632
  if (parsed.success) {
4793
- const key = parsed.data.turnId ?? parsed.data.project;
4794
- const startedAt = starts.get(key);
4795
- starts.delete(key);
4633
+ const { turnId } = parsed.data;
4634
+ const startedAt = turnId === void 0 ? void 0 : starts.get(turnId);
4635
+ if (turnId !== void 0) {
4636
+ starts.delete(turnId);
4637
+ }
4796
4638
  entries.push({
4797
4639
  id: event.id,
4798
4640
  ts: event.ts,
@@ -4901,16 +4743,20 @@ ${DASHBOARD_LINE}
4901
4743
  }
4902
4744
  /**
4903
4745
  * How many unread digest messages a member has in each conversation: a scope's home, for its
4904
- * general channel, one of its other channels, or one of its threads. The heartbeat asks, so that it wakes a member where its unread
4746
+ * general channel, one of its other channels, one of its threads, or, in the society scope, a
4747
+ * project's channels. The heartbeat asks, so that it wakes a member where its unread
4905
4748
  * messages are and nowhere else. Society channels count only for a member woken in the society
4906
4749
  * scope; a project member reads them at its next turn. Two kinds of post are in the digest but not
4907
4750
  * counted, and are read when the member wakes for something else: a task step's, which wakes
4908
4751
  * whoever it hands work to, and the board's own, operations signals and landing announcements,
4909
- * whose signals that call for action wake their readers directly.
4752
+ * whose signals that call for action wake their readers directly. A closed thread counts for
4753
+ * nothing: its conversation has ended, and a turn there could only read its posts and be refused
4754
+ * a reply, while a wake queued before it closed, as for a mention, still runs there.
4910
4755
  */
4911
4756
  async unreadByConversation(actor) {
4912
4757
  const { cursors, upto } = await this.digestSnapshot(actor.name);
4913
4758
  const counts = /* @__PURE__ */ new Map();
4759
+ const closed = /* @__PURE__ */ new Map();
4914
4760
  const unscoped = { name: actor.name, role: actor.role };
4915
4761
  for (const { scope, part, message, news } of await this.digestSince(unscoped, cursors, upto)) {
4916
4762
  if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
@@ -4920,11 +4766,23 @@ ${DASHBOARD_LINE}
4920
4766
  if (posted !== scope && scope !== SOCIETY_SCOPE) {
4921
4767
  continue;
4922
4768
  }
4769
+ const thread2 = threadOfPart(part);
4770
+ if (thread2 !== void 0) {
4771
+ let ended = closed.get(thread2);
4772
+ if (ended === void 0) {
4773
+ ended = (await this.tryFindThread(thread2))?.thread.state === "closed";
4774
+ closed.set(thread2, ended);
4775
+ }
4776
+ if (ended) {
4777
+ continue;
4778
+ }
4779
+ }
4923
4780
  const key = conversationKey(scope, part);
4924
4781
  const entry = counts.get(key) ?? {
4925
4782
  scope,
4926
- thread: threadOfPart(part) ?? null,
4783
+ thread: thread2 ?? null,
4927
4784
  channel: channelOfPart(part) ?? null,
4785
+ place: placeOfPart(part) ?? null,
4928
4786
  count: 0
4929
4787
  };
4930
4788
  counts.set(key, { ...entry, count: entry.count + 1 });
@@ -4982,8 +4840,9 @@ ${DASHBOARD_LINE}
4982
4840
  * thread it takes part in, and for the front desk, which every post by the user wakes, every post
4983
4841
  * by the user wherever it is. With a conversation, only what is filed there: for a thread, every
4984
4842
  * message of the thread, since the conversation is about it; for a channel's, that channel's
4985
- * top-level messages; for a scope's home, general's and those of a project the member is not in,
4986
- * since each thread and each other channel is a conversation of its own. Nothing newer than
4843
+ * top-level messages; for a project's in the society scope, the top-level messages of every
4844
+ * channel of a project the member is not in; for a scope's home, general's, since each thread,
4845
+ * each other channel, and each such project is a conversation of its own. Nothing newer than
4987
4846
  * `upto` counts (see `digestSnapshot`).
4988
4847
  */
4989
4848
  async digestSince(actor, cursors, upto, conversation) {
@@ -5003,11 +4862,17 @@ ${DASHBOARD_LINE}
5003
4862
  const frontDesk = charter.wakeTriggers.includes(FRONT_DESK_TRIGGER);
5004
4863
  const threadParticipation = /* @__PURE__ */ new Map();
5005
4864
  const openChannels = /* @__PURE__ */ new Map();
4865
+ const liveProjects = /* @__PURE__ */ new Map();
5006
4866
  const collected = [];
5007
4867
  const newsFrom = charter.societyScope ? void 0 : newsStart(cursors);
5008
4868
  const scopes = conversation !== void 0 ? [conversation.scope] : [...agent2.memberships, SOCIETY_SCOPE];
5009
4869
  const starts = [
5010
- ...scopes.flatMap((each) => conversation === void 0 ? [cursorOf(cursors, each), threadStart(cursors, each), channelStart(cursors, each)] : [cursorOf(cursors, each, conversation.part)]),
4870
+ ...scopes.flatMap((each) => conversation === void 0 ? [
4871
+ cursorOf(cursors, each),
4872
+ cursors.digest,
4873
+ channelStart(cursors, each),
4874
+ placeStart(cursors, each)
4875
+ ] : [cursorOf(cursors, each, conversation.part)]),
5011
4876
  ...newsFrom === void 0 ? [] : [newsFrom]
5012
4877
  ];
5013
4878
  const known = starts.filter((start) => start !== null);
@@ -5017,7 +4882,7 @@ ${DASHBOARD_LINE}
5017
4882
  const thread2 = message.thread ?? null;
5018
4883
  const asked = message.mentions.includes(actor.name) || frontDesk && message.author === USER_NAME;
5019
4884
  if (newsFrom !== void 0 && thread2 === null && filedIn === SOCIETY_SCOPE && !asked) {
5020
- if ((newsFrom === null || message.id > newsFrom) && subscribed.has(message.channel) && message.author !== actor.name) {
4885
+ if ((conversation === void 0 || conversation.part === null) && (newsFrom === null || message.id > newsFrom) && subscribed.has(message.channel) && message.author !== actor.name) {
5021
4886
  collected.push({
5022
4887
  scope: conversation?.scope ?? SOCIETY_SCOPE,
5023
4888
  part: null,
@@ -5028,12 +4893,18 @@ ${DASHBOARD_LINE}
5028
4893
  continue;
5029
4894
  }
5030
4895
  const named = thread2 === null ? channelConversation(filedIn, message.channel) : void 0;
5031
- let open3 = named === void 0 ? void 0 : openChannels.get(message.channel);
5032
- if (named !== void 0 && open3 === void 0) {
5033
- open3 = await this.channelOpen(message.channel);
5034
- openChannels.set(message.channel, open3);
5035
- }
5036
- const part = thread2 ?? (open3 === true ? `#${named}` : null);
4896
+ let open4 = named === void 0 ? void 0 : openChannels.get(message.channel);
4897
+ if (named !== void 0 && open4 === void 0) {
4898
+ open4 = await this.channelOpen(message.channel);
4899
+ openChannels.set(message.channel, open4);
4900
+ }
4901
+ const project2 = thread2 === null ? placeConversation(filedIn, message.channel) : void 0;
4902
+ let live = project2 === void 0 ? void 0 : liveProjects.get(project2);
4903
+ if (project2 !== void 0 && live === void 0) {
4904
+ live = await this.projectLive(project2);
4905
+ liveProjects.set(project2, live);
4906
+ }
4907
+ const part = thread2 ?? (open4 === true ? `#${named}` : live === true ? `~${project2}` : null);
5037
4908
  if (conversation !== void 0 && (filedIn !== conversation.scope || part !== conversation.part)) {
5038
4909
  continue;
5039
4910
  }
@@ -5098,7 +4969,7 @@ ${task2.body}`;
5098
4969
  }
5099
4970
  }
5100
4971
  for (const file of await listFiles(this.paths.projectKnowledge(project2))) {
5101
- const content = await readFile4(path6.join(this.paths.projectKnowledge(project2), file), "utf8");
4972
+ const content = await readFile4(path7.join(this.paths.projectKnowledge(project2), file), "utf8");
5102
4973
  if (content.toLowerCase().includes(needle)) {
5103
4974
  hits.push({
5104
4975
  kind: "knowledge",
@@ -5119,7 +4990,7 @@ ${task2.body}`;
5119
4990
  shared.push({
5120
4991
  kind: "knowledge",
5121
4992
  ref: `society/knowledge/${file}`,
5122
- file: path6.join(this.paths.societyKnowledge(), file)
4993
+ file: path7.join(this.paths.societyKnowledge(), file)
5123
4994
  });
5124
4995
  }
5125
4996
  for (const skill2 of await this.listSocietySkills()) {
@@ -5132,7 +5003,7 @@ ${task2.body}`;
5132
5003
  shared.push({
5133
5004
  kind: "memory",
5134
5005
  ref: `memory/${file}`,
5135
- file: path6.join(this.paths.agentMemory(actor.name), file)
5006
+ file: path7.join(this.paths.agentMemory(actor.name), file)
5136
5007
  });
5137
5008
  }
5138
5009
  for (const entry of shared) {
@@ -5222,7 +5093,7 @@ ${current.body}`);
5222
5093
  });
5223
5094
  const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
5224
5095
  await mkdir4(aside, { recursive: true });
5225
- await rename3(file, path6.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
5096
+ await rename3(file, path7.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
5226
5097
  await this.events.append("knowledge.removed", actor.name, {
5227
5098
  topic: args.topic,
5228
5099
  project: args.project
@@ -5270,7 +5141,7 @@ ${current.body}`);
5270
5141
  });
5271
5142
  const aside = this.paths.removedSkills();
5272
5143
  await mkdir4(aside, { recursive: true });
5273
- await rename3(this.paths.societySkill(args.name), path6.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
5144
+ await rename3(this.paths.societySkill(args.name), path7.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
5274
5145
  await this.events.append("skill.removed", actor.name, {
5275
5146
  name: args.name,
5276
5147
  reason: args.reason
@@ -5347,7 +5218,7 @@ ${current.body}`);
5347
5218
  type,
5348
5219
  size: bytes.byteLength
5349
5220
  });
5350
- const name = path6.basename(args.path).replaceAll(/[[\]\\\r\n]/g, "");
5221
+ const name = path7.basename(args.path).replaceAll(/[[\]\\\r\n]/g, "");
5351
5222
  return {
5352
5223
  id,
5353
5224
  type,
@@ -5373,7 +5244,7 @@ ${current.body}`);
5373
5244
  async listCrons() {
5374
5245
  const crons = [];
5375
5246
  for (const file of await listFiles(this.paths.crons())) {
5376
- crons.push(await this.readCronFile(path6.join(this.paths.crons(), file)));
5247
+ crons.push(await this.readCronFile(path7.join(this.paths.crons(), file)));
5377
5248
  }
5378
5249
  return crons;
5379
5250
  }
@@ -5495,11 +5366,16 @@ ${current.body}`);
5495
5366
  }
5496
5367
  /**
5497
5368
  * Where a cron fires for its citizen: the thread, the channel, or the scope's home it names, else
5498
- * the conversation of the turn that set it when it wakes its setter, else its home outside projects.
5369
+ * the conversation of the turn that set it when it wakes its setter, a project's channels' in
5370
+ * the society scope included, else its home outside projects.
5499
5371
  */
5500
5372
  async cronConversation(actor, agent2, named) {
5501
5373
  const own = actor.name === agent2.name;
5502
5374
  if (named.project === void 0 && named.thread_id === void 0 && named.channel === void 0) {
5375
+ if (own && actor.scope === SOCIETY_SCOPE && actor.place !== void 0) {
5376
+ await this.readActiveProject(actor.place);
5377
+ return { scope: SOCIETY_SCOPE, place: actor.place };
5378
+ }
5503
5379
  return own && actor.scope !== void 0 ? this.cronConversation(actor, agent2, {
5504
5380
  project: actor.scope,
5505
5381
  thread_id: actor.thread,
@@ -5575,7 +5451,8 @@ ${current.body}`);
5575
5451
  agent: cron2.agent,
5576
5452
  project: cron2.scope,
5577
5453
  ...cron2.thread === void 0 ? {} : { thread: cron2.thread },
5578
- ...cron2.channel === void 0 ? {} : { channel: cron2.channel }
5454
+ ...cron2.channel === void 0 ? {} : { channel: cron2.channel },
5455
+ ...cron2.place === void 0 ? {} : { place: cron2.place }
5579
5456
  };
5580
5457
  }
5581
5458
  async readCronFile(file) {
@@ -5608,11 +5485,12 @@ ${current.body}`);
5608
5485
  const args = VerbInputs.open_thread.parse(input);
5609
5486
  await this.authorize(actor, "open_thread");
5610
5487
  return this.mutex.run(async () => {
5611
- const opening = await this.threadToOpen(args);
5612
- if (await this.tryFindThread(opening.id) !== null) {
5613
- throw new BoardError("INVALID_STATE", `${opening.subject?.kind ?? "thread"} ${opening.id} already has a thread`);
5614
- }
5615
- return this.openThreadUnlocked(actor.name, opening);
5488
+ await this.assertChannelOpen(args.channel);
5489
+ return this.openThreadUnlocked(actor.name, {
5490
+ id: this.newId(),
5491
+ channel: args.channel,
5492
+ title: args.title
5493
+ });
5616
5494
  });
5617
5495
  }
5618
5496
  /** Writes a new open thread and its event. Callers hold the mutex and chose where it hangs. */
@@ -5647,28 +5525,13 @@ ${current.body}`);
5647
5525
  subject: { kind: "proposal", id: proposal2.id }
5648
5526
  };
5649
5527
  }
5650
- /**
5651
- * The open thread of a proposal being decided. A proposal from before proposals opened their
5652
- * threads gets one now, opened as its proposer, and a thread closed before the decision opens
5653
- * again.
5654
- */
5655
- async proposalThreadUnlocked(by, proposal2) {
5528
+ /** The open thread of a proposal being decided, which `propose` opened with it. */
5529
+ async proposalThreadUnlocked(proposal2) {
5656
5530
  const found = await this.tryFindThread(proposal2.id);
5657
- if (found?.thread.state === "open") {
5658
- return found.thread;
5659
- }
5660
- if (found === null) {
5661
- return this.openThreadUnlocked(proposal2.proposedBy, this.proposalThreadOpening(proposal2));
5531
+ if (found?.thread.state !== "open") {
5532
+ throw new BoardError("INVALID_STATE", `proposal ${proposal2.id} has no open thread`);
5662
5533
  }
5663
- const { closedBy: _by, closedAt: _at, ...kept } = found.thread;
5664
- const reopened = await this.writeThread(found.threads, { ...kept, state: "open" });
5665
- await this.events.append("thread.opened", by, {
5666
- threadId: proposal2.id,
5667
- channel: reopened.channel,
5668
- subject: reopened.subject ?? null,
5669
- reopened: true
5670
- });
5671
- return reopened;
5534
+ return found.thread;
5672
5535
  }
5673
5536
  /** Where a task's thread hangs, what it is called, and what it is about. */
5674
5537
  taskThreadOpening(task2) {
@@ -5680,79 +5543,21 @@ ${current.body}`);
5680
5543
  };
5681
5544
  }
5682
5545
  /**
5683
- * The open thread of a task in play, for a verb about to post into it. A task from before tasks
5684
- * opened their threads gets one now, and a thread closed early while its task is in play opens
5685
- * again; an ended task's thread stays closed.
5546
+ * The open thread of a task in play, for a verb about to post into it, which `createTask` opened
5547
+ * with the task; an ended task's thread closed with it.
5686
5548
  */
5687
- async taskThreadUnlocked(by, task2) {
5549
+ async taskThreadUnlocked(task2) {
5688
5550
  const found = await this.tryFindThread(task2.id);
5689
5551
  if (found?.thread.state === "open") {
5690
5552
  return found.thread;
5691
5553
  }
5692
- if (task2.status === "done" || task2.status === "abandoned") {
5693
- throw new BoardError("INVALID_STATE", `task ${task2.id} is ${task2.status} and its thread closed with it`);
5694
- }
5695
- if (found === null) {
5696
- return this.openThreadUnlocked(by, this.taskThreadOpening(task2));
5697
- }
5698
- const { closedBy: _by, closedAt: _at, ...kept } = found.thread;
5699
- const reopened = await this.writeThread(found.threads, { ...kept, state: "open" });
5700
- await this.events.append("thread.opened", by, {
5701
- threadId: task2.id,
5702
- channel: reopened.channel,
5703
- subject: reopened.subject ?? null,
5704
- reopened: true
5705
- });
5706
- return reopened;
5554
+ throw new BoardError("INVALID_STATE", task2.status === "done" || task2.status === "abandoned" ? `task ${task2.id} is ${task2.status} and its thread closed with it` : `task ${task2.id} has no open thread`);
5707
5555
  }
5708
5556
  /** Posts what a task verb was told to say into the task's thread, as its author, with the step it recorded. */
5709
5557
  async postTaskNote(author, task2, body, step) {
5710
- const thread2 = await this.taskThreadUnlocked(author, task2);
5558
+ const thread2 = await this.taskThreadUnlocked(task2);
5711
5559
  return this.appendThreadMessage(author, thread2.id, body, step === void 0 ? {} : { step });
5712
5560
  }
5713
- /** Where a new thread hangs, what it is called, and what it is about. */
5714
- async threadToOpen(args) {
5715
- if (args.task_id !== void 0 && args.proposal_id !== void 0) {
5716
- throw new BoardError("VALIDATION", "a thread is about one task or one proposal, not both");
5717
- }
5718
- if (args.task_id !== void 0) {
5719
- const { task: task2, project: project2 } = await this.findTask(args.task_id);
5720
- if (task2.status === "done" || task2.status === "abandoned") {
5721
- throw new BoardError("INVALID_STATE", `task ${task2.id} is ${task2.status}`);
5722
- }
5723
- const channel = args.channel ?? channelRef(project2, task2.channel);
5724
- if (parseChannelRef(channel).project !== project2) {
5725
- throw new BoardError("VALIDATION", `a task's thread hangs off a channel of its project, ${project2}`);
5726
- }
5727
- await this.assertChannelOpen(channel);
5728
- return {
5729
- id: task2.id,
5730
- channel,
5731
- title: args.title ?? task2.title,
5732
- subject: { kind: "task", id: task2.id }
5733
- };
5734
- }
5735
- if (args.proposal_id !== void 0) {
5736
- const proposal2 = await this.readProposal(args.proposal_id);
5737
- if (proposal2.status !== "proposed") {
5738
- throw new BoardError("INVALID_STATE", `proposal ${proposal2.id} is already ${proposal2.status}`);
5739
- }
5740
- const channel = args.channel ?? "governance";
5741
- await this.assertChannelOpen(channel);
5742
- const described = `${proposal2.kind} proposal: ${describeCharter(proposal2.kind, proposal2.charter)}`;
5743
- return {
5744
- id: proposal2.id,
5745
- channel,
5746
- title: args.title ?? described.slice(0, 200),
5747
- subject: { kind: "proposal", id: proposal2.id }
5748
- };
5749
- }
5750
- if (args.channel === void 0 || args.title === void 0) {
5751
- throw new BoardError("VALIDATION", "a thread needs a task_id, a proposal_id, or a channel and a title");
5752
- }
5753
- await this.assertChannelOpen(args.channel);
5754
- return { id: this.newId(), channel: args.channel, title: args.title };
5755
- }
5756
5561
  /**
5757
5562
  * Where the board learns which turns are in flight, from the server that hosts them, so it
5758
5563
  * refuses to end a conversation under someone else's turn.
@@ -5786,17 +5591,20 @@ ${current.body}`);
5786
5591
  async openThreadIds(dir, keep) {
5787
5592
  const ids = /* @__PURE__ */ new Set();
5788
5593
  for (const file of await listFiles(dir)) {
5789
- const doc = await readMarkdown(path6.join(dir, file), ThreadFrontmatterSchema);
5594
+ const doc = await readMarkdown(path7.join(dir, file), ThreadFrontmatterSchema);
5790
5595
  if (doc.data.state === "open" && keep({ ...doc.data, body: doc.body })) {
5791
5596
  ids.add(doc.data.id);
5792
5597
  }
5793
5598
  }
5794
5599
  return ids;
5795
5600
  }
5796
- /** Refuses to archive a project while a turn runs in it or in a thread its archive closes. */
5601
+ /**
5602
+ * Refuses to archive a project while a turn runs in it, in a citizen's conversation of its
5603
+ * channels in the society scope, or in a thread its archive closes.
5604
+ */
5797
5605
  async assertProjectQuiet(actor, slug) {
5798
5606
  const threads = await this.openThreadIds(this.paths.threads(slug), () => true);
5799
- this.assertNoTurnCut(actor, `project ${slug} cannot be archived`, (turn2) => turn2.scope === slug || turn2.thread !== void 0 && threads.has(turn2.thread));
5607
+ this.assertNoTurnCut(actor, `project ${slug} cannot be archived`, (turn2) => turn2.scope === slug || turn2.place === slug || turn2.thread !== void 0 && threads.has(turn2.thread));
5800
5608
  }
5801
5609
  async closeThread(actor, input) {
5802
5610
  const args = VerbInputs.close_thread.parse(input);
@@ -6337,7 +6145,8 @@ ${args.summary}`);
6337
6145
  filedFrom: {
6338
6146
  scope: actor.scope,
6339
6147
  ...actor.thread === void 0 ? {} : { thread: actor.thread },
6340
- ...actor.channel === void 0 ? {} : { channel: actor.channel }
6148
+ ...actor.channel === void 0 ? {} : { channel: actor.channel },
6149
+ ...actor.place === void 0 ? {} : { place: actor.place }
6341
6150
  }
6342
6151
  },
6343
6152
  charter: parsed.data
@@ -6749,19 +6558,20 @@ ${rationale}`}`);
6749
6558
  project: input.project,
6750
6559
  ...input.thread === void 0 ? {} : { thread: input.thread },
6751
6560
  ...input.channel === void 0 ? {} : { channel: input.channel },
6561
+ ...input.place === void 0 ? {} : { place: input.place },
6752
6562
  messages: [...input.messages]
6753
6563
  });
6754
6564
  });
6755
6565
  }
6756
6566
  /**
6757
6567
  * Where a conversation's session ids and last turn are kept: the scope's home, or beside it a
6758
- * thread's or a channel's, named by `part` as `conversationPart` gives it.
6568
+ * thread's, a channel's, or a project's, named by `part` as `conversationPart` gives it.
6759
6569
  */
6760
6570
  sessionDir(agent2, scope, part) {
6761
6571
  return part === void 0 ? this.paths.agentProject(agent2, scope) : this.paths.agentConversation(agent2, scope, part);
6762
6572
  }
6763
6573
  async readSessions(agent2, project2, part) {
6764
- const file = path6.join(this.sessionDir(agent2, project2, part), "sessions.json");
6574
+ const file = path7.join(this.sessionDir(agent2, project2, part), "sessions.json");
6765
6575
  return await exists(file) ? readJson(file, SessionsFileSchema) : {};
6766
6576
  }
6767
6577
  /**
@@ -6772,15 +6582,34 @@ ${rationale}`}`);
6772
6582
  await this.mutex.run(async () => {
6773
6583
  const dir = this.sessionDir(agent2, project2, part);
6774
6584
  await ensureDir(dir);
6775
- const file = path6.join(dir, "sessions.json");
6585
+ const file = path7.join(dir, "sessions.json");
6776
6586
  const current = await exists(file) ? await readJson(file, SessionsFileSchema) : {};
6777
6587
  const kept = current.runner === void 0 || current.runner === runner2 ? current : {};
6778
6588
  await writeJson(file, { ...kept, [cli]: sessionId, runner: runner2 });
6779
6589
  });
6780
6590
  }
6781
- /** The last turn of one conversation: the scope's home, a channel's, or a thread's. */
6591
+ /**
6592
+ * Drops a conversation's session for a CLI, so its next turn starts a new one, when the record
6593
+ * still names `sessionId`; returns whether it did.
6594
+ */
6595
+ async dropSession(agent2, project2, cli, sessionId, part) {
6596
+ return this.mutex.run(async () => {
6597
+ const file = path7.join(this.sessionDir(agent2, project2, part), "sessions.json");
6598
+ if (!await exists(file)) {
6599
+ return false;
6600
+ }
6601
+ const current = await readJson(file, SessionsFileSchema);
6602
+ if (current[cli] !== sessionId) {
6603
+ return false;
6604
+ }
6605
+ const { [cli]: _dropped, ...kept } = current;
6606
+ await writeJson(file, kept);
6607
+ return true;
6608
+ });
6609
+ }
6610
+ /** The last turn of one conversation: the scope's home, a channel's, a project's, or a thread's. */
6782
6611
  async readLastTurn(agent2, project2, part) {
6783
- const file = path6.join(this.sessionDir(agent2, project2, part), "last-turn.json");
6612
+ const file = path7.join(this.sessionDir(agent2, project2, part), "last-turn.json");
6784
6613
  return await exists(file) ? readJson(file, TurnRecordSchema) : null;
6785
6614
  }
6786
6615
  /** The latest turn a citizen took in a scope, in any of its conversations there. */
@@ -6788,8 +6617,9 @@ ${rationale}`}`);
6788
6617
  let latest = await this.readLastTurn(agent2, project2);
6789
6618
  const place = this.paths.agentProject(agent2, project2);
6790
6619
  const parts = [
6791
- ...await listDirs(path6.join(place, "threads")),
6792
- ...(await listDirs(path6.join(place, "channels"))).map((name) => `#${name}`)
6620
+ ...await listDirs(path7.join(place, "threads")),
6621
+ ...(await listDirs(path7.join(place, "channels"))).map((name) => `#${name}`),
6622
+ ...(await listDirs(path7.join(place, "places"))).map((name) => `~${name}`)
6793
6623
  ];
6794
6624
  for (const part of parts) {
6795
6625
  const turn2 = await this.readLastTurn(agent2, project2, part);
@@ -6810,6 +6640,7 @@ ${rationale}`}`);
6810
6640
  project: parsed.project,
6811
6641
  ...parsed.thread === void 0 ? {} : { thread: parsed.thread },
6812
6642
  ...parsed.channel === void 0 ? {} : { channel: parsed.channel },
6643
+ ...parsed.place === void 0 ? {} : { place: parsed.place },
6813
6644
  trigger: parsed.trigger.kind,
6814
6645
  session: parsed.session,
6815
6646
  runner: parsed.runner
@@ -6839,7 +6670,8 @@ ${rationale}`}`);
6839
6670
  agent: parsed.agent,
6840
6671
  project: parsed.project,
6841
6672
  ...parsed.thread === void 0 ? {} : { thread: parsed.thread },
6842
- ...parsed.channel === void 0 ? {} : { channel: parsed.channel }
6673
+ ...parsed.channel === void 0 ? {} : { channel: parsed.channel },
6674
+ ...parsed.place === void 0 ? {} : { place: parsed.place }
6843
6675
  });
6844
6676
  }
6845
6677
  await this.events.append(failed ? "turn.failed" : "turn.completed", parsed.agent, {
@@ -6847,6 +6679,7 @@ ${rationale}`}`);
6847
6679
  project: parsed.project,
6848
6680
  ...parsed.thread === void 0 ? {} : { thread: parsed.thread },
6849
6681
  ...parsed.channel === void 0 ? {} : { channel: parsed.channel },
6682
+ ...parsed.place === void 0 ? {} : { place: parsed.place },
6850
6683
  trigger: parsed.trigger.kind,
6851
6684
  session: parsed.session,
6852
6685
  exitReason: parsed.exitReason,
@@ -6856,7 +6689,10 @@ ${rationale}`}`);
6856
6689
  model: parsed.model,
6857
6690
  summary: parsed.status?.summary ?? null,
6858
6691
  needsUserDecision: parsed.status?.needsUserDecision ?? false,
6859
- error: parsed.error
6692
+ error: parsed.error,
6693
+ ...parsed.resumeFailed === void 0 ? {} : { resumeFailed: parsed.resumeFailed },
6694
+ ...parsed.transient === true ? { transient: true } : {},
6695
+ ...parsed.sessionDropped === true ? { sessionDropped: true } : {}
6860
6696
  });
6861
6697
  });
6862
6698
  }
@@ -6893,6 +6729,20 @@ ${rationale}`}`);
6893
6729
  return task2;
6894
6730
  });
6895
6731
  }
6732
+ /**
6733
+ * Posts the board's word on a task's work into the task's thread, such as commits a runner
6734
+ * could not put on its branch: as the board, with every `@name` written as a plain name, so it
6735
+ * wakes nobody and counts toward no heartbeat. Null, posting nothing, once the thread is closed.
6736
+ */
6737
+ async postTaskNotice(taskId, body) {
6738
+ return this.mutex.run(async () => {
6739
+ const found = await this.tryFindThread(taskId);
6740
+ if (found === null || found.thread.state !== "open") {
6741
+ return null;
6742
+ }
6743
+ return this.appendThreadMessage(SYSTEM_ACTOR.name, taskId, withoutMentions(body));
6744
+ });
6745
+ }
6896
6746
  /**
6897
6747
  * Records that a citizen's workspace in a conversation that has ended holds work on no branch, so
6898
6748
  * the scheduler gives the citizen a closing turn there to decide about it. False, recording
@@ -6903,7 +6753,7 @@ ${rationale}`}`);
6903
6753
  const payload = {
6904
6754
  agent: input.agent,
6905
6755
  project: input.scope,
6906
- ...conversation.kind === "thread" ? { thread: conversation.id } : { channel: conversation.name }
6756
+ ...conversation.kind === "thread" ? { thread: conversation.id } : conversation.kind === "channel" ? { channel: conversation.name } : { place: conversation.project }
6907
6757
  };
6908
6758
  return this.mutex.run(async () => {
6909
6759
  const earlier = (await this.events.readSince(null, Number.MAX_SAFE_INTEGER)).filter((event) => event.type === "workspace.leftovers" && Object.entries(payload).every(([key, value]) => event.payload[key] === value)).length;
@@ -6955,9 +6805,13 @@ ${rationale}`}`);
6955
6805
  }
6956
6806
  /**
6957
6807
  * Whether a conversation with a workspace of its own has ended: a thread closed, a task ended
6958
- * (for one from before tasks had threads), or a channel archived.
6808
+ * (for one from before tasks had threads), a channel archived, or a project archived, which ends
6809
+ * every citizen's conversation of its channels in the society scope.
6959
6810
  */
6960
6811
  async conversationEnded(conversation) {
6812
+ if (conversation.kind === "place") {
6813
+ return !await this.projectLive(conversation.project);
6814
+ }
6961
6815
  if (conversation.kind === "channel") {
6962
6816
  const place = conversation.scope === SOCIETY_SCOPE ? null : conversation.scope;
6963
6817
  return !await this.channelOpen(channelRef(place, conversation.name));
@@ -7031,11 +6885,11 @@ ${rationale}`}`);
7031
6885
  }
7032
6886
  /** Small named state files under data/state, for cursors the scheduler must keep across restarts. */
7033
6887
  async readState(name, schema, fallback) {
7034
- const file = path6.join(this.paths.state(), `${name}.json`);
6888
+ const file = path7.join(this.paths.state(), `${name}.json`);
7035
6889
  return await exists(file) ? readJson(file, schema) : fallback;
7036
6890
  }
7037
6891
  async writeState(name, value) {
7038
- await this.mutex.run(() => writeJson(path6.join(this.paths.state(), `${name}.json`), value));
6892
+ await this.mutex.run(() => writeJson(path7.join(this.paths.state(), `${name}.json`), value));
7039
6893
  }
7040
6894
  /** The project's dashboard, empty at revision 0 when it has none. */
7041
6895
  async readDashboard(slug) {
@@ -7066,7 +6920,7 @@ ${rationale}`}`);
7066
6920
  async listRunners() {
7067
6921
  const runners = [];
7068
6922
  for (const file of await listFiles(this.paths.runners())) {
7069
- runners.push((await readMarkdown(path6.join(this.paths.runners(), file), RunnerSchema)).data);
6923
+ runners.push((await readMarkdown(path7.join(this.paths.runners(), file), RunnerSchema)).data);
7070
6924
  }
7071
6925
  return runners;
7072
6926
  }
@@ -7118,7 +6972,7 @@ ${rationale}`}`);
7118
6972
  return this.runnerTokens.get(hashToken(token)) ?? null;
7119
6973
  }
7120
6974
  runnerTokensFile() {
7121
- return path6.join(this.paths.state(), "runners.json");
6975
+ return path7.join(this.paths.state(), "runners.json");
7122
6976
  }
7123
6977
  async readRunnerTokens() {
7124
6978
  const file = this.runnerTokensFile();
@@ -7167,26 +7021,26 @@ ${rationale}`}`);
7167
7021
  async listHomeConflicts(agent2) {
7168
7022
  const home = this.paths.agent(agent2);
7169
7023
  const found = [];
7170
- const walk = async (dir, prefix) => {
7171
- for (const entry of await readdir3(dir, { withFileTypes: true }).catch(() => [])) {
7024
+ const walk2 = async (dir, prefix) => {
7025
+ for (const entry of await readdir4(dir, { withFileTypes: true }).catch(() => [])) {
7172
7026
  if (prefix === "" && SKIPPED_IN_HOME.has(entry.name)) {
7173
7027
  continue;
7174
7028
  }
7175
7029
  const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
7176
7030
  if (entry.isDirectory()) {
7177
- await walk(path6.join(dir, entry.name), relative);
7031
+ await walk2(path7.join(dir, entry.name), relative);
7178
7032
  continue;
7179
7033
  }
7180
7034
  const file = entry.isFile() ? conflictCopyOf(relative) : null;
7181
7035
  if (file !== null) {
7182
- const since = await this.homes.changedAt(home, relative) ?? (await stat3(path6.join(home, relative)).catch(() => null))?.mtime.toISOString();
7036
+ const since = await this.homes.changedAt(home, relative) ?? (await stat3(path7.join(home, relative)).catch(() => null))?.mtime.toISOString();
7183
7037
  if (since !== void 0) {
7184
7038
  found.push({ path: relative, file, since });
7185
7039
  }
7186
7040
  }
7187
7041
  }
7188
7042
  };
7189
- await walk(home, "");
7043
+ await walk2(home, "");
7190
7044
  return found.toSorted((a, b) => a.path.localeCompare(b.path));
7191
7045
  }
7192
7046
  /** The newest `limit` changes to an agent's home, newest first; empty while it is no repository. */
@@ -7263,7 +7117,7 @@ ${rationale}`}`);
7263
7117
  async writeTurnRecord(record) {
7264
7118
  const dir = this.sessionDir(record.agent, record.project, conversationPart(record));
7265
7119
  await ensureDir(dir);
7266
- await writeJson(path6.join(dir, "last-turn.json"), record);
7120
+ await writeJson(path7.join(dir, "last-turn.json"), record);
7267
7121
  }
7268
7122
  // ---------------------------------------------------------------------------------------------
7269
7123
  // Internals
@@ -7382,8 +7236,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7382
7236
  async ensureAgentProject(name, slug) {
7383
7237
  const dir = this.paths.agentProject(name, slug);
7384
7238
  await ensureDir(dir);
7385
- if (!await exists(path6.join(dir, "sessions.json"))) {
7386
- await writeJson(path6.join(dir, "sessions.json"), {});
7239
+ if (!await exists(path7.join(dir, "sessions.json"))) {
7240
+ await writeJson(path7.join(dir, "sessions.json"), {});
7387
7241
  }
7388
7242
  }
7389
7243
  /**
@@ -7415,7 +7269,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7415
7269
  const at = turn2?.endedAt ?? turn2?.startedAt;
7416
7270
  if (turn2 !== null && at !== void 0 && (lastTurnAt === void 0 || at > lastTurnAt)) {
7417
7271
  lastTurnAt = at;
7418
- lastTurnOutcome = `${turn2.trigger.kind} on ${scope}: ${turn2.exitReason ?? "running"}${turn2.status === null ? "" : `, ${turn2.status.summary.slice(0, 160)}`}`;
7272
+ lastTurnOutcome = `${turn2.trigger.kind} on ${scope}${turnConversation(turn2)}: ${turn2.exitReason ?? "running"}${turn2.status === null ? "" : `, ${turn2.status.summary.slice(0, 160)}`}`;
7419
7273
  lastModel = turn2.model ?? void 0;
7420
7274
  }
7421
7275
  }
@@ -7545,15 +7399,9 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7545
7399
  case "archive":
7546
7400
  await this.validateArchive(ArchiveProposalSchema.parse(charter).project);
7547
7401
  return;
7548
- case "reallocation": {
7549
- const parsed = ReallocationProposalSchema.safeParse(charter);
7550
- if (parsed.success) {
7551
- await this.reallocatedRole(parsed.data);
7552
- } else {
7553
- LegacyReallocationSchema.parse(charter);
7554
- }
7402
+ case "reallocation":
7403
+ await this.reallocatedRole(ReallocationProposalSchema.parse(charter));
7555
7404
  return;
7556
- }
7557
7405
  default:
7558
7406
  return;
7559
7407
  }
@@ -7613,11 +7461,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7613
7461
  return { project: input.project, members };
7614
7462
  }
7615
7463
  case "reallocation": {
7616
- const parsed = ReallocationProposalSchema.safeParse(proposal2.charter);
7617
- if (!parsed.success) {
7618
- return void 0;
7619
- }
7620
- const { next, changed } = await this.reallocatedRole(parsed.data);
7464
+ const { next, changed } = await this.reallocatedRole(ReallocationProposalSchema.parse(proposal2.charter));
7621
7465
  await this.writeRoleUnlocked(by, next, meta);
7622
7466
  return { role: next.name, changed };
7623
7467
  }
@@ -7627,25 +7471,21 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7627
7471
  }
7628
7472
  /**
7629
7473
  * Publishes a promoted skill under the society's skills, where every citizen's skills index lists
7630
- * it: the folder the board took from its proposer's home, or the text proposed alone. A
7631
- * replacement is the whole skill, so files the new version dropped go with the old one.
7474
+ * it: the folder the board took from its proposer's home. A replacement is the whole skill, so
7475
+ * files the new version dropped go with the old one.
7632
7476
  */
7633
7477
  async promoteSkillUnlocked(by, proposal2, skill2, meta) {
7634
7478
  const dir = this.paths.societySkill(skill2.name);
7635
7479
  const file = this.paths.societySkillFile(skill2.name);
7636
7480
  const replaced = await exists(file);
7637
7481
  const taken = proposal2.skillFiles;
7638
- const staged = path6.join(this.paths.proposalFiles(proposal2.id), "SKILL.md");
7639
- if (skill2.body === void 0 && (taken === void 0 || !await exists(staged))) {
7482
+ const staged = path7.join(this.paths.proposalFiles(proposal2.id), "SKILL.md");
7483
+ if (taken === void 0 || !await exists(staged)) {
7640
7484
  throw new BoardError("INVALID_STATE", `proposal ${proposal2.id} holds no skill files`);
7641
7485
  }
7642
7486
  await rm2(dir, { recursive: true, force: true });
7643
- if (skill2.body === void 0) {
7644
- await cp(this.paths.proposalFiles(proposal2.id), dir, { recursive: true });
7645
- } else {
7646
- await ensureDir(dir);
7647
- }
7648
- const body = skill2.body ?? (await readLooseMarkdown(file)).body;
7487
+ await cp(this.paths.proposalFiles(proposal2.id), dir, { recursive: true });
7488
+ const { body } = await readLooseMarkdown(file);
7649
7489
  await writeMarkdown(file, {
7650
7490
  name: skill2.name,
7651
7491
  description: skill2.summary,
@@ -7653,7 +7493,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7653
7493
  proposedBy: proposal2.proposedBy,
7654
7494
  promotedBy: by,
7655
7495
  promotedAt: this.now().toISOString(),
7656
- ...taken === void 0 ? {} : { commit: taken.commit }
7496
+ commit: taken.commit
7657
7497
  }
7658
7498
  }, body);
7659
7499
  await this.events.append("skill.promoted", by, {
@@ -7661,7 +7501,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7661
7501
  summary: skill2.summary,
7662
7502
  proposedBy: proposal2.proposedBy,
7663
7503
  replaced,
7664
- ...taken === void 0 ? {} : { files: taken.files.length },
7504
+ files: taken.files.length,
7665
7505
  ...meta
7666
7506
  });
7667
7507
  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.`);
@@ -7698,8 +7538,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7698
7538
  const dir = this.paths.proposalFiles(proposal2.id);
7699
7539
  await rm2(dir, { recursive: true, force: true });
7700
7540
  for (const entry of taken.files) {
7701
- const target = path6.join(dir, ...entry.path.split("/"));
7702
- await ensureDir(path6.dirname(target));
7541
+ const target = path7.join(dir, ...entry.path.split("/"));
7542
+ await ensureDir(path7.dirname(target));
7703
7543
  await writeFile4(target, contents.get(entry.path) ?? Buffer.alloc(0));
7704
7544
  await chmod2(target, entry.executable ? 493 : 420);
7705
7545
  }
@@ -7757,7 +7597,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7757
7597
  const where = {
7758
7598
  scope: turn2.project,
7759
7599
  ...turn2.thread === void 0 ? {} : { thread: turn2.thread },
7760
- ...turn2.channel === void 0 ? {} : { channel: turn2.channel }
7600
+ ...turn2.channel === void 0 ? {} : { channel: turn2.channel },
7601
+ ...turn2.place === void 0 ? {} : { place: turn2.place }
7761
7602
  };
7762
7603
  await this.mutex.run(async () => {
7763
7604
  for (const proposal2 of await this.listProposals()) {
@@ -7789,16 +7630,25 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7789
7630
  if (!isSafeRelativePath(entry.path)) {
7790
7631
  continue;
7791
7632
  }
7792
- const bytes = await readFile4(path6.join(dir, ...entry.path.split("/"))).catch(() => null);
7793
- const text = bytes === null || bytes.includes(0) ? null : bytes.toString("utf8");
7633
+ const bytes = await readFile4(path7.join(dir, ...entry.path.split("/"))).catch(() => null);
7794
7634
  files.push({
7795
7635
  ...entry,
7796
- text: text === null ? null : text.slice(0, SKILL_TEXT_LIMIT_CHARS),
7797
- truncated: text !== null && text.length > SKILL_TEXT_LIMIT_CHARS
7636
+ ...bytes === null ? { text: null, truncated: false } : skillFileText(bytes)
7798
7637
  });
7799
7638
  }
7800
7639
  return files;
7801
7640
  }
7641
+ /**
7642
+ * A society skill's folder as it is published, with each file's text, for the skill's view; an
7643
+ * unknown skill, or a folder without a SKILL.md, is not found.
7644
+ */
7645
+ async readSocietySkillFiles(name) {
7646
+ const files = await readSkillFolder(this.paths.societySkill(NameSchema.parse(name)));
7647
+ if (files === null) {
7648
+ throw new BoardError("NOT_FOUND", `the society has no skill ${name}`);
7649
+ }
7650
+ return files;
7651
+ }
7802
7652
  async validateAddAgent(input) {
7803
7653
  if (await exists(this.paths.agentFile(input.name))) {
7804
7654
  throw new BoardError("ALREADY_EXISTS", `agent ${input.name} already exists`);
@@ -7929,7 +7779,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7929
7779
  this.assertNoTurnCut(actor, `${ref} cannot be archived`, (turn2) => turn2.channel === channel && turn2.scope === scope || turn2.thread !== void 0 && closing.has(turn2.thread));
7930
7780
  const threadsClosed = [];
7931
7781
  for (const file of await listFiles(threads)) {
7932
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
7782
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
7933
7783
  if (doc.data.channel !== ref || doc.data.state !== "open") {
7934
7784
  continue;
7935
7785
  }
@@ -8035,7 +7885,8 @@ ${charter.purpose}
8035
7885
  }
8036
7886
  }
8037
7887
  /**
8038
- * Every member leaves, every open thread on its channels closes, and the project is marked; its
7888
+ * Every member leaves, every open thread on its channels closes, every citizen's conversation of
7889
+ * its channels in the society scope ends with the crons there, and the project is marked; its
8039
7890
  * files, repository, and history stay. The notice is the board's, since the reason is free text
8040
7891
  * a proposer wrote and a board-authored mention would wake whoever it names.
8041
7892
  */
@@ -8060,7 +7911,7 @@ ${charter.purpose}
8060
7911
  const threads = this.paths.threads(slug);
8061
7912
  const threadsClosed = [];
8062
7913
  for (const file of await listFiles(threads)) {
8063
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
7914
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
8064
7915
  if (doc.data.state !== "open") {
8065
7916
  continue;
8066
7917
  }
@@ -8078,7 +7929,7 @@ ${charter.purpose}
8078
7929
  });
8079
7930
  threadsClosed.push(doc.data.id);
8080
7931
  }
8081
- await this.endCronsUnlocked(by, `project ${slug} was archived`, (cron2) => cron2.scope === slug || cron2.thread !== void 0 && threadsClosed.includes(cron2.thread));
7932
+ await this.endCronsUnlocked(by, `project ${slug} was archived`, (cron2) => cron2.scope === slug || cron2.place === slug || cron2.thread !== void 0 && threadsClosed.includes(cron2.thread));
8082
7933
  const project2 = await this.updateProject(slug, (p) => ({
8083
7934
  ...p,
8084
7935
  members: [],
@@ -8135,10 +7986,10 @@ ${charter.purpose}
8135
7986
  ...project2,
8136
7987
  members: project2.members.filter((member) => member !== name)
8137
7988
  }));
8138
- const sessions = path6.join(this.paths.agentProject(name, slug), "sessions.json");
7989
+ const sessions = path7.join(this.paths.agentProject(name, slug), "sessions.json");
8139
7990
  if (await exists(sessions)) {
8140
7991
  const archived = `sessions.archived.${ts.replace(/[:.]/g, "-")}.json`;
8141
- await rename3(sessions, path6.join(this.paths.agentProject(name, slug), archived));
7992
+ await rename3(sessions, path7.join(this.paths.agentProject(name, slug), archived));
8142
7993
  }
8143
7994
  }
8144
7995
  this.tokenIndex.delete(current.tokenHash);
@@ -8218,6 +8069,17 @@ ${charter.purpose}
8218
8069
  return false;
8219
8070
  }
8220
8071
  }
8072
+ /**
8073
+ * Whether a project exists and is not archived, so citizens' conversations of its channels in
8074
+ * the society scope are still going.
8075
+ */
8076
+ async projectLive(slug) {
8077
+ try {
8078
+ return (await this.readProject(slug)).archived === void 0;
8079
+ } catch {
8080
+ return false;
8081
+ }
8082
+ }
8221
8083
  /**
8222
8084
  * A channel that takes new posts, threads, tasks, and followers: it exists, it is not archived,
8223
8085
  * and its project is not archived.
@@ -8328,6 +8190,7 @@ ${charter.purpose}
8328
8190
  await this.validateProvision(filed.kind, filed.charter);
8329
8191
  }
8330
8192
  this.assertNoTurnCut(actor, `proposal ${proposalId} cannot be decided`, (turn2) => turn2.thread === proposalId);
8193
+ const thread2 = await this.proposalThreadUnlocked(filed);
8331
8194
  if (outcome === "approved" && filed.kind === "archive") {
8332
8195
  await this.assertProjectQuiet(actor, ArchiveProposalSchema.parse(filed.charter).project);
8333
8196
  }
@@ -8369,7 +8232,6 @@ ${charter.purpose}
8369
8232
  const verdict = outcome === "approved" ? "Approved" : "Rejected";
8370
8233
  const why = reason === void 0 ? "" : ` Reason: ${reason}.`;
8371
8234
  const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
8372
- const thread2 = await this.proposalThreadUnlocked(actor.name, proposal2);
8373
8235
  const post = `${verdict}: ${describeCharter(proposal2.kind, proposal2.charter)}.${why}${result}`;
8374
8236
  await this.appendThreadMessage(actor.name, thread2.id, actor.name === SYSTEM_ACTOR.name ? withoutMentions(post) : post, { step: { action: outcome } });
8375
8237
  await this.endThreadOf(proposalId, actor.name, outcome);
@@ -8468,7 +8330,7 @@ ${charter.purpose}
8468
8330
  if (since !== null && id <= since || upto !== void 0 && id > upto) {
8469
8331
  continue;
8470
8332
  }
8471
- const doc = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
8333
+ const doc = await readMarkdown(path7.join(dir, file), MessageFrontmatterSchema);
8472
8334
  messages.push({ ...doc.data, body: doc.body });
8473
8335
  }
8474
8336
  return messages;
@@ -8525,7 +8387,7 @@ function globals() {
8525
8387
  function print(value, human) {
8526
8388
  console.log(globals().json ? JSON.stringify(value, null, 2) : human());
8527
8389
  }
8528
- async function open2() {
8390
+ async function open3() {
8529
8391
  return Board.open(globals().data);
8530
8392
  }
8531
8393
  async function actorFor(board, as) {
@@ -8571,7 +8433,7 @@ user.command("token").description("Issue the user a new token and print it once;
8571
8433
  if (!opts.rotate) {
8572
8434
  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");
8573
8435
  }
8574
- const board = await open2();
8436
+ const board = await open3();
8575
8437
  const userToken = await board.rotateUserToken();
8576
8438
  print({ userToken }, () => [
8577
8439
  `User token (shown once): ${userToken}`,
@@ -8580,7 +8442,7 @@ user.command("token").description("Issue the user a new token and print it once;
8580
8442
  });
8581
8443
  var project = program.command("project").description("Manage projects");
8582
8444
  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) => {
8583
- const board = await open2();
8445
+ const board = await open3();
8584
8446
  const added = await board.addProject(board.userActor(), {
8585
8447
  slug,
8586
8448
  name: opts.name,
@@ -8590,7 +8452,7 @@ project.command("add <slug>").description("Add a project with its default channe
8590
8452
  print(added, () => `Project ${added.slug} added with channels ${added.channels.join(", ")}`);
8591
8453
  });
8592
8454
  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) => {
8593
- const board = await open2();
8455
+ const board = await open3();
8594
8456
  const configured = await board.configureProject(await actorFor(board, opts.as), {
8595
8457
  project: slug,
8596
8458
  on_done: CompletionEffectSchema.parse(opts.onDone)
@@ -8598,7 +8460,7 @@ project.command("configure <slug>").description("Set a project's completion effe
8598
8460
  print(configured, () => `Project ${configured.slug}: on done ${configured.onDone}`);
8599
8461
  });
8600
8462
  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) => {
8601
- const board = await open2();
8463
+ const board = await open3();
8602
8464
  const archived = await board.archiveProject(board.userActor(), {
8603
8465
  project: slug,
8604
8466
  reason: opts.reason
@@ -8606,13 +8468,13 @@ project.command("archive <slug>").description("Archive a project with no task in
8606
8468
  print(archived, () => `Project ${archived.slug} archived`);
8607
8469
  });
8608
8470
  project.command("list").description("List projects").action(async () => {
8609
- const board = await open2();
8471
+ const board = await open3();
8610
8472
  const projects = await board.listProjects();
8611
8473
  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"));
8612
8474
  });
8613
8475
  var runner = program.command("runner").description("Manage runners");
8614
8476
  runner.command("add <name>").description("Register a runner and print its token once").action(async (name) => {
8615
- const board = await open2();
8477
+ const board = await open3();
8616
8478
  const { runner: added, token } = await board.addRunner(board.userActor(), name);
8617
8479
  print({ runner: added, token }, () => [
8618
8480
  `Runner ${added.name} added`,
@@ -8621,13 +8483,13 @@ runner.command("add <name>").description("Register a runner and print its token
8621
8483
  ].join("\n"));
8622
8484
  });
8623
8485
  runner.command("list").description("List runners and what they offer").action(async () => {
8624
- const board = await open2();
8486
+ const board = await open3();
8625
8487
  const runners = await board.listRunners();
8626
8488
  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"));
8627
8489
  });
8628
8490
  var agent = program.command("agent").description("Manage agents");
8629
8491
  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) => {
8630
- const board = await open2();
8492
+ const board = await open3();
8631
8493
  const cli = opts.cli === void 0 ? null : CliKindSchema.parse(opts.cli);
8632
8494
  const { agent: added, token } = await board.addAgent(board.userActor(), {
8633
8495
  name,
@@ -8640,7 +8502,7 @@ agent.command("add <name>").description("Add an agent and print its board token
8640
8502
  print({ agent: added, token }, () => [`Agent ${added.name} added as ${added.role}`, `Token (shown once): ${token}`].join("\n"));
8641
8503
  });
8642
8504
  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) => {
8643
- const board = await open2();
8505
+ const board = await open3();
8644
8506
  if (chosen === void 0) {
8645
8507
  const current = await board.readAgent(name);
8646
8508
  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}`);
@@ -8650,7 +8512,7 @@ agent.command("runner <name> [runner]").description("Show or set the runner a ci
8650
8512
  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`);
8651
8513
  });
8652
8514
  agent.command("list").description("List agents").action(async () => {
8653
- const board = await open2();
8515
+ const board = await open3();
8654
8516
  const agents = await board.listAgents();
8655
8517
  const members = await board.listMembers();
8656
8518
  print(agents, () => agents.map((a) => {
@@ -8659,28 +8521,28 @@ agent.command("list").description("List agents").action(async () => {
8659
8521
  }).join("\n"));
8660
8522
  });
8661
8523
  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) => {
8662
- const board = await open2();
8524
+ const board = await open3();
8663
8525
  const updated = await board.setAgentModel(board.userActor(), name, model ?? null);
8664
8526
  print(updated, () => `${updated.name} runs with ${updated.model ?? "its CLI's default model"} from its next turn`);
8665
8527
  });
8666
8528
  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) => {
8667
- const board = await open2();
8529
+ const board = await open3();
8668
8530
  const updated = await board.setAgentEffort(board.userActor(), name, effort ?? null);
8669
8531
  print(updated, () => `${updated.name} runs with ${updated.effort === void 0 ? "its model's default effort" : `${updated.effort} effort`} from its next turn`);
8670
8532
  });
8671
8533
  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) => {
8672
- const board = await open2();
8534
+ const board = await open3();
8673
8535
  const retired = await board.retireAgent(board.userActor(), { name, reason: opts.reason });
8674
8536
  print(retired, () => `Agent ${retired.name} retired: ${opts.reason}`);
8675
8537
  });
8676
8538
  var proposal = program.command("proposal").description("Governance proposals");
8677
8539
  proposal.command("list").description("List proposals").action(async () => {
8678
- const board = await open2();
8540
+ const board = await open3();
8679
8541
  const proposals = await board.listProposals();
8680
8542
  print(proposals, () => proposals.length === 0 ? "No proposals." : proposals.map((p) => `${p.id} ${p.kind} ${p.status} by ${p.proposedBy}`).join("\n"));
8681
8543
  });
8682
8544
  proposal.command("show <id>").description("Show a proposal with its charter and rationale").action(async (id) => {
8683
- const board = await open2();
8545
+ const board = await open3();
8684
8546
  const found = await board.readProposal(id);
8685
8547
  print(found, () => [
8686
8548
  `${found.id} ${found.kind} ${found.status} by ${found.proposedBy}`,
@@ -8690,7 +8552,7 @@ proposal.command("show <id>").description("Show a proposal with its charter and
8690
8552
  ].filter((line) => line.length > 0).join("\n"));
8691
8553
  });
8692
8554
  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) => {
8693
- const board = await open2();
8555
+ const board = await open3();
8694
8556
  const created = await board.propose(await actorFor(board, opts.as), {
8695
8557
  kind: ProposalKindSchema.parse(opts.kind),
8696
8558
  charter: parseCharter(opts.charter),
@@ -8699,7 +8561,7 @@ proposal.command("create").description("Propose a member, role, channel, realloc
8699
8561
  print(created, () => `Proposal ${created.id} (${created.kind}) is ${created.status}`);
8700
8562
  });
8701
8563
  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) => {
8702
- const board = await open2();
8564
+ const board = await open3();
8703
8565
  const decision = await board.approve(await actorFor(board, opts.as), {
8704
8566
  proposal_id: id,
8705
8567
  ...opts.reason === void 0 ? {} : { reason: opts.reason }
@@ -8708,7 +8570,7 @@ proposal.command("approve <id>").description("Approve a proposal; the board prov
8708
8570
  print({ decision, proposal: after }, () => `Proposal ${id} approved; it is now ${after.status}`);
8709
8571
  });
8710
8572
  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) => {
8711
- const board = await open2();
8573
+ const board = await open3();
8712
8574
  const decision = await board.reject(await actorFor(board, opts.as), {
8713
8575
  proposal_id: id,
8714
8576
  reason: opts.reason
@@ -8717,14 +8579,14 @@ proposal.command("reject <id>").description("Reject a proposal with a reason").r
8717
8579
  });
8718
8580
  var role = program.command("role").description("Role charters");
8719
8581
  role.command("list").description("List role charters").action(async () => {
8720
- const board = await open2();
8582
+ const board = await open3();
8721
8583
  const roles = await board.listRoles();
8722
8584
  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"));
8723
8585
  });
8724
8586
  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", [
8725
8587
  "heartbeat"
8726
8588
  ]).action(async (name, opts) => {
8727
- const board = await open2();
8589
+ const board = await open3();
8728
8590
  const added = await board.setRoleCharter(board.userActor(), {
8729
8591
  name,
8730
8592
  purpose: opts.purpose,
@@ -8734,7 +8596,7 @@ role.command("add <name>").description("Write a new role charter directly as the
8734
8596
  print(added, () => `Role ${added.name} written with ${added.verbs.length} verbs`);
8735
8597
  });
8736
8598
  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) => {
8737
- const board = await open2();
8599
+ const board = await open3();
8738
8600
  const current = await board.readRole(name);
8739
8601
  const updated = await board.setRoleCharter(board.userActor(), {
8740
8602
  ...current,
@@ -8746,7 +8608,7 @@ role.command("set <name>").description("Change a charter directly as the user: s
8746
8608
  });
8747
8609
  var channelCommand = program.command("channel").description("Channels");
8748
8610
  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) => {
8749
- const board = await open2();
8611
+ const board = await open3();
8750
8612
  const parsed = parseChannelRef(ref);
8751
8613
  const added = await board.addChannel(await actorFor(board, opts.as), {
8752
8614
  project: parsed.project,
@@ -8756,7 +8618,7 @@ channelCommand.command("add <ref>").description("Open a channel: <name> for the
8756
8618
  print({ channel: added }, () => `Channel ${added} opened`);
8757
8619
  });
8758
8620
  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) => {
8759
- const board = await open2();
8621
+ const board = await open3();
8760
8622
  const archived = await board.archiveChannel(await actorFor(board, opts.as), {
8761
8623
  channel: ref,
8762
8624
  reason: opts.reason
@@ -8764,12 +8626,12 @@ channelCommand.command("archive <ref>").description("Archive a channel whose wor
8764
8626
  print(archived, () => `Channel ${ref} archived: ${archived.reason}`);
8765
8627
  });
8766
8628
  program.command("signals").description("Recent operations signals, oldest first").option("--limit <n>", "how many", "50").action(async (opts) => {
8767
- const board = await open2();
8629
+ const board = await open3();
8768
8630
  const signals = await board.listSignals(Number(opts.limit));
8769
8631
  print(signals, () => signals.length === 0 ? "No signals yet." : signals.map((s) => `[${s.ts}] ${s.signal.kind} ${s.signal.summary}`).join("\n"));
8770
8632
  });
8771
8633
  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) => {
8772
- const board = await open2();
8634
+ const board = await open3();
8773
8635
  const who = await actorFor(board, opts.as);
8774
8636
  const message = await board.postMessage(who, {
8775
8637
  channel,
@@ -8779,13 +8641,13 @@ program.command("post <channel> <body>").description("Post a message to a channe
8779
8641
  print(message, () => `Posted ${message.id} to ${message.channel}${message.thread === void 0 ? "" : ` (thread ${message.thread})`}`);
8780
8642
  });
8781
8643
  program.command("ask <text>").description("Post to the society's general channel as the user; the front desk routes it").action(async (text) => {
8782
- const board = await open2();
8644
+ const board = await open3();
8783
8645
  const message = await board.postMessage(board.userActor(), { channel: "general", body: text });
8784
8646
  print(message, () => `Posted ${message.id} to general; the concierge wakes on it`);
8785
8647
  });
8786
8648
  var task = program.command("task").description("Manage tasks");
8787
8649
  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) => {
8788
- const board = await open2();
8650
+ const board = await open3();
8789
8651
  const who = await actorFor(board, opts.as);
8790
8652
  const created = await board.createTask(who, {
8791
8653
  project: projectSlug,
@@ -8801,17 +8663,17 @@ task.command("create <project> <title>").description("Create a task with a plan,
8801
8663
  ].join("\n"));
8802
8664
  });
8803
8665
  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) => {
8804
- const board = await open2();
8666
+ const board = await open3();
8805
8667
  const claimed = await board.claimTask(await actorFor(board, opts.as), { task_id: id });
8806
8668
  print(claimed, () => `Stage ${claimed.stage} of task ${claimed.id} held by ${claimed.claimedBy ?? "?"} until ${claimed.leaseExpiresAt ?? "?"}`);
8807
8669
  });
8808
8670
  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) => {
8809
- const board = await open2();
8671
+ const board = await open3();
8810
8672
  const released = await board.releaseTask(await actorFor(board, opts.as), { task_id: id });
8811
8673
  print(released, () => `Stage ${released.stage} of task ${released.id} released`);
8812
8674
  });
8813
8675
  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) => {
8814
- const board = await open2();
8676
+ const board = await open3();
8815
8677
  const advanced = await board.advanceTask(await actorFor(board, opts.as), {
8816
8678
  task_id: id,
8817
8679
  ...opts.note === void 0 ? {} : { note: opts.note }
@@ -8820,7 +8682,7 @@ task.command("advance <id>").description("Finish the current stage; past the las
8820
8682
  ${describePlan(advanced)}`);
8821
8683
  });
8822
8684
  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) => {
8823
- const board = await open2();
8685
+ const board = await open3();
8824
8686
  const planned = await board.planTask(await actorFor(board, opts.as), {
8825
8687
  task_id: id,
8826
8688
  stages: PlanEditSchema.parse(JSON.parse(stages)),
@@ -8829,7 +8691,7 @@ task.command("plan <id> <stages>").description("Reshape the stages ahead: pass e
8829
8691
  print(planned, () => describePlan(planned));
8830
8692
  });
8831
8693
  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) => {
8832
- const board = await open2();
8694
+ const board = await open3();
8833
8695
  const updated = await board.updateTask(await actorFor(board, opts.as), {
8834
8696
  task_id: id,
8835
8697
  ...opts.stage === void 0 ? {} : { stage: opts.stage },
@@ -8840,7 +8702,7 @@ task.command("update <id>").description("Send a task back to an earlier stage, a
8840
8702
  print(updated, () => `Task ${updated.id} is ${updated.status} at ${updated.stage}`);
8841
8703
  });
8842
8704
  task.command("show <id>").description("Show a task with its plan and its thread").action(async (id) => {
8843
- const board = await open2();
8705
+ const board = await open3();
8844
8706
  const found = await board.getTask(board.userActor(), { task_id: id });
8845
8707
  const threadState = await board.readThread(id).then((t) => t.state).catch(() => "none");
8846
8708
  const said = found.messages.map((message) => {
@@ -8860,7 +8722,7 @@ ${message.body.trim()}
8860
8722
  ].join("\n"));
8861
8723
  });
8862
8724
  task.command("list <project>").description("List a project's tasks with their current stage").action(async (projectSlug) => {
8863
- const board = await open2();
8725
+ const board = await open3();
8864
8726
  const tasks = await board.listTasks(projectSlug);
8865
8727
  print(tasks, () => tasks.length === 0 ? "No tasks." : tasks.map((t) => {
8866
8728
  const stage = t.stages[stageIndex(t, t.stage)];
@@ -8868,18 +8730,16 @@ task.command("list <project>").description("List a project's tasks with their cu
8868
8730
  }).join("\n"));
8869
8731
  });
8870
8732
  var thread = program.command("thread").description("Manage threads");
8871
- 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) => {
8872
- const board = await open2();
8733
+ thread.command("open").description("Open a thread on a channel with a title; a task's or a proposal's opens with it").requiredOption("--channel <ref>", "the channel it hangs off").requiredOption("--title <text>", "the thread's title").option("--as <agent>", "act as this agent instead of the user").action(async (opts) => {
8734
+ const board = await open3();
8873
8735
  const opened = await board.openThread(await actorFor(board, opts.as), {
8874
- ...taskId === void 0 ? {} : { task_id: taskId },
8875
- ...opts.proposal === void 0 ? {} : { proposal_id: opts.proposal },
8876
- ...opts.channel === void 0 ? {} : { channel: opts.channel },
8877
- ...opts.title === void 0 ? {} : { title: opts.title }
8736
+ channel: opts.channel,
8737
+ title: opts.title
8878
8738
  });
8879
8739
  print(opened, () => `Thread ${opened.id} opened on ${opened.channel}: ${opened.title}`);
8880
8740
  });
8881
8741
  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) => {
8882
- const board = await open2();
8742
+ const board = await open3();
8883
8743
  const summary = await board.closeThread(await actorFor(board, opts.as), {
8884
8744
  thread_id: threadId,
8885
8745
  summary: opts.summary
@@ -8887,7 +8747,7 @@ thread.command("close <threadId>").description("Close a thread with a summary po
8887
8747
  print(summary, () => `Thread closed; summary posted as ${summary.id} to ${summary.channel}`);
8888
8748
  });
8889
8749
  thread.command("list").description("List threads, the society's first, then each project's").option("--open", "only open threads", false).action(async (opts) => {
8890
- const board = await open2();
8750
+ const board = await open3();
8891
8751
  const threads = (await board.listThreads()).filter((t) => !opts.open || t.state === "open");
8892
8752
  print(threads, () => threads.length === 0 ? "No threads." : threads.map((t) => {
8893
8753
  const about = t.subject === void 0 ? "topic" : t.subject.kind;
@@ -8895,7 +8755,7 @@ thread.command("list").description("List threads, the society's first, then each
8895
8755
  }).join("\n"));
8896
8756
  });
8897
8757
  thread.command("show <threadId>").description("Show a thread and its messages").action(async (threadId) => {
8898
- const board = await open2();
8758
+ const board = await open3();
8899
8759
  const found = await board.readThread(threadId);
8900
8760
  const messages = await board.listThread(threadId);
8901
8761
  print({ thread: found, messages }, () => [
@@ -8906,17 +8766,17 @@ thread.command("show <threadId>").description("Show a thread and its messages").
8906
8766
  ].join("\n"));
8907
8767
  });
8908
8768
  program.command("pause").description("Stop all wakeups; turns in flight finish").action(async () => {
8909
- const board = await open2();
8769
+ const board = await open3();
8910
8770
  await board.setPaused(board.userActor(), true);
8911
8771
  print({ paused: true }, () => "Society paused");
8912
8772
  });
8913
8773
  program.command("resume").description("Allow wakeups again").action(async () => {
8914
- const board = await open2();
8774
+ const board = await open3();
8915
8775
  await board.setPaused(board.userActor(), false);
8916
8776
  print({ paused: false }, () => "Society resumed");
8917
8777
  });
8918
8778
  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) => {
8919
- const board = await open2();
8779
+ const board = await open3();
8920
8780
  const society = zone === void 0 ? await board.society() : await board.setTimezone(board.userActor(), zone);
8921
8781
  print({ timezone: society.timezone }, () => `Time zone: ${society.timezone}`);
8922
8782
  });
@@ -8924,7 +8784,8 @@ function describeCron(cron2, timeZone) {
8924
8784
  const where = [
8925
8785
  cron2.scope,
8926
8786
  cron2.thread,
8927
- cron2.channel === void 0 ? void 0 : `#${cron2.channel}`
8787
+ cron2.channel === void 0 ? void 0 : `#${cron2.channel}`,
8788
+ cron2.place === void 0 ? void 0 : `~${cron2.place}`
8928
8789
  ].filter((part) => part !== void 0).join("/");
8929
8790
  const next = nextFire(cron2, timeZone);
8930
8791
  const state = cron2.ended !== void 0 ? `ended (${cron2.ended.reason})` : cron2.paused !== void 0 ? "paused" : next === null ? "no time to come" : `next ${next.toISOString()}`;
@@ -8932,13 +8793,13 @@ function describeCron(cron2, timeZone) {
8932
8793
  }
8933
8794
  var cron = program.command("cron").description("Crons that wake citizens on a clock");
8934
8795
  cron.command("list").description("List crons, ended ones too with --all").option("--all", "include ended crons", false).action(async (opts) => {
8935
- const board = await open2();
8796
+ const board = await open3();
8936
8797
  const { timezone } = await board.society();
8937
8798
  const crons = (await board.listCrons()).filter((each) => opts.all || each.ended === void 0);
8938
8799
  print(crons, () => crons.length === 0 ? "No crons." : crons.map((each) => describeCron(each, timezone)).join("\n"));
8939
8800
  });
8940
8801
  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) => {
8941
- const board = await open2();
8802
+ const board = await open3();
8942
8803
  const created = await board.createCron(await actorFor(board, opts.as), {
8943
8804
  title,
8944
8805
  note: opts.note,
@@ -8958,7 +8819,7 @@ for (const [name, paused] of [
8958
8819
  ["resume", false]
8959
8820
  ]) {
8960
8821
  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) => {
8961
- const board = await open2();
8822
+ const board = await open3();
8962
8823
  const updated = await board.updateCron(await actorFor(board, opts.as), {
8963
8824
  cron_id: id,
8964
8825
  paused
@@ -8968,7 +8829,7 @@ for (const [name, paused] of [
8968
8829
  });
8969
8830
  }
8970
8831
  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) => {
8971
- const board = await open2();
8832
+ const board = await open3();
8972
8833
  const removed = await board.removeCron(await actorFor(board, opts.as), {
8973
8834
  cron_id: id,
8974
8835
  reason: opts.reason
@@ -8977,12 +8838,12 @@ cron.command("remove <id>").description("End a cron for good").requiredOption("-
8977
8838
  });
8978
8839
  var knowledge = program.command("knowledge").description("Shared knowledge, by project or society");
8979
8840
  knowledge.command("list [project]").description("List knowledge topics of a project, or of the society when no project is given").action(async (projectSlug) => {
8980
- const board = await open2();
8841
+ const board = await open3();
8981
8842
  const topics = await board.listKnowledge(projectSlug ?? null);
8982
8843
  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"));
8983
8844
  });
8984
8845
  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) => {
8985
- const board = await open2();
8846
+ const board = await open3();
8986
8847
  const found = (await board.listKnowledge(opts.project ?? null)).find((t) => t.topic === topic);
8987
8848
  if (found === void 0) {
8988
8849
  throw new BoardError("NOT_FOUND", `no knowledge topic ${topic}`);
@@ -8990,8 +8851,8 @@ knowledge.command("show <topic>").description("Print a knowledge topic").option(
8990
8851
  print(found, () => found.body);
8991
8852
  });
8992
8853
  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) => {
8993
- const board = await open2();
8994
- const body = opts.file === void 0 ? await readStdin() : await readFile5(path7.resolve(opts.file), "utf8");
8854
+ const board = await open3();
8855
+ const body = opts.file === void 0 ? await readStdin() : await readFile5(path8.resolve(opts.file), "utf8");
8995
8856
  const written = await board.writeKnowledge(await actorFor(board, opts.as), {
8996
8857
  project: opts.project ?? null,
8997
8858
  topic,
@@ -9002,7 +8863,7 @@ knowledge.command("write <topic>").description("Write a knowledge topic as the u
9002
8863
  });
9003
8864
  var skill = program.command("skill").description("Skills promoted to the society");
9004
8865
  skill.command("list").description("List the society's skills, and each member's own").action(async () => {
9005
- const board = await open2();
8866
+ const board = await open3();
9006
8867
  const society = await board.listSocietySkills();
9007
8868
  const own = [];
9008
8869
  for (const member of await board.listAgents()) {
@@ -9017,7 +8878,7 @@ skill.command("list").description("List the society's skills, and each member's
9017
8878
  ].join("\n") || "No skills yet.");
9018
8879
  });
9019
8880
  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) => {
9020
- const board = await open2();
8881
+ const board = await open3();
9021
8882
  const removed = await board.removeSkill(await actorFor(board, opts.as), {
9022
8883
  name,
9023
8884
  reason: opts.reason
@@ -9026,13 +8887,13 @@ skill.command("remove <name>").description("Remove a skill from the society's sk
9026
8887
  });
9027
8888
  var turn = program.command("turn").description("Turn operations");
9028
8889
  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) => {
9029
- const board = await open2();
8890
+ const board = await open3();
9030
8891
  const actor = await board.actorFor(agentName);
9031
8892
  const digest = await board.readDigest(opts.project === void 0 ? actor : { ...actor, scope: opts.project }, { advance: false, limit: Number(opts.limit) });
9032
8893
  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"));
9033
8894
  });
9034
8895
  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) => {
9035
- const board = await open2();
8896
+ const board = await open3();
9036
8897
  const event = await board.requestWake(board.userActor(), {
9037
8898
  agent: agentName,
9038
8899
  project: opts.project,