@kubitnodes/stellaris 0.3.23 → 0.3.24

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 (62) hide show
  1. package/dist/cli/index.js +479 -164
  2. package/dist/server/index.js +598 -191
  3. package/package.json +1 -1
  4. package/web/dist/assets/{BackLink-DGYs80bb.js → BackLink-DKMw_rHu.js} +1 -1
  5. package/web/dist/assets/{Button-f4jgBZY1.js → Button-BYJGqZ3Q.js} +1 -1
  6. package/web/dist/assets/{ChannelForms-q9zdj4p2.js → ChannelForms-CkYupEL2.js} +1 -1
  7. package/web/dist/assets/ChannelView-BIXouQMX.js +2 -0
  8. package/web/dist/assets/CitizenView-C3AMFixP.js +2 -0
  9. package/web/dist/assets/CitizensView-CYFS3f9U.js +1 -0
  10. package/web/dist/assets/{CliIcon-CLHaA65A.js → CliIcon-DVX5iLhG.js} +1 -1
  11. package/web/dist/assets/CronsView-BGCcO7bH.js +1 -0
  12. package/web/dist/assets/{KnowledgeView-Cxfi2-Pt.js → KnowledgeView-DLLYjd1G.js} +1 -1
  13. package/web/dist/assets/Markdown--whKJBYV.js +30 -0
  14. package/web/dist/assets/{MessageItem-Bk9eBp1U.js → MessageItem-BvDOeJci.js} +1 -1
  15. package/web/dist/assets/{MetricsView-CTEH6rJn.js → MetricsView-B-KUNOZm.js} +1 -1
  16. package/web/dist/assets/NeedsYouView-Dee_bOKj.js +1 -0
  17. package/web/dist/assets/{Overview-CS9HCSmm.js → Overview-DFiHDFnA.js} +1 -1
  18. package/web/dist/assets/{Pane-CJYRVkwh.js → Pane-DTmByqgA.js} +1 -1
  19. package/web/dist/assets/{Picker-DoxCN6C7.js → Picker-DsHm4f7s.js} +1 -1
  20. package/web/dist/assets/{ProjectView-CJ3r4UdN.js → ProjectView-DsRXW-qk.js} +1 -1
  21. package/web/dist/assets/ProposalView-B-4P1S-s.js +1 -0
  22. package/web/dist/assets/{ProposalsView-B-cOxTsQ.js → ProposalsView-uEuj-ooV.js} +1 -1
  23. package/web/dist/assets/Quota-D8WDWp5H.js +1 -0
  24. package/web/dist/assets/{RunnersView-DlQruhwp.js → RunnersView-BB3q65rG.js} +1 -1
  25. package/web/dist/assets/SkillFileList-DhdEcFQA.js +2 -0
  26. package/web/dist/assets/SkillView-vAv7zvtJ.js +1 -0
  27. package/web/dist/assets/SocietyView-Dv4phSkz.js +1 -0
  28. package/web/dist/assets/{StickyList-MVP34ytV.js → StickyList-C4fC_KJv.js} +1 -1
  29. package/web/dist/assets/SubjectThread-BEXt-tub.js +1 -0
  30. package/web/dist/assets/{TaskFileView-GZ0eQFUb.js → TaskFileView-B9sex0tS.js} +2 -2
  31. package/web/dist/assets/{TaskView-DppFelLW.js → TaskView-BCT388kb.js} +2 -2
  32. package/web/dist/assets/{TasksView-DDNXzeG4.js → TasksView-CuPay34-.js} +1 -1
  33. package/web/dist/assets/{ThreadForms-C846TvtS.js → ThreadForms-DzoRDeBh.js} +1 -1
  34. package/web/dist/assets/ThreadItem-CDVppcOZ.js +1 -0
  35. package/web/dist/assets/ThreadView-B4F10_j1.js +1 -0
  36. package/web/dist/assets/citizen-BPIJzKdn.js +1 -0
  37. package/web/dist/assets/{compose-BmqQmcZH.js → compose-BUhbVoI-.js} +1 -1
  38. package/web/dist/assets/format-CvfmDNoY.js +55 -0
  39. package/web/dist/assets/{governance-Cr5OlQnw.js → governance-Bba0OHvJ.js} +1 -1
  40. package/web/dist/assets/index-Bj8MlyAn.css +2 -0
  41. package/web/dist/assets/index-DD3J11PT.js +23 -0
  42. package/web/dist/assets/{mentions-D5hNgtg6.js → mentions-D838wlfQ.js} +1 -1
  43. package/web/dist/assets/{tasks-R3YgpYTe.js → tasks-CTaxXtGL.js} +1 -1
  44. package/web/dist/assets/useMatch-B8j0hm1q.js +1 -0
  45. package/web/dist/index.html +9 -9
  46. package/web/dist/assets/ChannelView-CGs_aWL3.js +0 -2
  47. package/web/dist/assets/CitizenView-gcIo_sKt.js +0 -2
  48. package/web/dist/assets/CitizensView-DZ7S8vnJ.js +0 -1
  49. package/web/dist/assets/CronsView-1wns6Uwv.js +0 -1
  50. package/web/dist/assets/Markdown-BhtIFcWc.js +0 -30
  51. package/web/dist/assets/NeedsYouView-2VNqH2Sb.js +0 -1
  52. package/web/dist/assets/ProposalView-DLn9dNPp.js +0 -2
  53. package/web/dist/assets/Quota-Bo7UdWTV.js +0 -1
  54. package/web/dist/assets/SocietyView-Bl4ErnV8.js +0 -1
  55. package/web/dist/assets/SubjectThread-BFFnHxIF.js +0 -1
  56. package/web/dist/assets/ThreadItem-C8cou6wD.js +0 -1
  57. package/web/dist/assets/ThreadView-DvSGDYoD.js +0 -1
  58. package/web/dist/assets/citizen-B2Dlbcpe.js +0 -1
  59. package/web/dist/assets/format-DT52SbbR.js +0 -55
  60. package/web/dist/assets/index-BTVy_xFF.js +0 -23
  61. package/web/dist/assets/index-DzV6CCnG.css +0 -2
  62. 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,
@@ -939,7 +940,8 @@ var DashboardSchema = z5.object({
939
940
  var ProposalOriginSchema = z5.object({
940
941
  scope: NameSchema,
941
942
  thread: UlidSchema.optional(),
942
- channel: NameSchema.optional()
943
+ channel: NameSchema.optional(),
944
+ place: NameSchema.optional()
943
945
  });
944
946
  var SKILL_FILES_LIMIT = 100;
945
947
  var SKILL_BYTES_LIMIT = 10 * 1024 * 1024;
@@ -1293,6 +1295,7 @@ var TurnExitReasonSchema = z8.enum([
1293
1295
  "blocked",
1294
1296
  "stopped"
1295
1297
  ]);
1298
+ var ResumeFailureSchema = z8.enum(["lost", "retry"]);
1296
1299
  var AgentEventSchema = z8.discriminatedUnion("type", [
1297
1300
  z8.object({
1298
1301
  type: z8.literal("turn_started"),
@@ -1344,6 +1347,8 @@ var LiveTurnEventSchema = z8.object({
1344
1347
  thread: UlidSchema.optional(),
1345
1348
  /** The channel, never general, whose conversation the turn is in. */
1346
1349
  channel: NameSchema.optional(),
1350
+ /** The project whose channels' conversation the turn is in, for a turn in the society scope. */
1351
+ place: NameSchema.optional(),
1347
1352
  event: AgentEventSchema
1348
1353
  });
1349
1354
 
@@ -1430,7 +1435,8 @@ var TurnWorkspaceSchema = z9.discriminatedUnion("kind", [
1430
1435
  z9.object({
1431
1436
  kind: z9.literal("home"),
1432
1437
  thread: UlidSchema.optional(),
1433
- channel: NameSchema.optional()
1438
+ channel: NameSchema.optional(),
1439
+ place: NameSchema.optional()
1434
1440
  }),
1435
1441
  z9.object({
1436
1442
  kind: z9.literal("project"),
@@ -1439,7 +1445,16 @@ var TurnWorkspaceSchema = z9.discriminatedUnion("kind", [
1439
1445
  thread: UlidSchema.optional(),
1440
1446
  channel: NameSchema.optional()
1441
1447
  }),
1442
- z9.object({ kind: z9.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
1448
+ z9.object({
1449
+ kind: z9.literal("task"),
1450
+ repo: ProjectRepoSchema,
1451
+ taskId: UlidSchema,
1452
+ /**
1453
+ * The citizen holding the task's current stage, when someone does: a turn of anyone else
1454
+ * works detached at the branch's tip, so the holder's turns find the branch free.
1455
+ */
1456
+ holder: NameSchema.optional()
1457
+ })
1443
1458
  ]);
1444
1459
  var TurnJobSchema = z9.object({
1445
1460
  turnId: UlidSchema,
@@ -1455,6 +1470,8 @@ var TurnJobSchema = z9.object({
1455
1470
  thread: UlidSchema.optional(),
1456
1471
  /** The channel, never general, whose conversation the turn is in. */
1457
1472
  channel: NameSchema.optional(),
1473
+ /** The project whose channels' conversation the turn is in, for a turn in the society scope. */
1474
+ place: NameSchema.optional(),
1458
1475
  /**
1459
1476
  * The session to resume, or null to start one: the CLI's adapter picks a fresh session's id, and
1460
1477
  * the outcome reports it.
@@ -1475,6 +1492,12 @@ var TurnJobSchema = z9.object({
1475
1492
  workspace: TurnWorkspaceSchema
1476
1493
  });
1477
1494
  var TurnWorkSchema = z9.object({ branch: z9.string().min(1), head: z9.string().min(1) });
1495
+ var StuckWorkSchema = z9.object({
1496
+ branch: z9.string().min(1),
1497
+ agent: NameSchema,
1498
+ commits: z9.array(z9.string()),
1499
+ total: z9.number().int().positive()
1500
+ });
1478
1501
  var TurnEventsSchema = z9.object({ entries: z9.array(TranscriptEntrySchema) });
1479
1502
  var TurnOutcomeSchema = z9.object({
1480
1503
  exitReason: TurnExitReasonSchema,
@@ -1484,8 +1507,18 @@ var TurnOutcomeSchema = z9.object({
1484
1507
  costUsd: z9.number().nonnegative(),
1485
1508
  /** The session's running total after this turn, for CLIs that report one. */
1486
1509
  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),
1510
+ /**
1511
+ * The session the CLI used, which differs from the job's for CLIs that assign their own ids. A
1512
+ * turn whose CLI never started, because it failed or was stopped first, reports the job's own,
1513
+ * null in a conversation's first turn, so a session never started is not recorded.
1514
+ */
1515
+ session: z9.string().min(1).nullable(),
1516
+ /**
1517
+ * Set when the CLI could not resume the job's session, which failed the turn before any model
1518
+ * call; the server then drops a `lost` session, and one that failed `retry` too often in a row. A
1519
+ * runner from before never sends it.
1520
+ */
1521
+ resumeFailed: ResumeFailureSchema.optional(),
1489
1522
  /** The model the CLI reported, when it reported one. */
1490
1523
  model: z9.string().nullable(),
1491
1524
  work: TurnWorkSchema.nullable(),
@@ -1494,6 +1527,11 @@ var TurnOutcomeSchema = z9.object({
1494
1527
  * branch after the hand-back: uncommitted changes, or commits no branch contains.
1495
1528
  */
1496
1529
  leftovers: z9.boolean().default(false),
1530
+ /**
1531
+ * Commits the hand-back found its task's worktrees holding that no fast-forward of the task's
1532
+ * branch can take, each told once; a runner from before never sends it.
1533
+ */
1534
+ stuck: z9.array(StuckWorkSchema).optional(),
1497
1535
  /**
1498
1536
  * The CLI's plan quota as the turn's stream last reported it, when it reported any; a runner from
1499
1537
  * before never sends it.
@@ -1612,7 +1650,8 @@ var TurnImageSchema = z9.discriminatedUnion("kind", [
1612
1650
  ]);
1613
1651
  var WorkspaceConversationSchema = z9.discriminatedUnion("kind", [
1614
1652
  z9.object({ kind: z9.literal("thread"), id: UlidSchema }),
1615
- z9.object({ kind: z9.literal("channel"), scope: NameSchema, name: NameSchema })
1653
+ z9.object({ kind: z9.literal("channel"), scope: NameSchema, name: NameSchema }),
1654
+ z9.object({ kind: z9.literal("place"), project: NameSchema })
1616
1655
  ]);
1617
1656
  var HeldWorkspaceSchema = z9.object({
1618
1657
  agent: NameSchema,
@@ -1718,6 +1757,7 @@ var TurnDispatchSchema = z10.object({
1718
1757
  project: NameSchema,
1719
1758
  thread: TurnThreadSchema.optional(),
1720
1759
  channel: NameSchema.optional(),
1760
+ place: NameSchema.optional(),
1721
1761
  trigger: TriggerSchema,
1722
1762
  priority: z10.number().int().min(0).max(2),
1723
1763
  onboarding: z10.boolean().default(false),
@@ -1725,7 +1765,13 @@ var TurnDispatchSchema = z10.object({
1725
1765
  crons: z10.array(UlidSchema).optional()
1726
1766
  });
1727
1767
  function conversationPart(conversation) {
1728
- return conversation.thread ?? (conversation.channel === void 0 ? void 0 : `#${conversation.channel}`);
1768
+ if (conversation.thread !== void 0) {
1769
+ return conversation.thread;
1770
+ }
1771
+ if (conversation.channel !== void 0) {
1772
+ return `#${conversation.channel}`;
1773
+ }
1774
+ return conversation.place === void 0 ? void 0 : `~${conversation.place}`;
1729
1775
  }
1730
1776
  function channelConversation(scope, channel) {
1731
1777
  const at = channel.indexOf("/");
@@ -1733,6 +1779,10 @@ function channelConversation(scope, channel) {
1733
1779
  const name = at < 0 ? channel : channel.slice(at + 1);
1734
1780
  return place === scope && name !== "general" ? name : void 0;
1735
1781
  }
1782
+ function placeConversation(scope, channel) {
1783
+ const at = channel.indexOf("/");
1784
+ return scope === SOCIETY_SCOPE && at >= 0 ? channel.slice(0, at) : void 0;
1785
+ }
1736
1786
  var TurnRecordSchema = z10.object({
1737
1787
  /** Assigned by the board when the turn begins; the turn's transcript is filed under it. */
1738
1788
  id: UlidSchema.optional(),
@@ -1742,6 +1792,8 @@ var TurnRecordSchema = z10.object({
1742
1792
  thread: UlidSchema.optional(),
1743
1793
  /** The channel, never general, whose conversation the turn was in. */
1744
1794
  channel: NameSchema.optional(),
1795
+ /** The project whose channels' conversation the turn was in, for a turn in the society scope. */
1796
+ place: NameSchema.optional(),
1745
1797
  runner: NameSchema,
1746
1798
  cli: CliKindSchema.nullable(),
1747
1799
  session: z10.string().nullable(),
@@ -1765,6 +1817,12 @@ var TurnRecordSchema = z10.object({
1765
1817
  work: TurnWorkSchema.optional(),
1766
1818
  /** Who stopped the turn, for one that ended `stopped`. */
1767
1819
  stoppedBy: NameSchema.optional(),
1820
+ /** Why the CLI could not resume the turn's session, for a turn that failed so. */
1821
+ resumeFailed: ResumeFailureSchema.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(),
@@ -2006,7 +2067,7 @@ var RunnerConfigSchema = z13.object({
2006
2067
  });
2007
2068
 
2008
2069
  // packages/shared/dist/version.js
2009
- var STELLARIS_VERSION = "0.3.23";
2070
+ var STELLARIS_VERSION = "0.3.24";
2010
2071
 
2011
2072
  // packages/shared/dist/mentions.js
2012
2073
  import { parse, postprocess, preprocess } from "micromark";
@@ -2387,6 +2448,7 @@ var HOME_GITIGNORE = [
2387
2448
  "/projects/*/last-turn.json",
2388
2449
  "/projects/*/threads/",
2389
2450
  "/projects/*/channels/",
2451
+ "/projects/*/places/",
2390
2452
  `/${HOME_SCRATCH}/`,
2391
2453
  "*.tmp",
2392
2454
  "node_modules/",
@@ -2404,7 +2466,7 @@ var HOME_GITIGNORE = [
2404
2466
  var IGNORE_FILE = `${HOME_GITIGNORE.join("\n")}
2405
2467
  `;
2406
2468
  var PRE_RECEIVE = `#!/bin/sh
2407
- forbidden='^(agent\\.json|cursors\\.json|role\\.md|turns/|\\.claude/|\\.codex/|projects/[^/]+/(sessions[^/]*\\.json|last-turn\\.json|threads/|channels/))'
2469
+ forbidden='^(agent\\.json|cursors\\.json|role\\.md|turns/|\\.claude/|\\.codex/|projects/[^/]+/(sessions[^/]*\\.json|last-turn\\.json|threads/|channels/|places/))'
2408
2470
  while read old new ref; do
2409
2471
  if [ "$ref" != "refs/heads/main" ]; then
2410
2472
  echo "a home takes pushes to main only" >&2
@@ -2730,6 +2792,7 @@ var TurnPayload = z14.object({
2730
2792
  project: z14.string(),
2731
2793
  thread: z14.string().optional(),
2732
2794
  channel: z14.string().optional(),
2795
+ place: z14.string().optional(),
2733
2796
  trigger: z14.string()
2734
2797
  });
2735
2798
  var TaskPayload = z14.object({
@@ -2775,11 +2838,16 @@ function answers(mention, turn2) {
2775
2838
  if (!mention.scopes.includes(turn2.project)) {
2776
2839
  return false;
2777
2840
  }
2778
- const own = mention.thread === null ? channelConversation(turn2.project, mention.channel) : void 0;
2841
+ const topLevel = mention.thread === null;
2842
+ const own = topLevel ? channelConversation(turn2.project, mention.channel) : void 0;
2843
+ const place = topLevel ? placeConversation(turn2.project, mention.channel) : void 0;
2779
2844
  if (turn2.channel !== null) {
2780
- return mention.thread === null && own === turn2.channel;
2845
+ return topLevel && own === turn2.channel;
2846
+ }
2847
+ if (turn2.place !== null) {
2848
+ return topLevel && place === turn2.place;
2781
2849
  }
2782
- return mention.thread === null && own === void 0 || turn2.trigger === "mention";
2850
+ return topLevel && own === void 0 && place === void 0 || turn2.trigger === "mention";
2783
2851
  }
2784
2852
  function computeMetrics(input) {
2785
2853
  const { events, now, window } = input;
@@ -2845,6 +2913,7 @@ function computeMetrics(input) {
2845
2913
  at,
2846
2914
  thread: turn2.data.thread ?? null,
2847
2915
  channel: turn2.data.channel ?? null,
2916
+ place: turn2.data.place ?? null,
2848
2917
  project: turn2.data.project,
2849
2918
  trigger: turn2.data.trigger
2850
2919
  };
@@ -3044,6 +3113,7 @@ var TurnHistoryPayloadSchema = z15.object({
3044
3113
  project: NameSchema,
3045
3114
  thread: UlidSchema.optional(),
3046
3115
  channel: NameSchema.optional(),
3116
+ place: NameSchema.optional(),
3047
3117
  trigger: z15.string().default("manual"),
3048
3118
  exitReason: z15.string().nullable().default(null),
3049
3119
  costUsd: z15.number().default(0),
@@ -3282,11 +3352,14 @@ var BoardPaths = class {
3282
3352
  return path5.join(this.agentProjects(name), slug);
3283
3353
  }
3284
3354
  /**
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`.
3355
+ * A thread's, a channel's, or, in the society scope, a project's conversation's session and last
3356
+ * turn, beside the scope's home conversation; `part` is a thread's id, `#channel`, or `~project`.
3287
3357
  */
3288
3358
  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);
3359
+ if (part.startsWith("#")) {
3360
+ return path5.join(this.agentProject(name, slug), "channels", part.slice(1));
3361
+ }
3362
+ return part.startsWith("~") ? path5.join(this.agentProject(name, slug), "places", part.slice(1)) : path5.join(this.agentProject(name, slug), "threads", part);
3290
3363
  }
3291
3364
  agentCursors(name) {
3292
3365
  return path5.join(this.agent(name), "cursors.json");
@@ -3320,6 +3393,101 @@ var BoardPaths = class {
3320
3393
  }
3321
3394
  };
3322
3395
 
3396
+ // packages/board-core/dist/skill-folder.js
3397
+ import { constants } from "node:fs";
3398
+ import { lstat, open as open2, readdir as readdir3, realpath } from "node:fs/promises";
3399
+ import path6 from "node:path";
3400
+ var SKILL_TEXT_LIMIT_CHARS = 1e5;
3401
+ var SKILL_TEXT_LIMIT_BYTES = SKILL_TEXT_LIMIT_CHARS * 4;
3402
+ function skillFileText(bytes) {
3403
+ const text = bytes.includes(0) ? null : bytes.toString("utf8");
3404
+ return {
3405
+ text: text === null ? null : text.slice(0, SKILL_TEXT_LIMIT_CHARS),
3406
+ truncated: text !== null && text.length > SKILL_TEXT_LIMIT_CHARS
3407
+ };
3408
+ }
3409
+ function inside(root, file) {
3410
+ const relative = path6.relative(root, file);
3411
+ return relative !== ".." && !relative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(relative);
3412
+ }
3413
+ async function walk(root, prefix, found) {
3414
+ const entries = await readdir3(path6.join(root, ...prefix === "" ? [] : prefix.split("/")), {
3415
+ withFileTypes: true
3416
+ }).catch(() => []);
3417
+ for (const entry of entries.toSorted((a, b) => a.name < b.name ? -1 : 1)) {
3418
+ if (found.length >= SKILL_FILES_LIMIT) {
3419
+ return;
3420
+ }
3421
+ const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
3422
+ if (!isSafeRelativePath(relative)) {
3423
+ continue;
3424
+ }
3425
+ if (entry.isDirectory()) {
3426
+ await walk(root, relative, found);
3427
+ } else if (entry.isFile() && !entry.name.endsWith(".tmp")) {
3428
+ found.push(relative);
3429
+ }
3430
+ }
3431
+ }
3432
+ async function readEntry(root, relative) {
3433
+ let real;
3434
+ try {
3435
+ real = await realpath(path6.join(root, ...relative.split("/")));
3436
+ } catch {
3437
+ return null;
3438
+ }
3439
+ if (!inside(root, real)) {
3440
+ return null;
3441
+ }
3442
+ const handle = await open2(real, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK).catch(() => null);
3443
+ if (handle === null) {
3444
+ return null;
3445
+ }
3446
+ try {
3447
+ const stats = await handle.stat();
3448
+ if (!stats.isFile()) {
3449
+ return null;
3450
+ }
3451
+ const buffer = Buffer.alloc(Math.min(stats.size, SKILL_TEXT_LIMIT_BYTES));
3452
+ let size = 0;
3453
+ while (size < buffer.length) {
3454
+ const { bytesRead } = await handle.read(buffer, size, buffer.length - size, size);
3455
+ if (bytesRead === 0) {
3456
+ break;
3457
+ }
3458
+ size += bytesRead;
3459
+ }
3460
+ return {
3461
+ path: relative,
3462
+ size: stats.size,
3463
+ executable: (stats.mode & 73) !== 0,
3464
+ ...skillFileText(buffer.subarray(0, size))
3465
+ };
3466
+ } finally {
3467
+ await handle.close();
3468
+ }
3469
+ }
3470
+ async function readSkillFolder(dir) {
3471
+ const info = await lstat(dir).catch(() => null);
3472
+ if (info === null || !info.isDirectory()) {
3473
+ return null;
3474
+ }
3475
+ const root = await realpath(dir).catch(() => null);
3476
+ if (root === null) {
3477
+ return null;
3478
+ }
3479
+ const found = [];
3480
+ await walk(root, "", found);
3481
+ const files = [];
3482
+ for (const relative of found) {
3483
+ const file = await readEntry(root, relative);
3484
+ if (file !== null) {
3485
+ files.push(file);
3486
+ }
3487
+ }
3488
+ return files.some((file) => file.path === "SKILL.md") ? files : null;
3489
+ }
3490
+
3323
3491
  // packages/board-core/dist/tokens.js
3324
3492
  import { createHash as createHash2, randomBytes } from "node:crypto";
3325
3493
  function mintToken() {
@@ -3333,7 +3501,7 @@ function hashToken(token) {
3333
3501
  var SYSTEM_ACTOR = { name: "board", role: USER_ROLE };
3334
3502
  var CLOSING_ATTEMPTS = 3;
3335
3503
  function ownTurn(actor, turn2) {
3336
- return turn2.agent === actor.name && turn2.scope === actor.scope && turn2.thread === actor.thread && turn2.channel === actor.channel;
3504
+ return turn2.agent === actor.name && turn2.scope === actor.scope && turn2.thread === actor.thread && turn2.channel === actor.channel && turn2.place === actor.place;
3337
3505
  }
3338
3506
  function namesList(names) {
3339
3507
  return names.length <= 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}`;
@@ -3358,23 +3526,37 @@ var CursorsSchema = z16.object({
3358
3526
  scopes: z16.record(z16.string(), z16.string()).default({}),
3359
3527
  threadsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3360
3528
  channelsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3529
+ placesFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3361
3530
  /** How far a work role has read news, which goes to whichever of its general conversations reads first. */
3362
3531
  news: z16.string().optional()
3363
3532
  });
3364
- var NO_CURSORS = { digest: null, scopes: {}, threadsFrom: {}, channelsFrom: {} };
3533
+ var NO_CURSORS = {
3534
+ digest: null,
3535
+ scopes: {},
3536
+ threadsFrom: {},
3537
+ channelsFrom: {},
3538
+ placesFrom: {}
3539
+ };
3365
3540
  function conversationKey(scope, part) {
3366
3541
  return part === null ? scope : `${scope}/${part}`;
3367
3542
  }
3368
3543
  function channelOfPart(part) {
3369
3544
  return part?.startsWith("#") === true ? part.slice(1) : void 0;
3370
3545
  }
3546
+ function placeOfPart(part) {
3547
+ return part?.startsWith("~") === true ? part.slice(1) : void 0;
3548
+ }
3371
3549
  function threadOfPart(part) {
3372
- return part === null || part === void 0 || part.startsWith("#") ? void 0 : part;
3550
+ return part === null || part === void 0 || part.startsWith("#") || part.startsWith("~") ? void 0 : part;
3373
3551
  }
3374
3552
  function channelStart(cursors, scope) {
3375
3553
  const from = cursors.channelsFrom[scope];
3376
3554
  return from === void 0 ? cursors.digest : from;
3377
3555
  }
3556
+ function placeStart(cursors, scope) {
3557
+ const from = cursors.placesFrom[scope];
3558
+ return from === void 0 ? cursors.digest : from;
3559
+ }
3378
3560
  function threadStart(cursors, scope) {
3379
3561
  const from = cursors.threadsFrom[scope];
3380
3562
  return from === void 0 ? cursors.digest : from;
@@ -3390,7 +3572,7 @@ function cursorOf(cursors, scope, part = null) {
3390
3572
  if (part === null) {
3391
3573
  return cursors.scopes[scope] ?? cursors.digest;
3392
3574
  }
3393
- return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : threadStart(cursors, scope));
3575
+ return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : part.startsWith("~") ? placeStart(cursors, scope) : threadStart(cursors, scope));
3394
3576
  }
3395
3577
  function byId(a, b) {
3396
3578
  return a.message.id < b.message.id ? -1 : a.message.id > b.message.id ? 1 : 0;
@@ -3398,12 +3580,14 @@ function byId(a, b) {
3398
3580
  function turnTime(record) {
3399
3581
  return record.endedAt ?? record.startedAt;
3400
3582
  }
3583
+ function turnConversation(record) {
3584
+ 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` : "";
3585
+ }
3401
3586
  var PausedSchema = z16.object({ paused: z16.boolean() });
3402
3587
  var DEFAULT_LEASE_MS = 30 * 60 * 1e3;
3403
3588
  function withoutMentions(text) {
3404
3589
  return text.replaceAll(MENTION_PATTERN, "$1$2");
3405
3590
  }
3406
- var SKILL_TEXT_LIMIT_CHARS = 1e5;
3407
3591
  function skillFolderOf(proposal2) {
3408
3592
  if (proposal2.kind !== "skill") {
3409
3593
  return null;
@@ -3412,7 +3596,7 @@ function skillFolderOf(proposal2) {
3412
3596
  return parsed.success && parsed.data.body === void 0 ? parsed.data.name : null;
3413
3597
  }
3414
3598
  function proposedFrom(origin, where) {
3415
- return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel;
3599
+ return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel && origin.place === where.place;
3416
3600
  }
3417
3601
  function megabytes(bytes) {
3418
3602
  return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "");
@@ -3434,6 +3618,7 @@ var THREAD_CONVERSATIONS = "thread-conversations";
3434
3618
  var ASKS_CHANNEL_OPENED = "asks-channel-opened";
3435
3619
  var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
3436
3620
  var CHANNEL_CONVERSATIONS = "channel-conversations";
3621
+ var PLACE_CONVERSATIONS = "place-conversations";
3437
3622
  var DASHBOARD_UPDATES = "dashboard-updates-granted";
3438
3623
  var CRON_VERBS = "cron-verbs-granted";
3439
3624
  var KNOWLEDGE_NOTICES = "knowledge-notices";
@@ -3508,8 +3693,8 @@ function restage(existing, edit) {
3508
3693
  };
3509
3694
  }
3510
3695
  function reopenStage(stage) {
3511
- const { completedBy: _by, completedAt: _at, ...open3 } = stage;
3512
- return open3;
3696
+ const { completedBy: _by, completedAt: _at, ...open4 } = stage;
3697
+ return open4;
3513
3698
  }
3514
3699
  function gateChanges(before, after) {
3515
3700
  const changes = [];
@@ -3590,7 +3775,7 @@ var Board = class _Board {
3590
3775
  homes;
3591
3776
  constructor(dataDir, options) {
3592
3777
  this.paths = new BoardPaths(dataDir);
3593
- this.homes = new HomeRepos(path6.join(this.paths.state(), "home-hooks"));
3778
+ this.homes = new HomeRepos(path7.join(this.paths.state(), "home-hooks"));
3594
3779
  this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
3595
3780
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
3596
3781
  this.events = new EventLog(this.paths.eventLog(), () => this.newId(), this.now);
@@ -3623,7 +3808,7 @@ var Board = class _Board {
3623
3808
  * repeated, so a member that later chooses otherwise keeps its choice.
3624
3809
  */
3625
3810
  async applyAlignments() {
3626
- const file = path6.join(this.paths.state(), "alignments.json");
3811
+ const file = path7.join(this.paths.state(), "alignments.json");
3627
3812
  const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
3628
3813
  const pending = [
3629
3814
  FOLLOW_DECISIONS,
@@ -3639,7 +3824,8 @@ var Board = class _Board {
3639
3824
  DASHBOARD_UPDATES,
3640
3825
  CRON_VERBS,
3641
3826
  KNOWLEDGE_NOTICES,
3642
- IMAGE_UPLOADS
3827
+ IMAGE_UPLOADS,
3828
+ PLACE_CONVERSATIONS
3643
3829
  ].filter((name) => !applied.includes(name));
3644
3830
  if (pending.length === 0) {
3645
3831
  return;
@@ -3687,6 +3873,9 @@ var Board = class _Board {
3687
3873
  if (pending.includes(IMAGE_UPLOADS)) {
3688
3874
  await this.grantBeside("post_message", "upload_image");
3689
3875
  }
3876
+ if (pending.includes(PLACE_CONVERSATIONS)) {
3877
+ await this.startPlaceCursors();
3878
+ }
3690
3879
  await writeJson(file, { applied: [...applied, ...pending] });
3691
3880
  }
3692
3881
  /**
@@ -3891,7 +4080,7 @@ var Board = class _Board {
3891
4080
  }
3892
4081
  }
3893
4082
  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()));
4083
+ const raw = await readMarkdown(path7.join(this.paths.tasks(slug), file), z16.record(z16.string(), z16.unknown()));
3895
4084
  const legacy = raw.data["thread"];
3896
4085
  if (legacy === void 0) {
3897
4086
  continue;
@@ -3899,7 +4088,7 @@ var Board = class _Board {
3899
4088
  const task2 = { ...TaskFrontmatterSchema.parse(raw.data), body: raw.body };
3900
4089
  if ((legacy === "open" || legacy === "closed") && !await exists(this.paths.threadFile(threads, task2.id))) {
3901
4090
  const ended = task2.status === "done" || task2.status === "abandoned";
3902
- const open3 = legacy === "open" && !ended;
4091
+ const open4 = legacy === "open" && !ended;
3903
4092
  const openedBy = opened.get(task2.id);
3904
4093
  const closedBy = closed.get(task2.id);
3905
4094
  await this.writeThread(threads, {
@@ -3907,10 +4096,10 @@ var Board = class _Board {
3907
4096
  channel: channelRef(slug, "general"),
3908
4097
  title: task2.title,
3909
4098
  subject: { kind: "task", id: task2.id },
3910
- state: open3 ? "open" : "closed",
4099
+ state: open4 ? "open" : "closed",
3911
4100
  openedBy: openedBy?.actor ?? task2.createdBy,
3912
4101
  openedAt: openedBy?.ts ?? task2.createdAt,
3913
- ...open3 ? {} : {
4102
+ ...open4 ? {} : {
3914
4103
  closedBy: closedBy?.actor ?? SYSTEM_ACTOR.name,
3915
4104
  closedAt: closedBy?.ts ?? this.now().toISOString()
3916
4105
  },
@@ -3942,6 +4131,24 @@ var Board = class _Board {
3942
4131
  await writeJson(file, { ...cursors, channelsFrom });
3943
4132
  }
3944
4133
  }
4134
+ /**
4135
+ * A project's channels were once read, from the society scope, in the society's home
4136
+ * conversation, so a member had read each up to that cursor. Each project's conversation in the
4137
+ * society scope starts from it now, as each takes a conversation of its own.
4138
+ */
4139
+ async startPlaceCursors() {
4140
+ for (const agent2 of await this.listAgents()) {
4141
+ const file = this.paths.agentCursors(agent2.name);
4142
+ if (!await exists(file)) {
4143
+ continue;
4144
+ }
4145
+ const cursors = await readJson(file, CursorsSchema);
4146
+ await writeJson(file, {
4147
+ ...cursors,
4148
+ placesFrom: { [SOCIETY_SCOPE]: cursorOf(cursors, SOCIETY_SCOPE) }
4149
+ });
4150
+ }
4151
+ }
3945
4152
  /**
3946
4153
  * Threads were once read in the scope's single conversation, so a member had read every thread
3947
4154
  * up to its scope's cursor. Each scope's threads start from that cursor now, as each thread's
@@ -4082,13 +4289,15 @@ ${charter.purpose}
4082
4289
  const token = mintToken();
4083
4290
  const thread2 = threadOfPart(part);
4084
4291
  const channel = channelOfPart(part);
4292
+ const place = placeOfPart(part);
4085
4293
  this.turnTokens.set(hashToken(token), {
4086
4294
  actor: {
4087
4295
  name: agent2,
4088
4296
  role: role2,
4089
4297
  ...scope === void 0 ? {} : { scope },
4090
4298
  ...thread2 === void 0 ? {} : { thread: thread2 },
4091
- ...channel === void 0 ? {} : { channel }
4299
+ ...channel === void 0 ? {} : { channel },
4300
+ ...place === void 0 ? {} : { place }
4092
4301
  },
4093
4302
  expiresAt: this.now().getTime() + ttlMs
4094
4303
  });
@@ -4177,7 +4386,7 @@ ${charter.purpose}
4177
4386
  });
4178
4387
  }
4179
4388
  signInsFile() {
4180
- return path6.join(this.paths.state(), "sign-ins.json");
4389
+ return path7.join(this.paths.state(), "sign-ins.json");
4181
4390
  }
4182
4391
  async readSignIns() {
4183
4392
  const file = this.signInsFile();
@@ -4274,7 +4483,7 @@ ${charter.purpose}
4274
4483
  const files = await listFiles(this.paths.tasks(project2));
4275
4484
  const tasks = [];
4276
4485
  for (const file of files) {
4277
- const doc = await readMarkdown(path6.join(this.paths.tasks(project2), file), TaskFrontmatterSchema);
4486
+ const doc = await readMarkdown(path7.join(this.paths.tasks(project2), file), TaskFrontmatterSchema);
4278
4487
  tasks.push({ ...doc.data, body: doc.body });
4279
4488
  }
4280
4489
  return tasks;
@@ -4293,7 +4502,7 @@ ${charter.purpose}
4293
4502
  const files = await listFiles(this.paths.proposals());
4294
4503
  const proposals = [];
4295
4504
  for (const file of files) {
4296
- const doc = await readMarkdown(path6.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
4505
+ const doc = await readMarkdown(path7.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
4297
4506
  proposals.push({ ...doc.data, body: doc.body });
4298
4507
  }
4299
4508
  return proposals;
@@ -4320,7 +4529,7 @@ ${charter.purpose}
4320
4529
  async listMembers() {
4321
4530
  const members = [];
4322
4531
  for (const file of await listFiles(this.paths.members())) {
4323
- const doc = await readMarkdown(path6.join(this.paths.members(), file), MemberSchema);
4532
+ const doc = await readMarkdown(path7.join(this.paths.members(), file), MemberSchema);
4324
4533
  members.push({ ...doc.data, profile: doc.body });
4325
4534
  }
4326
4535
  return members;
@@ -4339,7 +4548,7 @@ ${charter.purpose}
4339
4548
  const dir = project2 === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project2);
4340
4549
  const topics = [];
4341
4550
  for (const file of await listFiles(dir)) {
4342
- topics.push(await this.knowledgeAt(path6.join(dir, file), project2, file.replace(/\.md$/, "")));
4551
+ topics.push(await this.knowledgeAt(path7.join(dir, file), project2, file.replace(/\.md$/, "")));
4343
4552
  }
4344
4553
  return topics;
4345
4554
  }
@@ -4369,7 +4578,7 @@ ${charter.purpose}
4369
4578
  const aside = this.paths.removedKnowledge(project2 ?? SOCIETY_SCOPE);
4370
4579
  let highest = 0;
4371
4580
  for (const file of (await listFiles(aside)).filter((name) => name.startsWith(`${topic}.`))) {
4372
- const copy = await this.knowledgeAt(path6.join(aside, file), project2, topic);
4581
+ const copy = await this.knowledgeAt(path7.join(aside, file), project2, topic);
4373
4582
  highest = Math.max(highest, copy.revision);
4374
4583
  }
4375
4584
  return highest;
@@ -4385,7 +4594,7 @@ ${charter.purpose}
4385
4594
  async listSkillsIn(dir, scope) {
4386
4595
  const skills = [];
4387
4596
  for (const entry of await listDirs(dir)) {
4388
- const file = path6.join(dir, entry, "SKILL.md");
4597
+ const file = path7.join(dir, entry, "SKILL.md");
4389
4598
  if (!await exists(file)) {
4390
4599
  continue;
4391
4600
  }
@@ -4475,7 +4684,7 @@ ${charter.purpose}
4475
4684
  if (!wanted.delete(file.slice(0, 26))) {
4476
4685
  continue;
4477
4686
  }
4478
- const { data } = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
4687
+ const { data } = await readMarkdown(path7.join(dir, file), MessageFrontmatterSchema);
4479
4688
  found.push(MessageRefSchema.parse(data));
4480
4689
  }
4481
4690
  if (wanted.size === 0) {
@@ -4507,7 +4716,7 @@ ${charter.purpose}
4507
4716
  if (!file.endsWith(".md")) {
4508
4717
  continue;
4509
4718
  }
4510
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
4719
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
4511
4720
  if (doc.data.state !== "open") {
4512
4721
  continue;
4513
4722
  }
@@ -4534,7 +4743,7 @@ ${charter.purpose}
4534
4743
  const all = [];
4535
4744
  for (const threads of await this.threadScopes()) {
4536
4745
  for (const file of await listFiles(threads)) {
4537
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
4746
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
4538
4747
  const messages = await listFiles(this.paths.threadMessages(threads, doc.data.id));
4539
4748
  const last = messages.at(-1);
4540
4749
  all.push({
@@ -4901,16 +5110,20 @@ ${DASHBOARD_LINE}
4901
5110
  }
4902
5111
  /**
4903
5112
  * 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
5113
+ * general channel, one of its other channels, one of its threads, or, in the society scope, a
5114
+ * project's channels. The heartbeat asks, so that it wakes a member where its unread
4905
5115
  * messages are and nowhere else. Society channels count only for a member woken in the society
4906
5116
  * scope; a project member reads them at its next turn. Two kinds of post are in the digest but not
4907
5117
  * counted, and are read when the member wakes for something else: a task step's, which wakes
4908
5118
  * 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.
5119
+ * whose signals that call for action wake their readers directly. A closed thread counts for
5120
+ * nothing: its conversation has ended, and a turn there could only read its posts and be refused
5121
+ * a reply, while a wake queued before it closed, as for a mention, still runs there.
4910
5122
  */
4911
5123
  async unreadByConversation(actor) {
4912
5124
  const { cursors, upto } = await this.digestSnapshot(actor.name);
4913
5125
  const counts = /* @__PURE__ */ new Map();
5126
+ const closed = /* @__PURE__ */ new Map();
4914
5127
  const unscoped = { name: actor.name, role: actor.role };
4915
5128
  for (const { scope, part, message, news } of await this.digestSince(unscoped, cursors, upto)) {
4916
5129
  if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
@@ -4920,11 +5133,23 @@ ${DASHBOARD_LINE}
4920
5133
  if (posted !== scope && scope !== SOCIETY_SCOPE) {
4921
5134
  continue;
4922
5135
  }
5136
+ const thread2 = threadOfPart(part);
5137
+ if (thread2 !== void 0) {
5138
+ let ended = closed.get(thread2);
5139
+ if (ended === void 0) {
5140
+ ended = (await this.tryFindThread(thread2))?.thread.state === "closed";
5141
+ closed.set(thread2, ended);
5142
+ }
5143
+ if (ended) {
5144
+ continue;
5145
+ }
5146
+ }
4923
5147
  const key = conversationKey(scope, part);
4924
5148
  const entry = counts.get(key) ?? {
4925
5149
  scope,
4926
- thread: threadOfPart(part) ?? null,
5150
+ thread: thread2 ?? null,
4927
5151
  channel: channelOfPart(part) ?? null,
5152
+ place: placeOfPart(part) ?? null,
4928
5153
  count: 0
4929
5154
  };
4930
5155
  counts.set(key, { ...entry, count: entry.count + 1 });
@@ -4982,8 +5207,9 @@ ${DASHBOARD_LINE}
4982
5207
  * thread it takes part in, and for the front desk, which every post by the user wakes, every post
4983
5208
  * by the user wherever it is. With a conversation, only what is filed there: for a thread, every
4984
5209
  * 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
5210
+ * top-level messages; for a project's in the society scope, the top-level messages of every
5211
+ * channel of a project the member is not in; for a scope's home, general's, since each thread,
5212
+ * each other channel, and each such project is a conversation of its own. Nothing newer than
4987
5213
  * `upto` counts (see `digestSnapshot`).
4988
5214
  */
4989
5215
  async digestSince(actor, cursors, upto, conversation) {
@@ -5003,11 +5229,17 @@ ${DASHBOARD_LINE}
5003
5229
  const frontDesk = charter.wakeTriggers.includes(FRONT_DESK_TRIGGER);
5004
5230
  const threadParticipation = /* @__PURE__ */ new Map();
5005
5231
  const openChannels = /* @__PURE__ */ new Map();
5232
+ const liveProjects = /* @__PURE__ */ new Map();
5006
5233
  const collected = [];
5007
5234
  const newsFrom = charter.societyScope ? void 0 : newsStart(cursors);
5008
5235
  const scopes = conversation !== void 0 ? [conversation.scope] : [...agent2.memberships, SOCIETY_SCOPE];
5009
5236
  const starts = [
5010
- ...scopes.flatMap((each) => conversation === void 0 ? [cursorOf(cursors, each), threadStart(cursors, each), channelStart(cursors, each)] : [cursorOf(cursors, each, conversation.part)]),
5237
+ ...scopes.flatMap((each) => conversation === void 0 ? [
5238
+ cursorOf(cursors, each),
5239
+ threadStart(cursors, each),
5240
+ channelStart(cursors, each),
5241
+ placeStart(cursors, each)
5242
+ ] : [cursorOf(cursors, each, conversation.part)]),
5011
5243
  ...newsFrom === void 0 ? [] : [newsFrom]
5012
5244
  ];
5013
5245
  const known = starts.filter((start) => start !== null);
@@ -5017,7 +5249,7 @@ ${DASHBOARD_LINE}
5017
5249
  const thread2 = message.thread ?? null;
5018
5250
  const asked = message.mentions.includes(actor.name) || frontDesk && message.author === USER_NAME;
5019
5251
  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) {
5252
+ if ((conversation === void 0 || conversation.part === null) && (newsFrom === null || message.id > newsFrom) && subscribed.has(message.channel) && message.author !== actor.name) {
5021
5253
  collected.push({
5022
5254
  scope: conversation?.scope ?? SOCIETY_SCOPE,
5023
5255
  part: null,
@@ -5028,12 +5260,18 @@ ${DASHBOARD_LINE}
5028
5260
  continue;
5029
5261
  }
5030
5262
  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);
5263
+ let open4 = named === void 0 ? void 0 : openChannels.get(message.channel);
5264
+ if (named !== void 0 && open4 === void 0) {
5265
+ open4 = await this.channelOpen(message.channel);
5266
+ openChannels.set(message.channel, open4);
5267
+ }
5268
+ const project2 = thread2 === null ? placeConversation(filedIn, message.channel) : void 0;
5269
+ let live = project2 === void 0 ? void 0 : liveProjects.get(project2);
5270
+ if (project2 !== void 0 && live === void 0) {
5271
+ live = await this.projectLive(project2);
5272
+ liveProjects.set(project2, live);
5273
+ }
5274
+ const part = thread2 ?? (open4 === true ? `#${named}` : live === true ? `~${project2}` : null);
5037
5275
  if (conversation !== void 0 && (filedIn !== conversation.scope || part !== conversation.part)) {
5038
5276
  continue;
5039
5277
  }
@@ -5098,7 +5336,7 @@ ${task2.body}`;
5098
5336
  }
5099
5337
  }
5100
5338
  for (const file of await listFiles(this.paths.projectKnowledge(project2))) {
5101
- const content = await readFile4(path6.join(this.paths.projectKnowledge(project2), file), "utf8");
5339
+ const content = await readFile4(path7.join(this.paths.projectKnowledge(project2), file), "utf8");
5102
5340
  if (content.toLowerCase().includes(needle)) {
5103
5341
  hits.push({
5104
5342
  kind: "knowledge",
@@ -5119,7 +5357,7 @@ ${task2.body}`;
5119
5357
  shared.push({
5120
5358
  kind: "knowledge",
5121
5359
  ref: `society/knowledge/${file}`,
5122
- file: path6.join(this.paths.societyKnowledge(), file)
5360
+ file: path7.join(this.paths.societyKnowledge(), file)
5123
5361
  });
5124
5362
  }
5125
5363
  for (const skill2 of await this.listSocietySkills()) {
@@ -5132,7 +5370,7 @@ ${task2.body}`;
5132
5370
  shared.push({
5133
5371
  kind: "memory",
5134
5372
  ref: `memory/${file}`,
5135
- file: path6.join(this.paths.agentMemory(actor.name), file)
5373
+ file: path7.join(this.paths.agentMemory(actor.name), file)
5136
5374
  });
5137
5375
  }
5138
5376
  for (const entry of shared) {
@@ -5222,7 +5460,7 @@ ${current.body}`);
5222
5460
  });
5223
5461
  const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
5224
5462
  await mkdir4(aside, { recursive: true });
5225
- await rename3(file, path6.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
5463
+ await rename3(file, path7.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
5226
5464
  await this.events.append("knowledge.removed", actor.name, {
5227
5465
  topic: args.topic,
5228
5466
  project: args.project
@@ -5270,7 +5508,7 @@ ${current.body}`);
5270
5508
  });
5271
5509
  const aside = this.paths.removedSkills();
5272
5510
  await mkdir4(aside, { recursive: true });
5273
- await rename3(this.paths.societySkill(args.name), path6.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
5511
+ await rename3(this.paths.societySkill(args.name), path7.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
5274
5512
  await this.events.append("skill.removed", actor.name, {
5275
5513
  name: args.name,
5276
5514
  reason: args.reason
@@ -5347,7 +5585,7 @@ ${current.body}`);
5347
5585
  type,
5348
5586
  size: bytes.byteLength
5349
5587
  });
5350
- const name = path6.basename(args.path).replaceAll(/[[\]\\\r\n]/g, "");
5588
+ const name = path7.basename(args.path).replaceAll(/[[\]\\\r\n]/g, "");
5351
5589
  return {
5352
5590
  id,
5353
5591
  type,
@@ -5373,7 +5611,7 @@ ${current.body}`);
5373
5611
  async listCrons() {
5374
5612
  const crons = [];
5375
5613
  for (const file of await listFiles(this.paths.crons())) {
5376
- crons.push(await this.readCronFile(path6.join(this.paths.crons(), file)));
5614
+ crons.push(await this.readCronFile(path7.join(this.paths.crons(), file)));
5377
5615
  }
5378
5616
  return crons;
5379
5617
  }
@@ -5495,11 +5733,16 @@ ${current.body}`);
5495
5733
  }
5496
5734
  /**
5497
5735
  * 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.
5736
+ * the conversation of the turn that set it when it wakes its setter, a project's channels' in
5737
+ * the society scope included, else its home outside projects.
5499
5738
  */
5500
5739
  async cronConversation(actor, agent2, named) {
5501
5740
  const own = actor.name === agent2.name;
5502
5741
  if (named.project === void 0 && named.thread_id === void 0 && named.channel === void 0) {
5742
+ if (own && actor.scope === SOCIETY_SCOPE && actor.place !== void 0) {
5743
+ await this.readActiveProject(actor.place);
5744
+ return { scope: SOCIETY_SCOPE, place: actor.place };
5745
+ }
5503
5746
  return own && actor.scope !== void 0 ? this.cronConversation(actor, agent2, {
5504
5747
  project: actor.scope,
5505
5748
  thread_id: actor.thread,
@@ -5575,7 +5818,8 @@ ${current.body}`);
5575
5818
  agent: cron2.agent,
5576
5819
  project: cron2.scope,
5577
5820
  ...cron2.thread === void 0 ? {} : { thread: cron2.thread },
5578
- ...cron2.channel === void 0 ? {} : { channel: cron2.channel }
5821
+ ...cron2.channel === void 0 ? {} : { channel: cron2.channel },
5822
+ ...cron2.place === void 0 ? {} : { place: cron2.place }
5579
5823
  };
5580
5824
  }
5581
5825
  async readCronFile(file) {
@@ -5786,17 +6030,20 @@ ${current.body}`);
5786
6030
  async openThreadIds(dir, keep) {
5787
6031
  const ids = /* @__PURE__ */ new Set();
5788
6032
  for (const file of await listFiles(dir)) {
5789
- const doc = await readMarkdown(path6.join(dir, file), ThreadFrontmatterSchema);
6033
+ const doc = await readMarkdown(path7.join(dir, file), ThreadFrontmatterSchema);
5790
6034
  if (doc.data.state === "open" && keep({ ...doc.data, body: doc.body })) {
5791
6035
  ids.add(doc.data.id);
5792
6036
  }
5793
6037
  }
5794
6038
  return ids;
5795
6039
  }
5796
- /** Refuses to archive a project while a turn runs in it or in a thread its archive closes. */
6040
+ /**
6041
+ * Refuses to archive a project while a turn runs in it, in a citizen's conversation of its
6042
+ * channels in the society scope, or in a thread its archive closes.
6043
+ */
5797
6044
  async assertProjectQuiet(actor, slug) {
5798
6045
  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));
6046
+ this.assertNoTurnCut(actor, `project ${slug} cannot be archived`, (turn2) => turn2.scope === slug || turn2.place === slug || turn2.thread !== void 0 && threads.has(turn2.thread));
5800
6047
  }
5801
6048
  async closeThread(actor, input) {
5802
6049
  const args = VerbInputs.close_thread.parse(input);
@@ -6337,7 +6584,8 @@ ${args.summary}`);
6337
6584
  filedFrom: {
6338
6585
  scope: actor.scope,
6339
6586
  ...actor.thread === void 0 ? {} : { thread: actor.thread },
6340
- ...actor.channel === void 0 ? {} : { channel: actor.channel }
6587
+ ...actor.channel === void 0 ? {} : { channel: actor.channel },
6588
+ ...actor.place === void 0 ? {} : { place: actor.place }
6341
6589
  }
6342
6590
  },
6343
6591
  charter: parsed.data
@@ -6749,19 +6997,20 @@ ${rationale}`}`);
6749
6997
  project: input.project,
6750
6998
  ...input.thread === void 0 ? {} : { thread: input.thread },
6751
6999
  ...input.channel === void 0 ? {} : { channel: input.channel },
7000
+ ...input.place === void 0 ? {} : { place: input.place },
6752
7001
  messages: [...input.messages]
6753
7002
  });
6754
7003
  });
6755
7004
  }
6756
7005
  /**
6757
7006
  * 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.
7007
+ * thread's, a channel's, or a project's, named by `part` as `conversationPart` gives it.
6759
7008
  */
6760
7009
  sessionDir(agent2, scope, part) {
6761
7010
  return part === void 0 ? this.paths.agentProject(agent2, scope) : this.paths.agentConversation(agent2, scope, part);
6762
7011
  }
6763
7012
  async readSessions(agent2, project2, part) {
6764
- const file = path6.join(this.sessionDir(agent2, project2, part), "sessions.json");
7013
+ const file = path7.join(this.sessionDir(agent2, project2, part), "sessions.json");
6765
7014
  return await exists(file) ? readJson(file, SessionsFileSchema) : {};
6766
7015
  }
6767
7016
  /**
@@ -6772,15 +7021,34 @@ ${rationale}`}`);
6772
7021
  await this.mutex.run(async () => {
6773
7022
  const dir = this.sessionDir(agent2, project2, part);
6774
7023
  await ensureDir(dir);
6775
- const file = path6.join(dir, "sessions.json");
7024
+ const file = path7.join(dir, "sessions.json");
6776
7025
  const current = await exists(file) ? await readJson(file, SessionsFileSchema) : {};
6777
7026
  const kept = current.runner === void 0 || current.runner === runner2 ? current : {};
6778
7027
  await writeJson(file, { ...kept, [cli]: sessionId, runner: runner2 });
6779
7028
  });
6780
7029
  }
6781
- /** The last turn of one conversation: the scope's home, a channel's, or a thread's. */
7030
+ /**
7031
+ * Drops a conversation's session for a CLI, so its next turn starts a new one, when the record
7032
+ * still names `sessionId`; returns whether it did.
7033
+ */
7034
+ async dropSession(agent2, project2, cli, sessionId, part) {
7035
+ return this.mutex.run(async () => {
7036
+ const file = path7.join(this.sessionDir(agent2, project2, part), "sessions.json");
7037
+ if (!await exists(file)) {
7038
+ return false;
7039
+ }
7040
+ const current = await readJson(file, SessionsFileSchema);
7041
+ if (current[cli] !== sessionId) {
7042
+ return false;
7043
+ }
7044
+ const { [cli]: _dropped, ...kept } = current;
7045
+ await writeJson(file, kept);
7046
+ return true;
7047
+ });
7048
+ }
7049
+ /** The last turn of one conversation: the scope's home, a channel's, a project's, or a thread's. */
6782
7050
  async readLastTurn(agent2, project2, part) {
6783
- const file = path6.join(this.sessionDir(agent2, project2, part), "last-turn.json");
7051
+ const file = path7.join(this.sessionDir(agent2, project2, part), "last-turn.json");
6784
7052
  return await exists(file) ? readJson(file, TurnRecordSchema) : null;
6785
7053
  }
6786
7054
  /** The latest turn a citizen took in a scope, in any of its conversations there. */
@@ -6788,8 +7056,9 @@ ${rationale}`}`);
6788
7056
  let latest = await this.readLastTurn(agent2, project2);
6789
7057
  const place = this.paths.agentProject(agent2, project2);
6790
7058
  const parts = [
6791
- ...await listDirs(path6.join(place, "threads")),
6792
- ...(await listDirs(path6.join(place, "channels"))).map((name) => `#${name}`)
7059
+ ...await listDirs(path7.join(place, "threads")),
7060
+ ...(await listDirs(path7.join(place, "channels"))).map((name) => `#${name}`),
7061
+ ...(await listDirs(path7.join(place, "places"))).map((name) => `~${name}`)
6793
7062
  ];
6794
7063
  for (const part of parts) {
6795
7064
  const turn2 = await this.readLastTurn(agent2, project2, part);
@@ -6810,6 +7079,7 @@ ${rationale}`}`);
6810
7079
  project: parsed.project,
6811
7080
  ...parsed.thread === void 0 ? {} : { thread: parsed.thread },
6812
7081
  ...parsed.channel === void 0 ? {} : { channel: parsed.channel },
7082
+ ...parsed.place === void 0 ? {} : { place: parsed.place },
6813
7083
  trigger: parsed.trigger.kind,
6814
7084
  session: parsed.session,
6815
7085
  runner: parsed.runner
@@ -6839,7 +7109,8 @@ ${rationale}`}`);
6839
7109
  agent: parsed.agent,
6840
7110
  project: parsed.project,
6841
7111
  ...parsed.thread === void 0 ? {} : { thread: parsed.thread },
6842
- ...parsed.channel === void 0 ? {} : { channel: parsed.channel }
7112
+ ...parsed.channel === void 0 ? {} : { channel: parsed.channel },
7113
+ ...parsed.place === void 0 ? {} : { place: parsed.place }
6843
7114
  });
6844
7115
  }
6845
7116
  await this.events.append(failed ? "turn.failed" : "turn.completed", parsed.agent, {
@@ -6847,6 +7118,7 @@ ${rationale}`}`);
6847
7118
  project: parsed.project,
6848
7119
  ...parsed.thread === void 0 ? {} : { thread: parsed.thread },
6849
7120
  ...parsed.channel === void 0 ? {} : { channel: parsed.channel },
7121
+ ...parsed.place === void 0 ? {} : { place: parsed.place },
6850
7122
  trigger: parsed.trigger.kind,
6851
7123
  session: parsed.session,
6852
7124
  exitReason: parsed.exitReason,
@@ -6856,7 +7128,9 @@ ${rationale}`}`);
6856
7128
  model: parsed.model,
6857
7129
  summary: parsed.status?.summary ?? null,
6858
7130
  needsUserDecision: parsed.status?.needsUserDecision ?? false,
6859
- error: parsed.error
7131
+ error: parsed.error,
7132
+ ...parsed.resumeFailed === void 0 ? {} : { resumeFailed: parsed.resumeFailed },
7133
+ ...parsed.sessionDropped === true ? { sessionDropped: true } : {}
6860
7134
  });
6861
7135
  });
6862
7136
  }
@@ -6893,6 +7167,20 @@ ${rationale}`}`);
6893
7167
  return task2;
6894
7168
  });
6895
7169
  }
7170
+ /**
7171
+ * Posts the board's word on a task's work into the task's thread, such as commits a runner
7172
+ * could not put on its branch: as the board, with every `@name` written as a plain name, so it
7173
+ * wakes nobody and counts toward no heartbeat. Null, posting nothing, once the thread is closed.
7174
+ */
7175
+ async postTaskNotice(taskId, body) {
7176
+ return this.mutex.run(async () => {
7177
+ const found = await this.tryFindThread(taskId);
7178
+ if (found === null || found.thread.state !== "open") {
7179
+ return null;
7180
+ }
7181
+ return this.appendThreadMessage(SYSTEM_ACTOR.name, taskId, withoutMentions(body));
7182
+ });
7183
+ }
6896
7184
  /**
6897
7185
  * Records that a citizen's workspace in a conversation that has ended holds work on no branch, so
6898
7186
  * the scheduler gives the citizen a closing turn there to decide about it. False, recording
@@ -6903,7 +7191,7 @@ ${rationale}`}`);
6903
7191
  const payload = {
6904
7192
  agent: input.agent,
6905
7193
  project: input.scope,
6906
- ...conversation.kind === "thread" ? { thread: conversation.id } : { channel: conversation.name }
7194
+ ...conversation.kind === "thread" ? { thread: conversation.id } : conversation.kind === "channel" ? { channel: conversation.name } : { place: conversation.project }
6907
7195
  };
6908
7196
  return this.mutex.run(async () => {
6909
7197
  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 +7243,13 @@ ${rationale}`}`);
6955
7243
  }
6956
7244
  /**
6957
7245
  * 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.
7246
+ * (for one from before tasks had threads), a channel archived, or a project archived, which ends
7247
+ * every citizen's conversation of its channels in the society scope.
6959
7248
  */
6960
7249
  async conversationEnded(conversation) {
7250
+ if (conversation.kind === "place") {
7251
+ return !await this.projectLive(conversation.project);
7252
+ }
6961
7253
  if (conversation.kind === "channel") {
6962
7254
  const place = conversation.scope === SOCIETY_SCOPE ? null : conversation.scope;
6963
7255
  return !await this.channelOpen(channelRef(place, conversation.name));
@@ -7031,11 +7323,11 @@ ${rationale}`}`);
7031
7323
  }
7032
7324
  /** Small named state files under data/state, for cursors the scheduler must keep across restarts. */
7033
7325
  async readState(name, schema, fallback) {
7034
- const file = path6.join(this.paths.state(), `${name}.json`);
7326
+ const file = path7.join(this.paths.state(), `${name}.json`);
7035
7327
  return await exists(file) ? readJson(file, schema) : fallback;
7036
7328
  }
7037
7329
  async writeState(name, value) {
7038
- await this.mutex.run(() => writeJson(path6.join(this.paths.state(), `${name}.json`), value));
7330
+ await this.mutex.run(() => writeJson(path7.join(this.paths.state(), `${name}.json`), value));
7039
7331
  }
7040
7332
  /** The project's dashboard, empty at revision 0 when it has none. */
7041
7333
  async readDashboard(slug) {
@@ -7066,7 +7358,7 @@ ${rationale}`}`);
7066
7358
  async listRunners() {
7067
7359
  const runners = [];
7068
7360
  for (const file of await listFiles(this.paths.runners())) {
7069
- runners.push((await readMarkdown(path6.join(this.paths.runners(), file), RunnerSchema)).data);
7361
+ runners.push((await readMarkdown(path7.join(this.paths.runners(), file), RunnerSchema)).data);
7070
7362
  }
7071
7363
  return runners;
7072
7364
  }
@@ -7118,7 +7410,7 @@ ${rationale}`}`);
7118
7410
  return this.runnerTokens.get(hashToken(token)) ?? null;
7119
7411
  }
7120
7412
  runnerTokensFile() {
7121
- return path6.join(this.paths.state(), "runners.json");
7413
+ return path7.join(this.paths.state(), "runners.json");
7122
7414
  }
7123
7415
  async readRunnerTokens() {
7124
7416
  const file = this.runnerTokensFile();
@@ -7167,26 +7459,26 @@ ${rationale}`}`);
7167
7459
  async listHomeConflicts(agent2) {
7168
7460
  const home = this.paths.agent(agent2);
7169
7461
  const found = [];
7170
- const walk = async (dir, prefix) => {
7171
- for (const entry of await readdir3(dir, { withFileTypes: true }).catch(() => [])) {
7462
+ const walk2 = async (dir, prefix) => {
7463
+ for (const entry of await readdir4(dir, { withFileTypes: true }).catch(() => [])) {
7172
7464
  if (prefix === "" && SKIPPED_IN_HOME.has(entry.name)) {
7173
7465
  continue;
7174
7466
  }
7175
7467
  const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
7176
7468
  if (entry.isDirectory()) {
7177
- await walk(path6.join(dir, entry.name), relative);
7469
+ await walk2(path7.join(dir, entry.name), relative);
7178
7470
  continue;
7179
7471
  }
7180
7472
  const file = entry.isFile() ? conflictCopyOf(relative) : null;
7181
7473
  if (file !== null) {
7182
- const since = await this.homes.changedAt(home, relative) ?? (await stat3(path6.join(home, relative)).catch(() => null))?.mtime.toISOString();
7474
+ const since = await this.homes.changedAt(home, relative) ?? (await stat3(path7.join(home, relative)).catch(() => null))?.mtime.toISOString();
7183
7475
  if (since !== void 0) {
7184
7476
  found.push({ path: relative, file, since });
7185
7477
  }
7186
7478
  }
7187
7479
  }
7188
7480
  };
7189
- await walk(home, "");
7481
+ await walk2(home, "");
7190
7482
  return found.toSorted((a, b) => a.path.localeCompare(b.path));
7191
7483
  }
7192
7484
  /** The newest `limit` changes to an agent's home, newest first; empty while it is no repository. */
@@ -7263,7 +7555,7 @@ ${rationale}`}`);
7263
7555
  async writeTurnRecord(record) {
7264
7556
  const dir = this.sessionDir(record.agent, record.project, conversationPart(record));
7265
7557
  await ensureDir(dir);
7266
- await writeJson(path6.join(dir, "last-turn.json"), record);
7558
+ await writeJson(path7.join(dir, "last-turn.json"), record);
7267
7559
  }
7268
7560
  // ---------------------------------------------------------------------------------------------
7269
7561
  // Internals
@@ -7382,8 +7674,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7382
7674
  async ensureAgentProject(name, slug) {
7383
7675
  const dir = this.paths.agentProject(name, slug);
7384
7676
  await ensureDir(dir);
7385
- if (!await exists(path6.join(dir, "sessions.json"))) {
7386
- await writeJson(path6.join(dir, "sessions.json"), {});
7677
+ if (!await exists(path7.join(dir, "sessions.json"))) {
7678
+ await writeJson(path7.join(dir, "sessions.json"), {});
7387
7679
  }
7388
7680
  }
7389
7681
  /**
@@ -7415,7 +7707,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7415
7707
  const at = turn2?.endedAt ?? turn2?.startedAt;
7416
7708
  if (turn2 !== null && at !== void 0 && (lastTurnAt === void 0 || at > lastTurnAt)) {
7417
7709
  lastTurnAt = at;
7418
- lastTurnOutcome = `${turn2.trigger.kind} on ${scope}: ${turn2.exitReason ?? "running"}${turn2.status === null ? "" : `, ${turn2.status.summary.slice(0, 160)}`}`;
7710
+ lastTurnOutcome = `${turn2.trigger.kind} on ${scope}${turnConversation(turn2)}: ${turn2.exitReason ?? "running"}${turn2.status === null ? "" : `, ${turn2.status.summary.slice(0, 160)}`}`;
7419
7711
  lastModel = turn2.model ?? void 0;
7420
7712
  }
7421
7713
  }
@@ -7635,7 +7927,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7635
7927
  const file = this.paths.societySkillFile(skill2.name);
7636
7928
  const replaced = await exists(file);
7637
7929
  const taken = proposal2.skillFiles;
7638
- const staged = path6.join(this.paths.proposalFiles(proposal2.id), "SKILL.md");
7930
+ const staged = path7.join(this.paths.proposalFiles(proposal2.id), "SKILL.md");
7639
7931
  if (skill2.body === void 0 && (taken === void 0 || !await exists(staged))) {
7640
7932
  throw new BoardError("INVALID_STATE", `proposal ${proposal2.id} holds no skill files`);
7641
7933
  }
@@ -7698,8 +7990,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7698
7990
  const dir = this.paths.proposalFiles(proposal2.id);
7699
7991
  await rm2(dir, { recursive: true, force: true });
7700
7992
  for (const entry of taken.files) {
7701
- const target = path6.join(dir, ...entry.path.split("/"));
7702
- await ensureDir(path6.dirname(target));
7993
+ const target = path7.join(dir, ...entry.path.split("/"));
7994
+ await ensureDir(path7.dirname(target));
7703
7995
  await writeFile4(target, contents.get(entry.path) ?? Buffer.alloc(0));
7704
7996
  await chmod2(target, entry.executable ? 493 : 420);
7705
7997
  }
@@ -7757,7 +8049,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7757
8049
  const where = {
7758
8050
  scope: turn2.project,
7759
8051
  ...turn2.thread === void 0 ? {} : { thread: turn2.thread },
7760
- ...turn2.channel === void 0 ? {} : { channel: turn2.channel }
8052
+ ...turn2.channel === void 0 ? {} : { channel: turn2.channel },
8053
+ ...turn2.place === void 0 ? {} : { place: turn2.place }
7761
8054
  };
7762
8055
  await this.mutex.run(async () => {
7763
8056
  for (const proposal2 of await this.listProposals()) {
@@ -7789,16 +8082,25 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7789
8082
  if (!isSafeRelativePath(entry.path)) {
7790
8083
  continue;
7791
8084
  }
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");
8085
+ const bytes = await readFile4(path7.join(dir, ...entry.path.split("/"))).catch(() => null);
7794
8086
  files.push({
7795
8087
  ...entry,
7796
- text: text === null ? null : text.slice(0, SKILL_TEXT_LIMIT_CHARS),
7797
- truncated: text !== null && text.length > SKILL_TEXT_LIMIT_CHARS
8088
+ ...bytes === null ? { text: null, truncated: false } : skillFileText(bytes)
7798
8089
  });
7799
8090
  }
7800
8091
  return files;
7801
8092
  }
8093
+ /**
8094
+ * A society skill's folder as it is published, with each file's text, for the skill's view; an
8095
+ * unknown skill, or a folder without a SKILL.md, is not found.
8096
+ */
8097
+ async readSocietySkillFiles(name) {
8098
+ const files = await readSkillFolder(this.paths.societySkill(NameSchema.parse(name)));
8099
+ if (files === null) {
8100
+ throw new BoardError("NOT_FOUND", `the society has no skill ${name}`);
8101
+ }
8102
+ return files;
8103
+ }
7802
8104
  async validateAddAgent(input) {
7803
8105
  if (await exists(this.paths.agentFile(input.name))) {
7804
8106
  throw new BoardError("ALREADY_EXISTS", `agent ${input.name} already exists`);
@@ -7929,7 +8231,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7929
8231
  this.assertNoTurnCut(actor, `${ref} cannot be archived`, (turn2) => turn2.channel === channel && turn2.scope === scope || turn2.thread !== void 0 && closing.has(turn2.thread));
7930
8232
  const threadsClosed = [];
7931
8233
  for (const file of await listFiles(threads)) {
7932
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
8234
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
7933
8235
  if (doc.data.channel !== ref || doc.data.state !== "open") {
7934
8236
  continue;
7935
8237
  }
@@ -8035,7 +8337,8 @@ ${charter.purpose}
8035
8337
  }
8036
8338
  }
8037
8339
  /**
8038
- * Every member leaves, every open thread on its channels closes, and the project is marked; its
8340
+ * Every member leaves, every open thread on its channels closes, every citizen's conversation of
8341
+ * its channels in the society scope ends with the crons there, and the project is marked; its
8039
8342
  * files, repository, and history stay. The notice is the board's, since the reason is free text
8040
8343
  * a proposer wrote and a board-authored mention would wake whoever it names.
8041
8344
  */
@@ -8060,7 +8363,7 @@ ${charter.purpose}
8060
8363
  const threads = this.paths.threads(slug);
8061
8364
  const threadsClosed = [];
8062
8365
  for (const file of await listFiles(threads)) {
8063
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
8366
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
8064
8367
  if (doc.data.state !== "open") {
8065
8368
  continue;
8066
8369
  }
@@ -8078,7 +8381,7 @@ ${charter.purpose}
8078
8381
  });
8079
8382
  threadsClosed.push(doc.data.id);
8080
8383
  }
8081
- await this.endCronsUnlocked(by, `project ${slug} was archived`, (cron2) => cron2.scope === slug || cron2.thread !== void 0 && threadsClosed.includes(cron2.thread));
8384
+ await this.endCronsUnlocked(by, `project ${slug} was archived`, (cron2) => cron2.scope === slug || cron2.place === slug || cron2.thread !== void 0 && threadsClosed.includes(cron2.thread));
8082
8385
  const project2 = await this.updateProject(slug, (p) => ({
8083
8386
  ...p,
8084
8387
  members: [],
@@ -8135,10 +8438,10 @@ ${charter.purpose}
8135
8438
  ...project2,
8136
8439
  members: project2.members.filter((member) => member !== name)
8137
8440
  }));
8138
- const sessions = path6.join(this.paths.agentProject(name, slug), "sessions.json");
8441
+ const sessions = path7.join(this.paths.agentProject(name, slug), "sessions.json");
8139
8442
  if (await exists(sessions)) {
8140
8443
  const archived = `sessions.archived.${ts.replace(/[:.]/g, "-")}.json`;
8141
- await rename3(sessions, path6.join(this.paths.agentProject(name, slug), archived));
8444
+ await rename3(sessions, path7.join(this.paths.agentProject(name, slug), archived));
8142
8445
  }
8143
8446
  }
8144
8447
  this.tokenIndex.delete(current.tokenHash);
@@ -8218,6 +8521,17 @@ ${charter.purpose}
8218
8521
  return false;
8219
8522
  }
8220
8523
  }
8524
+ /**
8525
+ * Whether a project exists and is not archived, so citizens' conversations of its channels in
8526
+ * the society scope are still going.
8527
+ */
8528
+ async projectLive(slug) {
8529
+ try {
8530
+ return (await this.readProject(slug)).archived === void 0;
8531
+ } catch {
8532
+ return false;
8533
+ }
8534
+ }
8221
8535
  /**
8222
8536
  * A channel that takes new posts, threads, tasks, and followers: it exists, it is not archived,
8223
8537
  * and its project is not archived.
@@ -8468,7 +8782,7 @@ ${charter.purpose}
8468
8782
  if (since !== null && id <= since || upto !== void 0 && id > upto) {
8469
8783
  continue;
8470
8784
  }
8471
- const doc = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
8785
+ const doc = await readMarkdown(path7.join(dir, file), MessageFrontmatterSchema);
8472
8786
  messages.push({ ...doc.data, body: doc.body });
8473
8787
  }
8474
8788
  return messages;
@@ -8525,7 +8839,7 @@ function globals() {
8525
8839
  function print(value, human) {
8526
8840
  console.log(globals().json ? JSON.stringify(value, null, 2) : human());
8527
8841
  }
8528
- async function open2() {
8842
+ async function open3() {
8529
8843
  return Board.open(globals().data);
8530
8844
  }
8531
8845
  async function actorFor(board, as) {
@@ -8571,7 +8885,7 @@ user.command("token").description("Issue the user a new token and print it once;
8571
8885
  if (!opts.rotate) {
8572
8886
  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
8887
  }
8574
- const board = await open2();
8888
+ const board = await open3();
8575
8889
  const userToken = await board.rotateUserToken();
8576
8890
  print({ userToken }, () => [
8577
8891
  `User token (shown once): ${userToken}`,
@@ -8580,7 +8894,7 @@ user.command("token").description("Issue the user a new token and print it once;
8580
8894
  });
8581
8895
  var project = program.command("project").description("Manage projects");
8582
8896
  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();
8897
+ const board = await open3();
8584
8898
  const added = await board.addProject(board.userActor(), {
8585
8899
  slug,
8586
8900
  name: opts.name,
@@ -8590,7 +8904,7 @@ project.command("add <slug>").description("Add a project with its default channe
8590
8904
  print(added, () => `Project ${added.slug} added with channels ${added.channels.join(", ")}`);
8591
8905
  });
8592
8906
  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();
8907
+ const board = await open3();
8594
8908
  const configured = await board.configureProject(await actorFor(board, opts.as), {
8595
8909
  project: slug,
8596
8910
  on_done: CompletionEffectSchema.parse(opts.onDone)
@@ -8598,7 +8912,7 @@ project.command("configure <slug>").description("Set a project's completion effe
8598
8912
  print(configured, () => `Project ${configured.slug}: on done ${configured.onDone}`);
8599
8913
  });
8600
8914
  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();
8915
+ const board = await open3();
8602
8916
  const archived = await board.archiveProject(board.userActor(), {
8603
8917
  project: slug,
8604
8918
  reason: opts.reason
@@ -8606,13 +8920,13 @@ project.command("archive <slug>").description("Archive a project with no task in
8606
8920
  print(archived, () => `Project ${archived.slug} archived`);
8607
8921
  });
8608
8922
  project.command("list").description("List projects").action(async () => {
8609
- const board = await open2();
8923
+ const board = await open3();
8610
8924
  const projects = await board.listProjects();
8611
8925
  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
8926
  });
8613
8927
  var runner = program.command("runner").description("Manage runners");
8614
8928
  runner.command("add <name>").description("Register a runner and print its token once").action(async (name) => {
8615
- const board = await open2();
8929
+ const board = await open3();
8616
8930
  const { runner: added, token } = await board.addRunner(board.userActor(), name);
8617
8931
  print({ runner: added, token }, () => [
8618
8932
  `Runner ${added.name} added`,
@@ -8621,13 +8935,13 @@ runner.command("add <name>").description("Register a runner and print its token
8621
8935
  ].join("\n"));
8622
8936
  });
8623
8937
  runner.command("list").description("List runners and what they offer").action(async () => {
8624
- const board = await open2();
8938
+ const board = await open3();
8625
8939
  const runners = await board.listRunners();
8626
8940
  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
8941
  });
8628
8942
  var agent = program.command("agent").description("Manage agents");
8629
8943
  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();
8944
+ const board = await open3();
8631
8945
  const cli = opts.cli === void 0 ? null : CliKindSchema.parse(opts.cli);
8632
8946
  const { agent: added, token } = await board.addAgent(board.userActor(), {
8633
8947
  name,
@@ -8640,7 +8954,7 @@ agent.command("add <name>").description("Add an agent and print its board token
8640
8954
  print({ agent: added, token }, () => [`Agent ${added.name} added as ${added.role}`, `Token (shown once): ${token}`].join("\n"));
8641
8955
  });
8642
8956
  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();
8957
+ const board = await open3();
8644
8958
  if (chosen === void 0) {
8645
8959
  const current = await board.readAgent(name);
8646
8960
  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 +8964,7 @@ agent.command("runner <name> [runner]").description("Show or set the runner a ci
8650
8964
  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
8965
  });
8652
8966
  agent.command("list").description("List agents").action(async () => {
8653
- const board = await open2();
8967
+ const board = await open3();
8654
8968
  const agents = await board.listAgents();
8655
8969
  const members = await board.listMembers();
8656
8970
  print(agents, () => agents.map((a) => {
@@ -8659,28 +8973,28 @@ agent.command("list").description("List agents").action(async () => {
8659
8973
  }).join("\n"));
8660
8974
  });
8661
8975
  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();
8976
+ const board = await open3();
8663
8977
  const updated = await board.setAgentModel(board.userActor(), name, model ?? null);
8664
8978
  print(updated, () => `${updated.name} runs with ${updated.model ?? "its CLI's default model"} from its next turn`);
8665
8979
  });
8666
8980
  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();
8981
+ const board = await open3();
8668
8982
  const updated = await board.setAgentEffort(board.userActor(), name, effort ?? null);
8669
8983
  print(updated, () => `${updated.name} runs with ${updated.effort === void 0 ? "its model's default effort" : `${updated.effort} effort`} from its next turn`);
8670
8984
  });
8671
8985
  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();
8986
+ const board = await open3();
8673
8987
  const retired = await board.retireAgent(board.userActor(), { name, reason: opts.reason });
8674
8988
  print(retired, () => `Agent ${retired.name} retired: ${opts.reason}`);
8675
8989
  });
8676
8990
  var proposal = program.command("proposal").description("Governance proposals");
8677
8991
  proposal.command("list").description("List proposals").action(async () => {
8678
- const board = await open2();
8992
+ const board = await open3();
8679
8993
  const proposals = await board.listProposals();
8680
8994
  print(proposals, () => proposals.length === 0 ? "No proposals." : proposals.map((p) => `${p.id} ${p.kind} ${p.status} by ${p.proposedBy}`).join("\n"));
8681
8995
  });
8682
8996
  proposal.command("show <id>").description("Show a proposal with its charter and rationale").action(async (id) => {
8683
- const board = await open2();
8997
+ const board = await open3();
8684
8998
  const found = await board.readProposal(id);
8685
8999
  print(found, () => [
8686
9000
  `${found.id} ${found.kind} ${found.status} by ${found.proposedBy}`,
@@ -8690,7 +9004,7 @@ proposal.command("show <id>").description("Show a proposal with its charter and
8690
9004
  ].filter((line) => line.length > 0).join("\n"));
8691
9005
  });
8692
9006
  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();
9007
+ const board = await open3();
8694
9008
  const created = await board.propose(await actorFor(board, opts.as), {
8695
9009
  kind: ProposalKindSchema.parse(opts.kind),
8696
9010
  charter: parseCharter(opts.charter),
@@ -8699,7 +9013,7 @@ proposal.command("create").description("Propose a member, role, channel, realloc
8699
9013
  print(created, () => `Proposal ${created.id} (${created.kind}) is ${created.status}`);
8700
9014
  });
8701
9015
  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();
9016
+ const board = await open3();
8703
9017
  const decision = await board.approve(await actorFor(board, opts.as), {
8704
9018
  proposal_id: id,
8705
9019
  ...opts.reason === void 0 ? {} : { reason: opts.reason }
@@ -8708,7 +9022,7 @@ proposal.command("approve <id>").description("Approve a proposal; the board prov
8708
9022
  print({ decision, proposal: after }, () => `Proposal ${id} approved; it is now ${after.status}`);
8709
9023
  });
8710
9024
  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();
9025
+ const board = await open3();
8712
9026
  const decision = await board.reject(await actorFor(board, opts.as), {
8713
9027
  proposal_id: id,
8714
9028
  reason: opts.reason
@@ -8717,14 +9031,14 @@ proposal.command("reject <id>").description("Reject a proposal with a reason").r
8717
9031
  });
8718
9032
  var role = program.command("role").description("Role charters");
8719
9033
  role.command("list").description("List role charters").action(async () => {
8720
- const board = await open2();
9034
+ const board = await open3();
8721
9035
  const roles = await board.listRoles();
8722
9036
  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
9037
  });
8724
9038
  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
9039
  "heartbeat"
8726
9040
  ]).action(async (name, opts) => {
8727
- const board = await open2();
9041
+ const board = await open3();
8728
9042
  const added = await board.setRoleCharter(board.userActor(), {
8729
9043
  name,
8730
9044
  purpose: opts.purpose,
@@ -8734,7 +9048,7 @@ role.command("add <name>").description("Write a new role charter directly as the
8734
9048
  print(added, () => `Role ${added.name} written with ${added.verbs.length} verbs`);
8735
9049
  });
8736
9050
  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();
9051
+ const board = await open3();
8738
9052
  const current = await board.readRole(name);
8739
9053
  const updated = await board.setRoleCharter(board.userActor(), {
8740
9054
  ...current,
@@ -8746,7 +9060,7 @@ role.command("set <name>").description("Change a charter directly as the user: s
8746
9060
  });
8747
9061
  var channelCommand = program.command("channel").description("Channels");
8748
9062
  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();
9063
+ const board = await open3();
8750
9064
  const parsed = parseChannelRef(ref);
8751
9065
  const added = await board.addChannel(await actorFor(board, opts.as), {
8752
9066
  project: parsed.project,
@@ -8756,7 +9070,7 @@ channelCommand.command("add <ref>").description("Open a channel: <name> for the
8756
9070
  print({ channel: added }, () => `Channel ${added} opened`);
8757
9071
  });
8758
9072
  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();
9073
+ const board = await open3();
8760
9074
  const archived = await board.archiveChannel(await actorFor(board, opts.as), {
8761
9075
  channel: ref,
8762
9076
  reason: opts.reason
@@ -8764,12 +9078,12 @@ channelCommand.command("archive <ref>").description("Archive a channel whose wor
8764
9078
  print(archived, () => `Channel ${ref} archived: ${archived.reason}`);
8765
9079
  });
8766
9080
  program.command("signals").description("Recent operations signals, oldest first").option("--limit <n>", "how many", "50").action(async (opts) => {
8767
- const board = await open2();
9081
+ const board = await open3();
8768
9082
  const signals = await board.listSignals(Number(opts.limit));
8769
9083
  print(signals, () => signals.length === 0 ? "No signals yet." : signals.map((s) => `[${s.ts}] ${s.signal.kind} ${s.signal.summary}`).join("\n"));
8770
9084
  });
8771
9085
  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();
9086
+ const board = await open3();
8773
9087
  const who = await actorFor(board, opts.as);
8774
9088
  const message = await board.postMessage(who, {
8775
9089
  channel,
@@ -8779,13 +9093,13 @@ program.command("post <channel> <body>").description("Post a message to a channe
8779
9093
  print(message, () => `Posted ${message.id} to ${message.channel}${message.thread === void 0 ? "" : ` (thread ${message.thread})`}`);
8780
9094
  });
8781
9095
  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();
9096
+ const board = await open3();
8783
9097
  const message = await board.postMessage(board.userActor(), { channel: "general", body: text });
8784
9098
  print(message, () => `Posted ${message.id} to general; the concierge wakes on it`);
8785
9099
  });
8786
9100
  var task = program.command("task").description("Manage tasks");
8787
9101
  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();
9102
+ const board = await open3();
8789
9103
  const who = await actorFor(board, opts.as);
8790
9104
  const created = await board.createTask(who, {
8791
9105
  project: projectSlug,
@@ -8801,17 +9115,17 @@ task.command("create <project> <title>").description("Create a task with a plan,
8801
9115
  ].join("\n"));
8802
9116
  });
8803
9117
  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();
9118
+ const board = await open3();
8805
9119
  const claimed = await board.claimTask(await actorFor(board, opts.as), { task_id: id });
8806
9120
  print(claimed, () => `Stage ${claimed.stage} of task ${claimed.id} held by ${claimed.claimedBy ?? "?"} until ${claimed.leaseExpiresAt ?? "?"}`);
8807
9121
  });
8808
9122
  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();
9123
+ const board = await open3();
8810
9124
  const released = await board.releaseTask(await actorFor(board, opts.as), { task_id: id });
8811
9125
  print(released, () => `Stage ${released.stage} of task ${released.id} released`);
8812
9126
  });
8813
9127
  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();
9128
+ const board = await open3();
8815
9129
  const advanced = await board.advanceTask(await actorFor(board, opts.as), {
8816
9130
  task_id: id,
8817
9131
  ...opts.note === void 0 ? {} : { note: opts.note }
@@ -8820,7 +9134,7 @@ task.command("advance <id>").description("Finish the current stage; past the las
8820
9134
  ${describePlan(advanced)}`);
8821
9135
  });
8822
9136
  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();
9137
+ const board = await open3();
8824
9138
  const planned = await board.planTask(await actorFor(board, opts.as), {
8825
9139
  task_id: id,
8826
9140
  stages: PlanEditSchema.parse(JSON.parse(stages)),
@@ -8829,7 +9143,7 @@ task.command("plan <id> <stages>").description("Reshape the stages ahead: pass e
8829
9143
  print(planned, () => describePlan(planned));
8830
9144
  });
8831
9145
  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();
9146
+ const board = await open3();
8833
9147
  const updated = await board.updateTask(await actorFor(board, opts.as), {
8834
9148
  task_id: id,
8835
9149
  ...opts.stage === void 0 ? {} : { stage: opts.stage },
@@ -8840,7 +9154,7 @@ task.command("update <id>").description("Send a task back to an earlier stage, a
8840
9154
  print(updated, () => `Task ${updated.id} is ${updated.status} at ${updated.stage}`);
8841
9155
  });
8842
9156
  task.command("show <id>").description("Show a task with its plan and its thread").action(async (id) => {
8843
- const board = await open2();
9157
+ const board = await open3();
8844
9158
  const found = await board.getTask(board.userActor(), { task_id: id });
8845
9159
  const threadState = await board.readThread(id).then((t) => t.state).catch(() => "none");
8846
9160
  const said = found.messages.map((message) => {
@@ -8860,7 +9174,7 @@ ${message.body.trim()}
8860
9174
  ].join("\n"));
8861
9175
  });
8862
9176
  task.command("list <project>").description("List a project's tasks with their current stage").action(async (projectSlug) => {
8863
- const board = await open2();
9177
+ const board = await open3();
8864
9178
  const tasks = await board.listTasks(projectSlug);
8865
9179
  print(tasks, () => tasks.length === 0 ? "No tasks." : tasks.map((t) => {
8866
9180
  const stage = t.stages[stageIndex(t, t.stage)];
@@ -8869,7 +9183,7 @@ task.command("list <project>").description("List a project's tasks with their cu
8869
9183
  });
8870
9184
  var thread = program.command("thread").description("Manage threads");
8871
9185
  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();
9186
+ const board = await open3();
8873
9187
  const opened = await board.openThread(await actorFor(board, opts.as), {
8874
9188
  ...taskId === void 0 ? {} : { task_id: taskId },
8875
9189
  ...opts.proposal === void 0 ? {} : { proposal_id: opts.proposal },
@@ -8879,7 +9193,7 @@ thread.command("open [taskId]").description("Open a thread on a task, on a propo
8879
9193
  print(opened, () => `Thread ${opened.id} opened on ${opened.channel}: ${opened.title}`);
8880
9194
  });
8881
9195
  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();
9196
+ const board = await open3();
8883
9197
  const summary = await board.closeThread(await actorFor(board, opts.as), {
8884
9198
  thread_id: threadId,
8885
9199
  summary: opts.summary
@@ -8887,7 +9201,7 @@ thread.command("close <threadId>").description("Close a thread with a summary po
8887
9201
  print(summary, () => `Thread closed; summary posted as ${summary.id} to ${summary.channel}`);
8888
9202
  });
8889
9203
  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();
9204
+ const board = await open3();
8891
9205
  const threads = (await board.listThreads()).filter((t) => !opts.open || t.state === "open");
8892
9206
  print(threads, () => threads.length === 0 ? "No threads." : threads.map((t) => {
8893
9207
  const about = t.subject === void 0 ? "topic" : t.subject.kind;
@@ -8895,7 +9209,7 @@ thread.command("list").description("List threads, the society's first, then each
8895
9209
  }).join("\n"));
8896
9210
  });
8897
9211
  thread.command("show <threadId>").description("Show a thread and its messages").action(async (threadId) => {
8898
- const board = await open2();
9212
+ const board = await open3();
8899
9213
  const found = await board.readThread(threadId);
8900
9214
  const messages = await board.listThread(threadId);
8901
9215
  print({ thread: found, messages }, () => [
@@ -8906,17 +9220,17 @@ thread.command("show <threadId>").description("Show a thread and its messages").
8906
9220
  ].join("\n"));
8907
9221
  });
8908
9222
  program.command("pause").description("Stop all wakeups; turns in flight finish").action(async () => {
8909
- const board = await open2();
9223
+ const board = await open3();
8910
9224
  await board.setPaused(board.userActor(), true);
8911
9225
  print({ paused: true }, () => "Society paused");
8912
9226
  });
8913
9227
  program.command("resume").description("Allow wakeups again").action(async () => {
8914
- const board = await open2();
9228
+ const board = await open3();
8915
9229
  await board.setPaused(board.userActor(), false);
8916
9230
  print({ paused: false }, () => "Society resumed");
8917
9231
  });
8918
9232
  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();
9233
+ const board = await open3();
8920
9234
  const society = zone === void 0 ? await board.society() : await board.setTimezone(board.userActor(), zone);
8921
9235
  print({ timezone: society.timezone }, () => `Time zone: ${society.timezone}`);
8922
9236
  });
@@ -8924,7 +9238,8 @@ function describeCron(cron2, timeZone) {
8924
9238
  const where = [
8925
9239
  cron2.scope,
8926
9240
  cron2.thread,
8927
- cron2.channel === void 0 ? void 0 : `#${cron2.channel}`
9241
+ cron2.channel === void 0 ? void 0 : `#${cron2.channel}`,
9242
+ cron2.place === void 0 ? void 0 : `~${cron2.place}`
8928
9243
  ].filter((part) => part !== void 0).join("/");
8929
9244
  const next = nextFire(cron2, timeZone);
8930
9245
  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 +9247,13 @@ function describeCron(cron2, timeZone) {
8932
9247
  }
8933
9248
  var cron = program.command("cron").description("Crons that wake citizens on a clock");
8934
9249
  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();
9250
+ const board = await open3();
8936
9251
  const { timezone } = await board.society();
8937
9252
  const crons = (await board.listCrons()).filter((each) => opts.all || each.ended === void 0);
8938
9253
  print(crons, () => crons.length === 0 ? "No crons." : crons.map((each) => describeCron(each, timezone)).join("\n"));
8939
9254
  });
8940
9255
  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();
9256
+ const board = await open3();
8942
9257
  const created = await board.createCron(await actorFor(board, opts.as), {
8943
9258
  title,
8944
9259
  note: opts.note,
@@ -8958,7 +9273,7 @@ for (const [name, paused] of [
8958
9273
  ["resume", false]
8959
9274
  ]) {
8960
9275
  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();
9276
+ const board = await open3();
8962
9277
  const updated = await board.updateCron(await actorFor(board, opts.as), {
8963
9278
  cron_id: id,
8964
9279
  paused
@@ -8968,7 +9283,7 @@ for (const [name, paused] of [
8968
9283
  });
8969
9284
  }
8970
9285
  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();
9286
+ const board = await open3();
8972
9287
  const removed = await board.removeCron(await actorFor(board, opts.as), {
8973
9288
  cron_id: id,
8974
9289
  reason: opts.reason
@@ -8977,12 +9292,12 @@ cron.command("remove <id>").description("End a cron for good").requiredOption("-
8977
9292
  });
8978
9293
  var knowledge = program.command("knowledge").description("Shared knowledge, by project or society");
8979
9294
  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();
9295
+ const board = await open3();
8981
9296
  const topics = await board.listKnowledge(projectSlug ?? null);
8982
9297
  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
9298
  });
8984
9299
  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();
9300
+ const board = await open3();
8986
9301
  const found = (await board.listKnowledge(opts.project ?? null)).find((t) => t.topic === topic);
8987
9302
  if (found === void 0) {
8988
9303
  throw new BoardError("NOT_FOUND", `no knowledge topic ${topic}`);
@@ -8990,8 +9305,8 @@ knowledge.command("show <topic>").description("Print a knowledge topic").option(
8990
9305
  print(found, () => found.body);
8991
9306
  });
8992
9307
  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");
9308
+ const board = await open3();
9309
+ const body = opts.file === void 0 ? await readStdin() : await readFile5(path8.resolve(opts.file), "utf8");
8995
9310
  const written = await board.writeKnowledge(await actorFor(board, opts.as), {
8996
9311
  project: opts.project ?? null,
8997
9312
  topic,
@@ -9002,7 +9317,7 @@ knowledge.command("write <topic>").description("Write a knowledge topic as the u
9002
9317
  });
9003
9318
  var skill = program.command("skill").description("Skills promoted to the society");
9004
9319
  skill.command("list").description("List the society's skills, and each member's own").action(async () => {
9005
- const board = await open2();
9320
+ const board = await open3();
9006
9321
  const society = await board.listSocietySkills();
9007
9322
  const own = [];
9008
9323
  for (const member of await board.listAgents()) {
@@ -9017,7 +9332,7 @@ skill.command("list").description("List the society's skills, and each member's
9017
9332
  ].join("\n") || "No skills yet.");
9018
9333
  });
9019
9334
  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();
9335
+ const board = await open3();
9021
9336
  const removed = await board.removeSkill(await actorFor(board, opts.as), {
9022
9337
  name,
9023
9338
  reason: opts.reason
@@ -9026,13 +9341,13 @@ skill.command("remove <name>").description("Remove a skill from the society's sk
9026
9341
  });
9027
9342
  var turn = program.command("turn").description("Turn operations");
9028
9343
  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();
9344
+ const board = await open3();
9030
9345
  const actor = await board.actorFor(agentName);
9031
9346
  const digest = await board.readDigest(opts.project === void 0 ? actor : { ...actor, scope: opts.project }, { advance: false, limit: Number(opts.limit) });
9032
9347
  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
9348
  });
9034
9349
  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();
9350
+ const board = await open3();
9036
9351
  const event = await board.requestWake(board.userActor(), {
9037
9352
  agent: agentName,
9038
9353
  project: opts.project,