@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
@@ -2,12 +2,12 @@
2
2
 
3
3
  // apps/server/dist/index.js
4
4
  import { access as access2 } from "node:fs/promises";
5
- import path9 from "node:path";
5
+ import path10 from "node:path";
6
6
  import { serve } from "@hono/node-server";
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
 
@@ -235,6 +235,7 @@ var CronFrontmatterSchema = z3.object({
235
235
  scope: NameSchema,
236
236
  thread: UlidSchema.optional(),
237
237
  channel: NameSchema.optional(),
238
+ place: NameSchema.optional(),
238
239
  schedule: CronScheduleSchema,
239
240
  createdBy: NameSchema,
240
241
  createdAt: IsoDateTimeSchema,
@@ -384,15 +385,16 @@ function quotaLevel(quota, now) {
384
385
  var WINDOW_NAMES = {
385
386
  five_hour: "5h",
386
387
  seven_day: "7d",
387
- seven_day_opus: "7d Opus",
388
- seven_day_sonnet: "7d Sonnet",
388
+ seven_day_opus: "7d opus",
389
+ seven_day_sonnet: "7d sonnet",
389
390
  seven_day_oauth_apps: "7d apps",
390
391
  seven_day_overage_included: "7d extra",
391
392
  overage: "extra usage"
392
393
  };
393
394
  function windowName(window) {
394
395
  if (window.label !== void 0) {
395
- return window.minutes === 10080 ? `7d ${window.label}` : window.label;
396
+ const label = window.label.toLowerCase();
397
+ return window.minutes === 10080 ? `7d ${label}` : label;
396
398
  }
397
399
  const named = WINDOW_NAMES[window.key];
398
400
  if (named !== void 0) {
@@ -991,7 +993,8 @@ var DashboardSchema = z5.object({
991
993
  var ProposalOriginSchema = z5.object({
992
994
  scope: NameSchema,
993
995
  thread: UlidSchema.optional(),
994
- channel: NameSchema.optional()
996
+ channel: NameSchema.optional(),
997
+ place: NameSchema.optional()
995
998
  });
996
999
  var SKILL_FILES_LIMIT = 100;
997
1000
  var SKILL_BYTES_LIMIT = 10 * 1024 * 1024;
@@ -1385,6 +1388,7 @@ var TurnExitReasonSchema = z8.enum([
1385
1388
  "blocked",
1386
1389
  "stopped"
1387
1390
  ]);
1391
+ var ResumeFailureSchema = z8.enum(["lost", "retry"]);
1388
1392
  var AgentEventSchema = z8.discriminatedUnion("type", [
1389
1393
  z8.object({
1390
1394
  type: z8.literal("turn_started"),
@@ -1436,6 +1440,8 @@ var LiveTurnEventSchema = z8.object({
1436
1440
  thread: UlidSchema.optional(),
1437
1441
  /** The channel, never general, whose conversation the turn is in. */
1438
1442
  channel: NameSchema.optional(),
1443
+ /** The project whose channels' conversation the turn is in, for a turn in the society scope. */
1444
+ place: NameSchema.optional(),
1439
1445
  event: AgentEventSchema
1440
1446
  });
1441
1447
 
@@ -1538,7 +1544,8 @@ var TurnWorkspaceSchema = z9.discriminatedUnion("kind", [
1538
1544
  z9.object({
1539
1545
  kind: z9.literal("home"),
1540
1546
  thread: UlidSchema.optional(),
1541
- channel: NameSchema.optional()
1547
+ channel: NameSchema.optional(),
1548
+ place: NameSchema.optional()
1542
1549
  }),
1543
1550
  z9.object({
1544
1551
  kind: z9.literal("project"),
@@ -1547,7 +1554,16 @@ var TurnWorkspaceSchema = z9.discriminatedUnion("kind", [
1547
1554
  thread: UlidSchema.optional(),
1548
1555
  channel: NameSchema.optional()
1549
1556
  }),
1550
- z9.object({ kind: z9.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
1557
+ z9.object({
1558
+ kind: z9.literal("task"),
1559
+ repo: ProjectRepoSchema,
1560
+ taskId: UlidSchema,
1561
+ /**
1562
+ * The citizen holding the task's current stage, when someone does: a turn of anyone else
1563
+ * works detached at the branch's tip, so the holder's turns find the branch free.
1564
+ */
1565
+ holder: NameSchema.optional()
1566
+ })
1551
1567
  ]);
1552
1568
  var TurnJobSchema = z9.object({
1553
1569
  turnId: UlidSchema,
@@ -1563,6 +1579,8 @@ var TurnJobSchema = z9.object({
1563
1579
  thread: UlidSchema.optional(),
1564
1580
  /** The channel, never general, whose conversation the turn is in. */
1565
1581
  channel: NameSchema.optional(),
1582
+ /** The project whose channels' conversation the turn is in, for a turn in the society scope. */
1583
+ place: NameSchema.optional(),
1566
1584
  /**
1567
1585
  * The session to resume, or null to start one: the CLI's adapter picks a fresh session's id, and
1568
1586
  * the outcome reports it.
@@ -1583,6 +1601,12 @@ var TurnJobSchema = z9.object({
1583
1601
  workspace: TurnWorkspaceSchema
1584
1602
  });
1585
1603
  var TurnWorkSchema = z9.object({ branch: z9.string().min(1), head: z9.string().min(1) });
1604
+ var StuckWorkSchema = z9.object({
1605
+ branch: z9.string().min(1),
1606
+ agent: NameSchema,
1607
+ commits: z9.array(z9.string()),
1608
+ total: z9.number().int().positive()
1609
+ });
1586
1610
  var TurnEventsSchema = z9.object({ entries: z9.array(TranscriptEntrySchema) });
1587
1611
  var TurnOutcomeSchema = z9.object({
1588
1612
  exitReason: TurnExitReasonSchema,
@@ -1592,8 +1616,18 @@ var TurnOutcomeSchema = z9.object({
1592
1616
  costUsd: z9.number().nonnegative(),
1593
1617
  /** The session's running total after this turn, for CLIs that report one. */
1594
1618
  sessionCostUsd: z9.number().nonnegative().optional(),
1595
- /** The session the CLI used, which differs from the job's for CLIs that assign their own ids. */
1596
- session: z9.string().min(1),
1619
+ /**
1620
+ * The session the CLI used, which differs from the job's for CLIs that assign their own ids. A
1621
+ * turn whose CLI never started, because it failed or was stopped first, reports the job's own,
1622
+ * null in a conversation's first turn, so a session never started is not recorded.
1623
+ */
1624
+ session: z9.string().min(1).nullable(),
1625
+ /**
1626
+ * Set when the CLI could not resume the job's session, which failed the turn before any model
1627
+ * call; the server then drops a `lost` session, and one that failed `retry` too often in a row. A
1628
+ * runner from before never sends it.
1629
+ */
1630
+ resumeFailed: ResumeFailureSchema.optional(),
1597
1631
  /** The model the CLI reported, when it reported one. */
1598
1632
  model: z9.string().nullable(),
1599
1633
  work: TurnWorkSchema.nullable(),
@@ -1602,6 +1636,11 @@ var TurnOutcomeSchema = z9.object({
1602
1636
  * branch after the hand-back: uncommitted changes, or commits no branch contains.
1603
1637
  */
1604
1638
  leftovers: z9.boolean().default(false),
1639
+ /**
1640
+ * Commits the hand-back found its task's worktrees holding that no fast-forward of the task's
1641
+ * branch can take, each told once; a runner from before never sends it.
1642
+ */
1643
+ stuck: z9.array(StuckWorkSchema).optional(),
1605
1644
  /**
1606
1645
  * The CLI's plan quota as the turn's stream last reported it, when it reported any; a runner from
1607
1646
  * before never sends it.
@@ -1720,13 +1759,17 @@ var TurnImageSchema = z9.discriminatedUnion("kind", [
1720
1759
  ]);
1721
1760
  var WorkspaceConversationSchema = z9.discriminatedUnion("kind", [
1722
1761
  z9.object({ kind: z9.literal("thread"), id: UlidSchema }),
1723
- z9.object({ kind: z9.literal("channel"), scope: NameSchema, name: NameSchema })
1762
+ z9.object({ kind: z9.literal("channel"), scope: NameSchema, name: NameSchema }),
1763
+ z9.object({ kind: z9.literal("place"), project: NameSchema })
1724
1764
  ]);
1725
1765
  function workspaceConversationOf(turn) {
1726
1766
  if (turn.thread !== void 0) {
1727
1767
  return { kind: "thread", id: turn.thread };
1728
1768
  }
1729
- return turn.channel === void 0 ? null : { kind: "channel", scope: turn.scope, name: turn.channel };
1769
+ if (turn.channel !== void 0) {
1770
+ return { kind: "channel", scope: turn.scope, name: turn.channel };
1771
+ }
1772
+ return turn.place === void 0 ? null : { kind: "place", project: turn.place };
1730
1773
  }
1731
1774
  var HeldWorkspaceSchema = z9.object({
1732
1775
  agent: NameSchema,
@@ -1832,6 +1875,7 @@ var TurnDispatchSchema = z10.object({
1832
1875
  project: NameSchema,
1833
1876
  thread: TurnThreadSchema.optional(),
1834
1877
  channel: NameSchema.optional(),
1878
+ place: NameSchema.optional(),
1835
1879
  trigger: TriggerSchema,
1836
1880
  priority: z10.number().int().min(0).max(2),
1837
1881
  onboarding: z10.boolean().default(false),
@@ -1839,7 +1883,13 @@ var TurnDispatchSchema = z10.object({
1839
1883
  crons: z10.array(UlidSchema).optional()
1840
1884
  });
1841
1885
  function conversationPart(conversation) {
1842
- return conversation.thread ?? (conversation.channel === void 0 ? void 0 : `#${conversation.channel}`);
1886
+ if (conversation.thread !== void 0) {
1887
+ return conversation.thread;
1888
+ }
1889
+ if (conversation.channel !== void 0) {
1890
+ return `#${conversation.channel}`;
1891
+ }
1892
+ return conversation.place === void 0 ? void 0 : `~${conversation.place}`;
1843
1893
  }
1844
1894
  function sessionKey(agent, scope, part) {
1845
1895
  return part === void 0 ? `${agent}/${scope}` : `${agent}/${scope}/${part}`;
@@ -1852,7 +1902,10 @@ function parseSessionKey(key) {
1852
1902
  if (part === void 0) {
1853
1903
  return { agent, scope };
1854
1904
  }
1855
- return part.startsWith("#") ? { agent, scope, channel: part.slice(1) } : { agent, scope, thread: part };
1905
+ if (part.startsWith("#")) {
1906
+ return { agent, scope, channel: part.slice(1) };
1907
+ }
1908
+ return part.startsWith("~") ? { agent, scope, place: part.slice(1) } : { agent, scope, thread: part };
1856
1909
  }
1857
1910
  function channelConversation(scope, channel) {
1858
1911
  const at = channel.indexOf("/");
@@ -1860,6 +1913,10 @@ function channelConversation(scope, channel) {
1860
1913
  const name = at < 0 ? channel : channel.slice(at + 1);
1861
1914
  return place === scope && name !== "general" ? name : void 0;
1862
1915
  }
1916
+ function placeConversation(scope, channel) {
1917
+ const at = channel.indexOf("/");
1918
+ return scope === SOCIETY_SCOPE && at >= 0 ? channel.slice(0, at) : void 0;
1919
+ }
1863
1920
  var TurnRecordSchema = z10.object({
1864
1921
  /** Assigned by the board when the turn begins; the turn's transcript is filed under it. */
1865
1922
  id: UlidSchema.optional(),
@@ -1869,6 +1926,8 @@ var TurnRecordSchema = z10.object({
1869
1926
  thread: UlidSchema.optional(),
1870
1927
  /** The channel, never general, whose conversation the turn was in. */
1871
1928
  channel: NameSchema.optional(),
1929
+ /** The project whose channels' conversation the turn was in, for a turn in the society scope. */
1930
+ place: NameSchema.optional(),
1872
1931
  runner: NameSchema,
1873
1932
  cli: CliKindSchema.nullable(),
1874
1933
  session: z10.string().nullable(),
@@ -1892,6 +1951,12 @@ var TurnRecordSchema = z10.object({
1892
1951
  work: TurnWorkSchema.optional(),
1893
1952
  /** Who stopped the turn, for one that ended `stopped`. */
1894
1953
  stoppedBy: NameSchema.optional(),
1954
+ /** Why the CLI could not resume the turn's session, for a turn that failed so. */
1955
+ resumeFailed: ResumeFailureSchema.optional(),
1956
+ /** How many turns in a row, this one included, could not resume the session for a `retry`. */
1957
+ resumeRetries: z10.number().int().positive().optional(),
1958
+ /** Whether the server dropped the conversation's session after this turn, which the next starts anew. */
1959
+ sessionDropped: z10.boolean().optional(),
1895
1960
  /** The crons whose fire started the turn, which record how it ended. */
1896
1961
  crons: z10.array(UlidSchema).optional()
1897
1962
  });
@@ -1901,6 +1966,7 @@ var RunningTurnSchema = z10.object({
1901
1966
  scope: NameSchema,
1902
1967
  thread: UlidSchema.optional(),
1903
1968
  channel: NameSchema.optional(),
1969
+ place: NameSchema.optional(),
1904
1970
  cli: CliKindSchema,
1905
1971
  askedIn: ChannelRefSchema.optional(),
1906
1972
  steerable: z10.boolean(),
@@ -1931,6 +1997,8 @@ var TurnHistoryEntrySchema = z10.object({
1931
1997
  thread: UlidSchema.optional(),
1932
1998
  /** The channel, never general, whose conversation the turn was in. */
1933
1999
  channel: NameSchema.optional(),
2000
+ /** The project whose channels' conversation the turn was in, for a turn in the society scope. */
2001
+ place: NameSchema.optional(),
1934
2002
  trigger: z10.string(),
1935
2003
  exitReason: z10.string().nullable(),
1936
2004
  costUsd: z10.number(),
@@ -2178,7 +2246,7 @@ var RunnerConfigSchema = z13.object({
2178
2246
  });
2179
2247
 
2180
2248
  // packages/shared/dist/version.js
2181
- var STELLARIS_VERSION = "0.3.23";
2249
+ var STELLARIS_VERSION = "0.3.24";
2182
2250
 
2183
2251
  // packages/shared/dist/mentions.js
2184
2252
  import { parse, postprocess, preprocess } from "micromark";
@@ -2562,6 +2630,7 @@ var HOME_GITIGNORE = [
2562
2630
  "/projects/*/last-turn.json",
2563
2631
  "/projects/*/threads/",
2564
2632
  "/projects/*/channels/",
2633
+ "/projects/*/places/",
2565
2634
  `/${HOME_SCRATCH}/`,
2566
2635
  "*.tmp",
2567
2636
  "node_modules/",
@@ -2579,7 +2648,7 @@ var HOME_GITIGNORE = [
2579
2648
  var IGNORE_FILE = `${HOME_GITIGNORE.join("\n")}
2580
2649
  `;
2581
2650
  var PRE_RECEIVE = `#!/bin/sh
2582
- forbidden='^(agent\\.json|cursors\\.json|role\\.md|turns/|\\.claude/|\\.codex/|projects/[^/]+/(sessions[^/]*\\.json|last-turn\\.json|threads/|channels/))'
2651
+ forbidden='^(agent\\.json|cursors\\.json|role\\.md|turns/|\\.claude/|\\.codex/|projects/[^/]+/(sessions[^/]*\\.json|last-turn\\.json|threads/|channels/|places/))'
2583
2652
  while read old new ref; do
2584
2653
  if [ "$ref" != "refs/heads/main" ]; then
2585
2654
  echo "a home takes pushes to main only" >&2
@@ -2905,6 +2974,7 @@ var TurnPayload = z14.object({
2905
2974
  project: z14.string(),
2906
2975
  thread: z14.string().optional(),
2907
2976
  channel: z14.string().optional(),
2977
+ place: z14.string().optional(),
2908
2978
  trigger: z14.string()
2909
2979
  });
2910
2980
  var TaskPayload = z14.object({
@@ -2950,11 +3020,16 @@ function answers(mention, turn) {
2950
3020
  if (!mention.scopes.includes(turn.project)) {
2951
3021
  return false;
2952
3022
  }
2953
- const own = mention.thread === null ? channelConversation(turn.project, mention.channel) : void 0;
3023
+ const topLevel = mention.thread === null;
3024
+ const own = topLevel ? channelConversation(turn.project, mention.channel) : void 0;
3025
+ const place = topLevel ? placeConversation(turn.project, mention.channel) : void 0;
2954
3026
  if (turn.channel !== null) {
2955
- return mention.thread === null && own === turn.channel;
3027
+ return topLevel && own === turn.channel;
2956
3028
  }
2957
- return mention.thread === null && own === void 0 || turn.trigger === "mention";
3029
+ if (turn.place !== null) {
3030
+ return topLevel && place === turn.place;
3031
+ }
3032
+ return topLevel && own === void 0 && place === void 0 || turn.trigger === "mention";
2958
3033
  }
2959
3034
  function computeMetrics(input) {
2960
3035
  const { events, now, window } = input;
@@ -3020,6 +3095,7 @@ function computeMetrics(input) {
3020
3095
  at,
3021
3096
  thread: turn.data.thread ?? null,
3022
3097
  channel: turn.data.channel ?? null,
3098
+ place: turn.data.place ?? null,
3023
3099
  project: turn.data.project,
3024
3100
  trigger: turn.data.trigger
3025
3101
  };
@@ -3219,6 +3295,7 @@ var TurnHistoryPayloadSchema = z15.object({
3219
3295
  project: NameSchema,
3220
3296
  thread: UlidSchema.optional(),
3221
3297
  channel: NameSchema.optional(),
3298
+ place: NameSchema.optional(),
3222
3299
  trigger: z15.string().default("manual"),
3223
3300
  exitReason: z15.string().nullable().default(null),
3224
3301
  costUsd: z15.number().default(0),
@@ -3457,11 +3534,14 @@ var BoardPaths = class {
3457
3534
  return path5.join(this.agentProjects(name), slug);
3458
3535
  }
3459
3536
  /**
3460
- * A thread's or a channel's conversation's session and last turn, beside the scope's home
3461
- * conversation; `part` is a thread's id or `#channel`.
3537
+ * A thread's, a channel's, or, in the society scope, a project's conversation's session and last
3538
+ * turn, beside the scope's home conversation; `part` is a thread's id, `#channel`, or `~project`.
3462
3539
  */
3463
3540
  agentConversation(name, slug, part) {
3464
- return part.startsWith("#") ? path5.join(this.agentProject(name, slug), "channels", part.slice(1)) : path5.join(this.agentProject(name, slug), "threads", part);
3541
+ if (part.startsWith("#")) {
3542
+ return path5.join(this.agentProject(name, slug), "channels", part.slice(1));
3543
+ }
3544
+ return part.startsWith("~") ? path5.join(this.agentProject(name, slug), "places", part.slice(1)) : path5.join(this.agentProject(name, slug), "threads", part);
3465
3545
  }
3466
3546
  agentCursors(name) {
3467
3547
  return path5.join(this.agent(name), "cursors.json");
@@ -3495,6 +3575,101 @@ var BoardPaths = class {
3495
3575
  }
3496
3576
  };
3497
3577
 
3578
+ // packages/board-core/dist/skill-folder.js
3579
+ import { constants } from "node:fs";
3580
+ import { lstat, open as open2, readdir as readdir3, realpath } from "node:fs/promises";
3581
+ import path6 from "node:path";
3582
+ var SKILL_TEXT_LIMIT_CHARS = 1e5;
3583
+ var SKILL_TEXT_LIMIT_BYTES = SKILL_TEXT_LIMIT_CHARS * 4;
3584
+ function skillFileText(bytes) {
3585
+ const text = bytes.includes(0) ? null : bytes.toString("utf8");
3586
+ return {
3587
+ text: text === null ? null : text.slice(0, SKILL_TEXT_LIMIT_CHARS),
3588
+ truncated: text !== null && text.length > SKILL_TEXT_LIMIT_CHARS
3589
+ };
3590
+ }
3591
+ function inside(root, file) {
3592
+ const relative = path6.relative(root, file);
3593
+ return relative !== ".." && !relative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(relative);
3594
+ }
3595
+ async function walk(root, prefix, found) {
3596
+ const entries = await readdir3(path6.join(root, ...prefix === "" ? [] : prefix.split("/")), {
3597
+ withFileTypes: true
3598
+ }).catch(() => []);
3599
+ for (const entry of entries.toSorted((a, b) => a.name < b.name ? -1 : 1)) {
3600
+ if (found.length >= SKILL_FILES_LIMIT) {
3601
+ return;
3602
+ }
3603
+ const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
3604
+ if (!isSafeRelativePath(relative)) {
3605
+ continue;
3606
+ }
3607
+ if (entry.isDirectory()) {
3608
+ await walk(root, relative, found);
3609
+ } else if (entry.isFile() && !entry.name.endsWith(".tmp")) {
3610
+ found.push(relative);
3611
+ }
3612
+ }
3613
+ }
3614
+ async function readEntry(root, relative) {
3615
+ let real;
3616
+ try {
3617
+ real = await realpath(path6.join(root, ...relative.split("/")));
3618
+ } catch {
3619
+ return null;
3620
+ }
3621
+ if (!inside(root, real)) {
3622
+ return null;
3623
+ }
3624
+ const handle = await open2(real, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK).catch(() => null);
3625
+ if (handle === null) {
3626
+ return null;
3627
+ }
3628
+ try {
3629
+ const stats = await handle.stat();
3630
+ if (!stats.isFile()) {
3631
+ return null;
3632
+ }
3633
+ const buffer = Buffer.alloc(Math.min(stats.size, SKILL_TEXT_LIMIT_BYTES));
3634
+ let size = 0;
3635
+ while (size < buffer.length) {
3636
+ const { bytesRead } = await handle.read(buffer, size, buffer.length - size, size);
3637
+ if (bytesRead === 0) {
3638
+ break;
3639
+ }
3640
+ size += bytesRead;
3641
+ }
3642
+ return {
3643
+ path: relative,
3644
+ size: stats.size,
3645
+ executable: (stats.mode & 73) !== 0,
3646
+ ...skillFileText(buffer.subarray(0, size))
3647
+ };
3648
+ } finally {
3649
+ await handle.close();
3650
+ }
3651
+ }
3652
+ async function readSkillFolder(dir) {
3653
+ const info = await lstat(dir).catch(() => null);
3654
+ if (info === null || !info.isDirectory()) {
3655
+ return null;
3656
+ }
3657
+ const root = await realpath(dir).catch(() => null);
3658
+ if (root === null) {
3659
+ return null;
3660
+ }
3661
+ const found = [];
3662
+ await walk(root, "", found);
3663
+ const files = [];
3664
+ for (const relative of found) {
3665
+ const file = await readEntry(root, relative);
3666
+ if (file !== null) {
3667
+ files.push(file);
3668
+ }
3669
+ }
3670
+ return files.some((file) => file.path === "SKILL.md") ? files : null;
3671
+ }
3672
+
3498
3673
  // packages/board-core/dist/tokens.js
3499
3674
  import { createHash as createHash2, randomBytes } from "node:crypto";
3500
3675
  function mintToken() {
@@ -3508,7 +3683,7 @@ function hashToken(token) {
3508
3683
  var SYSTEM_ACTOR = { name: "board", role: USER_ROLE };
3509
3684
  var CLOSING_ATTEMPTS = 3;
3510
3685
  function ownTurn(actor, turn) {
3511
- return turn.agent === actor.name && turn.scope === actor.scope && turn.thread === actor.thread && turn.channel === actor.channel;
3686
+ return turn.agent === actor.name && turn.scope === actor.scope && turn.thread === actor.thread && turn.channel === actor.channel && turn.place === actor.place;
3512
3687
  }
3513
3688
  function namesList(names) {
3514
3689
  return names.length <= 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}`;
@@ -3533,23 +3708,37 @@ var CursorsSchema = z16.object({
3533
3708
  scopes: z16.record(z16.string(), z16.string()).default({}),
3534
3709
  threadsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3535
3710
  channelsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3711
+ placesFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3536
3712
  /** How far a work role has read news, which goes to whichever of its general conversations reads first. */
3537
3713
  news: z16.string().optional()
3538
3714
  });
3539
- var NO_CURSORS = { digest: null, scopes: {}, threadsFrom: {}, channelsFrom: {} };
3715
+ var NO_CURSORS = {
3716
+ digest: null,
3717
+ scopes: {},
3718
+ threadsFrom: {},
3719
+ channelsFrom: {},
3720
+ placesFrom: {}
3721
+ };
3540
3722
  function conversationKey(scope, part) {
3541
3723
  return part === null ? scope : `${scope}/${part}`;
3542
3724
  }
3543
3725
  function channelOfPart(part) {
3544
3726
  return part?.startsWith("#") === true ? part.slice(1) : void 0;
3545
3727
  }
3728
+ function placeOfPart(part) {
3729
+ return part?.startsWith("~") === true ? part.slice(1) : void 0;
3730
+ }
3546
3731
  function threadOfPart(part) {
3547
- return part === null || part === void 0 || part.startsWith("#") ? void 0 : part;
3732
+ return part === null || part === void 0 || part.startsWith("#") || part.startsWith("~") ? void 0 : part;
3548
3733
  }
3549
3734
  function channelStart(cursors, scope) {
3550
3735
  const from = cursors.channelsFrom[scope];
3551
3736
  return from === void 0 ? cursors.digest : from;
3552
3737
  }
3738
+ function placeStart(cursors, scope) {
3739
+ const from = cursors.placesFrom[scope];
3740
+ return from === void 0 ? cursors.digest : from;
3741
+ }
3553
3742
  function threadStart(cursors, scope) {
3554
3743
  const from = cursors.threadsFrom[scope];
3555
3744
  return from === void 0 ? cursors.digest : from;
@@ -3565,7 +3754,7 @@ function cursorOf(cursors, scope, part = null) {
3565
3754
  if (part === null) {
3566
3755
  return cursors.scopes[scope] ?? cursors.digest;
3567
3756
  }
3568
- return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : threadStart(cursors, scope));
3757
+ return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : part.startsWith("~") ? placeStart(cursors, scope) : threadStart(cursors, scope));
3569
3758
  }
3570
3759
  function byId(a, b) {
3571
3760
  return a.message.id < b.message.id ? -1 : a.message.id > b.message.id ? 1 : 0;
@@ -3573,12 +3762,14 @@ function byId(a, b) {
3573
3762
  function turnTime(record) {
3574
3763
  return record.endedAt ?? record.startedAt;
3575
3764
  }
3765
+ function turnConversation(record) {
3766
+ 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` : "";
3767
+ }
3576
3768
  var PausedSchema = z16.object({ paused: z16.boolean() });
3577
3769
  var DEFAULT_LEASE_MS = 30 * 60 * 1e3;
3578
3770
  function withoutMentions(text) {
3579
3771
  return text.replaceAll(MENTION_PATTERN, "$1$2");
3580
3772
  }
3581
- var SKILL_TEXT_LIMIT_CHARS = 1e5;
3582
3773
  function skillFolderOf(proposal) {
3583
3774
  if (proposal.kind !== "skill") {
3584
3775
  return null;
@@ -3587,7 +3778,7 @@ function skillFolderOf(proposal) {
3587
3778
  return parsed.success && parsed.data.body === void 0 ? parsed.data.name : null;
3588
3779
  }
3589
3780
  function proposedFrom(origin, where) {
3590
- return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel;
3781
+ return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel && origin.place === where.place;
3591
3782
  }
3592
3783
  function megabytes(bytes) {
3593
3784
  return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "");
@@ -3609,6 +3800,7 @@ var THREAD_CONVERSATIONS = "thread-conversations";
3609
3800
  var ASKS_CHANNEL_OPENED = "asks-channel-opened";
3610
3801
  var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
3611
3802
  var CHANNEL_CONVERSATIONS = "channel-conversations";
3803
+ var PLACE_CONVERSATIONS = "place-conversations";
3612
3804
  var DASHBOARD_UPDATES = "dashboard-updates-granted";
3613
3805
  var CRON_VERBS = "cron-verbs-granted";
3614
3806
  var KNOWLEDGE_NOTICES = "knowledge-notices";
@@ -3683,8 +3875,8 @@ function restage(existing, edit) {
3683
3875
  };
3684
3876
  }
3685
3877
  function reopenStage(stage) {
3686
- const { completedBy: _by, completedAt: _at, ...open2 } = stage;
3687
- return open2;
3878
+ const { completedBy: _by, completedAt: _at, ...open3 } = stage;
3879
+ return open3;
3688
3880
  }
3689
3881
  function gateChanges(before, after) {
3690
3882
  const changes = [];
@@ -3765,7 +3957,7 @@ var Board = class _Board {
3765
3957
  homes;
3766
3958
  constructor(dataDir, options) {
3767
3959
  this.paths = new BoardPaths(dataDir);
3768
- this.homes = new HomeRepos(path6.join(this.paths.state(), "home-hooks"));
3960
+ this.homes = new HomeRepos(path7.join(this.paths.state(), "home-hooks"));
3769
3961
  this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
3770
3962
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
3771
3963
  this.events = new EventLog(this.paths.eventLog(), () => this.newId(), this.now);
@@ -3798,7 +3990,7 @@ var Board = class _Board {
3798
3990
  * repeated, so a member that later chooses otherwise keeps its choice.
3799
3991
  */
3800
3992
  async applyAlignments() {
3801
- const file = path6.join(this.paths.state(), "alignments.json");
3993
+ const file = path7.join(this.paths.state(), "alignments.json");
3802
3994
  const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
3803
3995
  const pending = [
3804
3996
  FOLLOW_DECISIONS,
@@ -3814,7 +4006,8 @@ var Board = class _Board {
3814
4006
  DASHBOARD_UPDATES,
3815
4007
  CRON_VERBS,
3816
4008
  KNOWLEDGE_NOTICES,
3817
- IMAGE_UPLOADS
4009
+ IMAGE_UPLOADS,
4010
+ PLACE_CONVERSATIONS
3818
4011
  ].filter((name) => !applied.includes(name));
3819
4012
  if (pending.length === 0) {
3820
4013
  return;
@@ -3862,6 +4055,9 @@ var Board = class _Board {
3862
4055
  if (pending.includes(IMAGE_UPLOADS)) {
3863
4056
  await this.grantBeside("post_message", "upload_image");
3864
4057
  }
4058
+ if (pending.includes(PLACE_CONVERSATIONS)) {
4059
+ await this.startPlaceCursors();
4060
+ }
3865
4061
  await writeJson(file, { applied: [...applied, ...pending] });
3866
4062
  }
3867
4063
  /**
@@ -4066,7 +4262,7 @@ var Board = class _Board {
4066
4262
  }
4067
4263
  }
4068
4264
  for (const file of await listFiles(this.paths.tasks(slug))) {
4069
- const raw = await readMarkdown(path6.join(this.paths.tasks(slug), file), z16.record(z16.string(), z16.unknown()));
4265
+ const raw = await readMarkdown(path7.join(this.paths.tasks(slug), file), z16.record(z16.string(), z16.unknown()));
4070
4266
  const legacy = raw.data["thread"];
4071
4267
  if (legacy === void 0) {
4072
4268
  continue;
@@ -4074,7 +4270,7 @@ var Board = class _Board {
4074
4270
  const task = { ...TaskFrontmatterSchema.parse(raw.data), body: raw.body };
4075
4271
  if ((legacy === "open" || legacy === "closed") && !await exists(this.paths.threadFile(threads, task.id))) {
4076
4272
  const ended = task.status === "done" || task.status === "abandoned";
4077
- const open2 = legacy === "open" && !ended;
4273
+ const open3 = legacy === "open" && !ended;
4078
4274
  const openedBy = opened.get(task.id);
4079
4275
  const closedBy = closed.get(task.id);
4080
4276
  await this.writeThread(threads, {
@@ -4082,10 +4278,10 @@ var Board = class _Board {
4082
4278
  channel: channelRef(slug, "general"),
4083
4279
  title: task.title,
4084
4280
  subject: { kind: "task", id: task.id },
4085
- state: open2 ? "open" : "closed",
4281
+ state: open3 ? "open" : "closed",
4086
4282
  openedBy: openedBy?.actor ?? task.createdBy,
4087
4283
  openedAt: openedBy?.ts ?? task.createdAt,
4088
- ...open2 ? {} : {
4284
+ ...open3 ? {} : {
4089
4285
  closedBy: closedBy?.actor ?? SYSTEM_ACTOR.name,
4090
4286
  closedAt: closedBy?.ts ?? this.now().toISOString()
4091
4287
  },
@@ -4117,6 +4313,24 @@ var Board = class _Board {
4117
4313
  await writeJson(file, { ...cursors, channelsFrom });
4118
4314
  }
4119
4315
  }
4316
+ /**
4317
+ * A project's channels were once read, from the society scope, in the society's home
4318
+ * conversation, so a member had read each up to that cursor. Each project's conversation in the
4319
+ * society scope starts from it now, as each takes a conversation of its own.
4320
+ */
4321
+ async startPlaceCursors() {
4322
+ for (const agent of await this.listAgents()) {
4323
+ const file = this.paths.agentCursors(agent.name);
4324
+ if (!await exists(file)) {
4325
+ continue;
4326
+ }
4327
+ const cursors = await readJson(file, CursorsSchema);
4328
+ await writeJson(file, {
4329
+ ...cursors,
4330
+ placesFrom: { [SOCIETY_SCOPE]: cursorOf(cursors, SOCIETY_SCOPE) }
4331
+ });
4332
+ }
4333
+ }
4120
4334
  /**
4121
4335
  * Threads were once read in the scope's single conversation, so a member had read every thread
4122
4336
  * up to its scope's cursor. Each scope's threads start from that cursor now, as each thread's
@@ -4257,13 +4471,15 @@ ${charter.purpose}
4257
4471
  const token = mintToken();
4258
4472
  const thread = threadOfPart(part);
4259
4473
  const channel = channelOfPart(part);
4474
+ const place = placeOfPart(part);
4260
4475
  this.turnTokens.set(hashToken(token), {
4261
4476
  actor: {
4262
4477
  name: agent,
4263
4478
  role,
4264
4479
  ...scope === void 0 ? {} : { scope },
4265
4480
  ...thread === void 0 ? {} : { thread },
4266
- ...channel === void 0 ? {} : { channel }
4481
+ ...channel === void 0 ? {} : { channel },
4482
+ ...place === void 0 ? {} : { place }
4267
4483
  },
4268
4484
  expiresAt: this.now().getTime() + ttlMs
4269
4485
  });
@@ -4352,7 +4568,7 @@ ${charter.purpose}
4352
4568
  });
4353
4569
  }
4354
4570
  signInsFile() {
4355
- return path6.join(this.paths.state(), "sign-ins.json");
4571
+ return path7.join(this.paths.state(), "sign-ins.json");
4356
4572
  }
4357
4573
  async readSignIns() {
4358
4574
  const file = this.signInsFile();
@@ -4449,7 +4665,7 @@ ${charter.purpose}
4449
4665
  const files = await listFiles(this.paths.tasks(project));
4450
4666
  const tasks = [];
4451
4667
  for (const file of files) {
4452
- const doc = await readMarkdown(path6.join(this.paths.tasks(project), file), TaskFrontmatterSchema);
4668
+ const doc = await readMarkdown(path7.join(this.paths.tasks(project), file), TaskFrontmatterSchema);
4453
4669
  tasks.push({ ...doc.data, body: doc.body });
4454
4670
  }
4455
4671
  return tasks;
@@ -4468,7 +4684,7 @@ ${charter.purpose}
4468
4684
  const files = await listFiles(this.paths.proposals());
4469
4685
  const proposals = [];
4470
4686
  for (const file of files) {
4471
- const doc = await readMarkdown(path6.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
4687
+ const doc = await readMarkdown(path7.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
4472
4688
  proposals.push({ ...doc.data, body: doc.body });
4473
4689
  }
4474
4690
  return proposals;
@@ -4495,7 +4711,7 @@ ${charter.purpose}
4495
4711
  async listMembers() {
4496
4712
  const members = [];
4497
4713
  for (const file of await listFiles(this.paths.members())) {
4498
- const doc = await readMarkdown(path6.join(this.paths.members(), file), MemberSchema);
4714
+ const doc = await readMarkdown(path7.join(this.paths.members(), file), MemberSchema);
4499
4715
  members.push({ ...doc.data, profile: doc.body });
4500
4716
  }
4501
4717
  return members;
@@ -4514,7 +4730,7 @@ ${charter.purpose}
4514
4730
  const dir = project === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project);
4515
4731
  const topics = [];
4516
4732
  for (const file of await listFiles(dir)) {
4517
- topics.push(await this.knowledgeAt(path6.join(dir, file), project, file.replace(/\.md$/, "")));
4733
+ topics.push(await this.knowledgeAt(path7.join(dir, file), project, file.replace(/\.md$/, "")));
4518
4734
  }
4519
4735
  return topics;
4520
4736
  }
@@ -4544,7 +4760,7 @@ ${charter.purpose}
4544
4760
  const aside = this.paths.removedKnowledge(project ?? SOCIETY_SCOPE);
4545
4761
  let highest = 0;
4546
4762
  for (const file of (await listFiles(aside)).filter((name) => name.startsWith(`${topic}.`))) {
4547
- const copy = await this.knowledgeAt(path6.join(aside, file), project, topic);
4763
+ const copy = await this.knowledgeAt(path7.join(aside, file), project, topic);
4548
4764
  highest = Math.max(highest, copy.revision);
4549
4765
  }
4550
4766
  return highest;
@@ -4560,7 +4776,7 @@ ${charter.purpose}
4560
4776
  async listSkillsIn(dir, scope) {
4561
4777
  const skills = [];
4562
4778
  for (const entry of await listDirs(dir)) {
4563
- const file = path6.join(dir, entry, "SKILL.md");
4779
+ const file = path7.join(dir, entry, "SKILL.md");
4564
4780
  if (!await exists(file)) {
4565
4781
  continue;
4566
4782
  }
@@ -4650,7 +4866,7 @@ ${charter.purpose}
4650
4866
  if (!wanted.delete(file.slice(0, 26))) {
4651
4867
  continue;
4652
4868
  }
4653
- const { data } = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
4869
+ const { data } = await readMarkdown(path7.join(dir, file), MessageFrontmatterSchema);
4654
4870
  found.push(MessageRefSchema.parse(data));
4655
4871
  }
4656
4872
  if (wanted.size === 0) {
@@ -4682,7 +4898,7 @@ ${charter.purpose}
4682
4898
  if (!file.endsWith(".md")) {
4683
4899
  continue;
4684
4900
  }
4685
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
4901
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
4686
4902
  if (doc.data.state !== "open") {
4687
4903
  continue;
4688
4904
  }
@@ -4709,7 +4925,7 @@ ${charter.purpose}
4709
4925
  const all = [];
4710
4926
  for (const threads of await this.threadScopes()) {
4711
4927
  for (const file of await listFiles(threads)) {
4712
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
4928
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
4713
4929
  const messages = await listFiles(this.paths.threadMessages(threads, doc.data.id));
4714
4930
  const last = messages.at(-1);
4715
4931
  all.push({
@@ -5076,16 +5292,20 @@ ${DASHBOARD_LINE}
5076
5292
  }
5077
5293
  /**
5078
5294
  * How many unread digest messages a member has in each conversation: a scope's home, for its
5079
- * general channel, one of its other channels, or one of its threads. The heartbeat asks, so that it wakes a member where its unread
5295
+ * general channel, one of its other channels, one of its threads, or, in the society scope, a
5296
+ * project's channels. The heartbeat asks, so that it wakes a member where its unread
5080
5297
  * messages are and nowhere else. Society channels count only for a member woken in the society
5081
5298
  * scope; a project member reads them at its next turn. Two kinds of post are in the digest but not
5082
5299
  * counted, and are read when the member wakes for something else: a task step's, which wakes
5083
5300
  * whoever it hands work to, and the board's own, operations signals and landing announcements,
5084
- * whose signals that call for action wake their readers directly.
5301
+ * whose signals that call for action wake their readers directly. A closed thread counts for
5302
+ * nothing: its conversation has ended, and a turn there could only read its posts and be refused
5303
+ * a reply, while a wake queued before it closed, as for a mention, still runs there.
5085
5304
  */
5086
5305
  async unreadByConversation(actor) {
5087
5306
  const { cursors, upto } = await this.digestSnapshot(actor.name);
5088
5307
  const counts = /* @__PURE__ */ new Map();
5308
+ const closed = /* @__PURE__ */ new Map();
5089
5309
  const unscoped = { name: actor.name, role: actor.role };
5090
5310
  for (const { scope, part, message, news } of await this.digestSince(unscoped, cursors, upto)) {
5091
5311
  if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
@@ -5095,11 +5315,23 @@ ${DASHBOARD_LINE}
5095
5315
  if (posted !== scope && scope !== SOCIETY_SCOPE) {
5096
5316
  continue;
5097
5317
  }
5318
+ const thread = threadOfPart(part);
5319
+ if (thread !== void 0) {
5320
+ let ended = closed.get(thread);
5321
+ if (ended === void 0) {
5322
+ ended = (await this.tryFindThread(thread))?.thread.state === "closed";
5323
+ closed.set(thread, ended);
5324
+ }
5325
+ if (ended) {
5326
+ continue;
5327
+ }
5328
+ }
5098
5329
  const key = conversationKey(scope, part);
5099
5330
  const entry = counts.get(key) ?? {
5100
5331
  scope,
5101
- thread: threadOfPart(part) ?? null,
5332
+ thread: thread ?? null,
5102
5333
  channel: channelOfPart(part) ?? null,
5334
+ place: placeOfPart(part) ?? null,
5103
5335
  count: 0
5104
5336
  };
5105
5337
  counts.set(key, { ...entry, count: entry.count + 1 });
@@ -5157,8 +5389,9 @@ ${DASHBOARD_LINE}
5157
5389
  * thread it takes part in, and for the front desk, which every post by the user wakes, every post
5158
5390
  * by the user wherever it is. With a conversation, only what is filed there: for a thread, every
5159
5391
  * message of the thread, since the conversation is about it; for a channel's, that channel's
5160
- * top-level messages; for a scope's home, general's and those of a project the member is not in,
5161
- * since each thread and each other channel is a conversation of its own. Nothing newer than
5392
+ * top-level messages; for a project's in the society scope, the top-level messages of every
5393
+ * channel of a project the member is not in; for a scope's home, general's, since each thread,
5394
+ * each other channel, and each such project is a conversation of its own. Nothing newer than
5162
5395
  * `upto` counts (see `digestSnapshot`).
5163
5396
  */
5164
5397
  async digestSince(actor, cursors, upto, conversation) {
@@ -5178,11 +5411,17 @@ ${DASHBOARD_LINE}
5178
5411
  const frontDesk = charter.wakeTriggers.includes(FRONT_DESK_TRIGGER);
5179
5412
  const threadParticipation = /* @__PURE__ */ new Map();
5180
5413
  const openChannels = /* @__PURE__ */ new Map();
5414
+ const liveProjects = /* @__PURE__ */ new Map();
5181
5415
  const collected = [];
5182
5416
  const newsFrom = charter.societyScope ? void 0 : newsStart(cursors);
5183
5417
  const scopes = conversation !== void 0 ? [conversation.scope] : [...agent.memberships, SOCIETY_SCOPE];
5184
5418
  const starts = [
5185
- ...scopes.flatMap((each) => conversation === void 0 ? [cursorOf(cursors, each), threadStart(cursors, each), channelStart(cursors, each)] : [cursorOf(cursors, each, conversation.part)]),
5419
+ ...scopes.flatMap((each) => conversation === void 0 ? [
5420
+ cursorOf(cursors, each),
5421
+ threadStart(cursors, each),
5422
+ channelStart(cursors, each),
5423
+ placeStart(cursors, each)
5424
+ ] : [cursorOf(cursors, each, conversation.part)]),
5186
5425
  ...newsFrom === void 0 ? [] : [newsFrom]
5187
5426
  ];
5188
5427
  const known = starts.filter((start) => start !== null);
@@ -5192,7 +5431,7 @@ ${DASHBOARD_LINE}
5192
5431
  const thread = message.thread ?? null;
5193
5432
  const asked = message.mentions.includes(actor.name) || frontDesk && message.author === USER_NAME;
5194
5433
  if (newsFrom !== void 0 && thread === null && filedIn === SOCIETY_SCOPE && !asked) {
5195
- if ((newsFrom === null || message.id > newsFrom) && subscribed.has(message.channel) && message.author !== actor.name) {
5434
+ if ((conversation === void 0 || conversation.part === null) && (newsFrom === null || message.id > newsFrom) && subscribed.has(message.channel) && message.author !== actor.name) {
5196
5435
  collected.push({
5197
5436
  scope: conversation?.scope ?? SOCIETY_SCOPE,
5198
5437
  part: null,
@@ -5203,12 +5442,18 @@ ${DASHBOARD_LINE}
5203
5442
  continue;
5204
5443
  }
5205
5444
  const named = thread === null ? channelConversation(filedIn, message.channel) : void 0;
5206
- let open2 = named === void 0 ? void 0 : openChannels.get(message.channel);
5207
- if (named !== void 0 && open2 === void 0) {
5208
- open2 = await this.channelOpen(message.channel);
5209
- openChannels.set(message.channel, open2);
5210
- }
5211
- const part = thread ?? (open2 === true ? `#${named}` : null);
5445
+ let open3 = named === void 0 ? void 0 : openChannels.get(message.channel);
5446
+ if (named !== void 0 && open3 === void 0) {
5447
+ open3 = await this.channelOpen(message.channel);
5448
+ openChannels.set(message.channel, open3);
5449
+ }
5450
+ const project = thread === null ? placeConversation(filedIn, message.channel) : void 0;
5451
+ let live = project === void 0 ? void 0 : liveProjects.get(project);
5452
+ if (project !== void 0 && live === void 0) {
5453
+ live = await this.projectLive(project);
5454
+ liveProjects.set(project, live);
5455
+ }
5456
+ const part = thread ?? (open3 === true ? `#${named}` : live === true ? `~${project}` : null);
5212
5457
  if (conversation !== void 0 && (filedIn !== conversation.scope || part !== conversation.part)) {
5213
5458
  continue;
5214
5459
  }
@@ -5273,7 +5518,7 @@ ${task.body}`;
5273
5518
  }
5274
5519
  }
5275
5520
  for (const file of await listFiles(this.paths.projectKnowledge(project))) {
5276
- const content = await readFile4(path6.join(this.paths.projectKnowledge(project), file), "utf8");
5521
+ const content = await readFile4(path7.join(this.paths.projectKnowledge(project), file), "utf8");
5277
5522
  if (content.toLowerCase().includes(needle)) {
5278
5523
  hits.push({
5279
5524
  kind: "knowledge",
@@ -5294,7 +5539,7 @@ ${task.body}`;
5294
5539
  shared.push({
5295
5540
  kind: "knowledge",
5296
5541
  ref: `society/knowledge/${file}`,
5297
- file: path6.join(this.paths.societyKnowledge(), file)
5542
+ file: path7.join(this.paths.societyKnowledge(), file)
5298
5543
  });
5299
5544
  }
5300
5545
  for (const skill of await this.listSocietySkills()) {
@@ -5307,7 +5552,7 @@ ${task.body}`;
5307
5552
  shared.push({
5308
5553
  kind: "memory",
5309
5554
  ref: `memory/${file}`,
5310
- file: path6.join(this.paths.agentMemory(actor.name), file)
5555
+ file: path7.join(this.paths.agentMemory(actor.name), file)
5311
5556
  });
5312
5557
  }
5313
5558
  for (const entry of shared) {
@@ -5397,7 +5642,7 @@ ${current.body}`);
5397
5642
  });
5398
5643
  const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
5399
5644
  await mkdir4(aside, { recursive: true });
5400
- await rename3(file, path6.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
5645
+ await rename3(file, path7.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
5401
5646
  await this.events.append("knowledge.removed", actor.name, {
5402
5647
  topic: args.topic,
5403
5648
  project: args.project
@@ -5445,7 +5690,7 @@ ${current.body}`);
5445
5690
  });
5446
5691
  const aside = this.paths.removedSkills();
5447
5692
  await mkdir4(aside, { recursive: true });
5448
- await rename3(this.paths.societySkill(args.name), path6.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
5693
+ await rename3(this.paths.societySkill(args.name), path7.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
5449
5694
  await this.events.append("skill.removed", actor.name, {
5450
5695
  name: args.name,
5451
5696
  reason: args.reason
@@ -5522,7 +5767,7 @@ ${current.body}`);
5522
5767
  type,
5523
5768
  size: bytes.byteLength
5524
5769
  });
5525
- const name = path6.basename(args.path).replaceAll(/[[\]\\\r\n]/g, "");
5770
+ const name = path7.basename(args.path).replaceAll(/[[\]\\\r\n]/g, "");
5526
5771
  return {
5527
5772
  id,
5528
5773
  type,
@@ -5548,7 +5793,7 @@ ${current.body}`);
5548
5793
  async listCrons() {
5549
5794
  const crons = [];
5550
5795
  for (const file of await listFiles(this.paths.crons())) {
5551
- crons.push(await this.readCronFile(path6.join(this.paths.crons(), file)));
5796
+ crons.push(await this.readCronFile(path7.join(this.paths.crons(), file)));
5552
5797
  }
5553
5798
  return crons;
5554
5799
  }
@@ -5670,11 +5915,16 @@ ${current.body}`);
5670
5915
  }
5671
5916
  /**
5672
5917
  * Where a cron fires for its citizen: the thread, the channel, or the scope's home it names, else
5673
- * the conversation of the turn that set it when it wakes its setter, else its home outside projects.
5918
+ * the conversation of the turn that set it when it wakes its setter, a project's channels' in
5919
+ * the society scope included, else its home outside projects.
5674
5920
  */
5675
5921
  async cronConversation(actor, agent, named) {
5676
5922
  const own = actor.name === agent.name;
5677
5923
  if (named.project === void 0 && named.thread_id === void 0 && named.channel === void 0) {
5924
+ if (own && actor.scope === SOCIETY_SCOPE && actor.place !== void 0) {
5925
+ await this.readActiveProject(actor.place);
5926
+ return { scope: SOCIETY_SCOPE, place: actor.place };
5927
+ }
5678
5928
  return own && actor.scope !== void 0 ? this.cronConversation(actor, agent, {
5679
5929
  project: actor.scope,
5680
5930
  thread_id: actor.thread,
@@ -5750,7 +6000,8 @@ ${current.body}`);
5750
6000
  agent: cron.agent,
5751
6001
  project: cron.scope,
5752
6002
  ...cron.thread === void 0 ? {} : { thread: cron.thread },
5753
- ...cron.channel === void 0 ? {} : { channel: cron.channel }
6003
+ ...cron.channel === void 0 ? {} : { channel: cron.channel },
6004
+ ...cron.place === void 0 ? {} : { place: cron.place }
5754
6005
  };
5755
6006
  }
5756
6007
  async readCronFile(file) {
@@ -5961,17 +6212,20 @@ ${current.body}`);
5961
6212
  async openThreadIds(dir, keep) {
5962
6213
  const ids = /* @__PURE__ */ new Set();
5963
6214
  for (const file of await listFiles(dir)) {
5964
- const doc = await readMarkdown(path6.join(dir, file), ThreadFrontmatterSchema);
6215
+ const doc = await readMarkdown(path7.join(dir, file), ThreadFrontmatterSchema);
5965
6216
  if (doc.data.state === "open" && keep({ ...doc.data, body: doc.body })) {
5966
6217
  ids.add(doc.data.id);
5967
6218
  }
5968
6219
  }
5969
6220
  return ids;
5970
6221
  }
5971
- /** Refuses to archive a project while a turn runs in it or in a thread its archive closes. */
6222
+ /**
6223
+ * Refuses to archive a project while a turn runs in it, in a citizen's conversation of its
6224
+ * channels in the society scope, or in a thread its archive closes.
6225
+ */
5972
6226
  async assertProjectQuiet(actor, slug) {
5973
6227
  const threads = await this.openThreadIds(this.paths.threads(slug), () => true);
5974
- this.assertNoTurnCut(actor, `project ${slug} cannot be archived`, (turn) => turn.scope === slug || turn.thread !== void 0 && threads.has(turn.thread));
6228
+ this.assertNoTurnCut(actor, `project ${slug} cannot be archived`, (turn) => turn.scope === slug || turn.place === slug || turn.thread !== void 0 && threads.has(turn.thread));
5975
6229
  }
5976
6230
  async closeThread(actor, input) {
5977
6231
  const args = VerbInputs.close_thread.parse(input);
@@ -6512,7 +6766,8 @@ ${args.summary}`);
6512
6766
  filedFrom: {
6513
6767
  scope: actor.scope,
6514
6768
  ...actor.thread === void 0 ? {} : { thread: actor.thread },
6515
- ...actor.channel === void 0 ? {} : { channel: actor.channel }
6769
+ ...actor.channel === void 0 ? {} : { channel: actor.channel },
6770
+ ...actor.place === void 0 ? {} : { place: actor.place }
6516
6771
  }
6517
6772
  },
6518
6773
  charter: parsed.data
@@ -6924,19 +7179,20 @@ ${rationale}`}`);
6924
7179
  project: input.project,
6925
7180
  ...input.thread === void 0 ? {} : { thread: input.thread },
6926
7181
  ...input.channel === void 0 ? {} : { channel: input.channel },
7182
+ ...input.place === void 0 ? {} : { place: input.place },
6927
7183
  messages: [...input.messages]
6928
7184
  });
6929
7185
  });
6930
7186
  }
6931
7187
  /**
6932
7188
  * Where a conversation's session ids and last turn are kept: the scope's home, or beside it a
6933
- * thread's or a channel's, named by `part` as `conversationPart` gives it.
7189
+ * thread's, a channel's, or a project's, named by `part` as `conversationPart` gives it.
6934
7190
  */
6935
7191
  sessionDir(agent, scope, part) {
6936
7192
  return part === void 0 ? this.paths.agentProject(agent, scope) : this.paths.agentConversation(agent, scope, part);
6937
7193
  }
6938
7194
  async readSessions(agent, project, part) {
6939
- const file = path6.join(this.sessionDir(agent, project, part), "sessions.json");
7195
+ const file = path7.join(this.sessionDir(agent, project, part), "sessions.json");
6940
7196
  return await exists(file) ? readJson(file, SessionsFileSchema) : {};
6941
7197
  }
6942
7198
  /**
@@ -6947,15 +7203,34 @@ ${rationale}`}`);
6947
7203
  await this.mutex.run(async () => {
6948
7204
  const dir = this.sessionDir(agent, project, part);
6949
7205
  await ensureDir(dir);
6950
- const file = path6.join(dir, "sessions.json");
7206
+ const file = path7.join(dir, "sessions.json");
6951
7207
  const current = await exists(file) ? await readJson(file, SessionsFileSchema) : {};
6952
7208
  const kept = current.runner === void 0 || current.runner === runner ? current : {};
6953
7209
  await writeJson(file, { ...kept, [cli]: sessionId, runner });
6954
7210
  });
6955
7211
  }
6956
- /** The last turn of one conversation: the scope's home, a channel's, or a thread's. */
7212
+ /**
7213
+ * Drops a conversation's session for a CLI, so its next turn starts a new one, when the record
7214
+ * still names `sessionId`; returns whether it did.
7215
+ */
7216
+ async dropSession(agent, project, cli, sessionId, part) {
7217
+ return this.mutex.run(async () => {
7218
+ const file = path7.join(this.sessionDir(agent, project, part), "sessions.json");
7219
+ if (!await exists(file)) {
7220
+ return false;
7221
+ }
7222
+ const current = await readJson(file, SessionsFileSchema);
7223
+ if (current[cli] !== sessionId) {
7224
+ return false;
7225
+ }
7226
+ const { [cli]: _dropped, ...kept } = current;
7227
+ await writeJson(file, kept);
7228
+ return true;
7229
+ });
7230
+ }
7231
+ /** The last turn of one conversation: the scope's home, a channel's, a project's, or a thread's. */
6957
7232
  async readLastTurn(agent, project, part) {
6958
- const file = path6.join(this.sessionDir(agent, project, part), "last-turn.json");
7233
+ const file = path7.join(this.sessionDir(agent, project, part), "last-turn.json");
6959
7234
  return await exists(file) ? readJson(file, TurnRecordSchema) : null;
6960
7235
  }
6961
7236
  /** The latest turn a citizen took in a scope, in any of its conversations there. */
@@ -6963,8 +7238,9 @@ ${rationale}`}`);
6963
7238
  let latest = await this.readLastTurn(agent, project);
6964
7239
  const place = this.paths.agentProject(agent, project);
6965
7240
  const parts = [
6966
- ...await listDirs(path6.join(place, "threads")),
6967
- ...(await listDirs(path6.join(place, "channels"))).map((name) => `#${name}`)
7241
+ ...await listDirs(path7.join(place, "threads")),
7242
+ ...(await listDirs(path7.join(place, "channels"))).map((name) => `#${name}`),
7243
+ ...(await listDirs(path7.join(place, "places"))).map((name) => `~${name}`)
6968
7244
  ];
6969
7245
  for (const part of parts) {
6970
7246
  const turn = await this.readLastTurn(agent, project, part);
@@ -6985,6 +7261,7 @@ ${rationale}`}`);
6985
7261
  project: parsed.project,
6986
7262
  ...parsed.thread === void 0 ? {} : { thread: parsed.thread },
6987
7263
  ...parsed.channel === void 0 ? {} : { channel: parsed.channel },
7264
+ ...parsed.place === void 0 ? {} : { place: parsed.place },
6988
7265
  trigger: parsed.trigger.kind,
6989
7266
  session: parsed.session,
6990
7267
  runner: parsed.runner
@@ -7014,7 +7291,8 @@ ${rationale}`}`);
7014
7291
  agent: parsed.agent,
7015
7292
  project: parsed.project,
7016
7293
  ...parsed.thread === void 0 ? {} : { thread: parsed.thread },
7017
- ...parsed.channel === void 0 ? {} : { channel: parsed.channel }
7294
+ ...parsed.channel === void 0 ? {} : { channel: parsed.channel },
7295
+ ...parsed.place === void 0 ? {} : { place: parsed.place }
7018
7296
  });
7019
7297
  }
7020
7298
  await this.events.append(failed ? "turn.failed" : "turn.completed", parsed.agent, {
@@ -7022,6 +7300,7 @@ ${rationale}`}`);
7022
7300
  project: parsed.project,
7023
7301
  ...parsed.thread === void 0 ? {} : { thread: parsed.thread },
7024
7302
  ...parsed.channel === void 0 ? {} : { channel: parsed.channel },
7303
+ ...parsed.place === void 0 ? {} : { place: parsed.place },
7025
7304
  trigger: parsed.trigger.kind,
7026
7305
  session: parsed.session,
7027
7306
  exitReason: parsed.exitReason,
@@ -7031,7 +7310,9 @@ ${rationale}`}`);
7031
7310
  model: parsed.model,
7032
7311
  summary: parsed.status?.summary ?? null,
7033
7312
  needsUserDecision: parsed.status?.needsUserDecision ?? false,
7034
- error: parsed.error
7313
+ error: parsed.error,
7314
+ ...parsed.resumeFailed === void 0 ? {} : { resumeFailed: parsed.resumeFailed },
7315
+ ...parsed.sessionDropped === true ? { sessionDropped: true } : {}
7035
7316
  });
7036
7317
  });
7037
7318
  }
@@ -7068,6 +7349,20 @@ ${rationale}`}`);
7068
7349
  return task;
7069
7350
  });
7070
7351
  }
7352
+ /**
7353
+ * Posts the board's word on a task's work into the task's thread, such as commits a runner
7354
+ * could not put on its branch: as the board, with every `@name` written as a plain name, so it
7355
+ * wakes nobody and counts toward no heartbeat. Null, posting nothing, once the thread is closed.
7356
+ */
7357
+ async postTaskNotice(taskId, body) {
7358
+ return this.mutex.run(async () => {
7359
+ const found = await this.tryFindThread(taskId);
7360
+ if (found === null || found.thread.state !== "open") {
7361
+ return null;
7362
+ }
7363
+ return this.appendThreadMessage(SYSTEM_ACTOR.name, taskId, withoutMentions(body));
7364
+ });
7365
+ }
7071
7366
  /**
7072
7367
  * Records that a citizen's workspace in a conversation that has ended holds work on no branch, so
7073
7368
  * the scheduler gives the citizen a closing turn there to decide about it. False, recording
@@ -7078,7 +7373,7 @@ ${rationale}`}`);
7078
7373
  const payload = {
7079
7374
  agent: input.agent,
7080
7375
  project: input.scope,
7081
- ...conversation.kind === "thread" ? { thread: conversation.id } : { channel: conversation.name }
7376
+ ...conversation.kind === "thread" ? { thread: conversation.id } : conversation.kind === "channel" ? { channel: conversation.name } : { place: conversation.project }
7082
7377
  };
7083
7378
  return this.mutex.run(async () => {
7084
7379
  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;
@@ -7130,9 +7425,13 @@ ${rationale}`}`);
7130
7425
  }
7131
7426
  /**
7132
7427
  * Whether a conversation with a workspace of its own has ended: a thread closed, a task ended
7133
- * (for one from before tasks had threads), or a channel archived.
7428
+ * (for one from before tasks had threads), a channel archived, or a project archived, which ends
7429
+ * every citizen's conversation of its channels in the society scope.
7134
7430
  */
7135
7431
  async conversationEnded(conversation) {
7432
+ if (conversation.kind === "place") {
7433
+ return !await this.projectLive(conversation.project);
7434
+ }
7136
7435
  if (conversation.kind === "channel") {
7137
7436
  const place = conversation.scope === SOCIETY_SCOPE ? null : conversation.scope;
7138
7437
  return !await this.channelOpen(channelRef(place, conversation.name));
@@ -7206,11 +7505,11 @@ ${rationale}`}`);
7206
7505
  }
7207
7506
  /** Small named state files under data/state, for cursors the scheduler must keep across restarts. */
7208
7507
  async readState(name, schema, fallback) {
7209
- const file = path6.join(this.paths.state(), `${name}.json`);
7508
+ const file = path7.join(this.paths.state(), `${name}.json`);
7210
7509
  return await exists(file) ? readJson(file, schema) : fallback;
7211
7510
  }
7212
7511
  async writeState(name, value) {
7213
- await this.mutex.run(() => writeJson(path6.join(this.paths.state(), `${name}.json`), value));
7512
+ await this.mutex.run(() => writeJson(path7.join(this.paths.state(), `${name}.json`), value));
7214
7513
  }
7215
7514
  /** The project's dashboard, empty at revision 0 when it has none. */
7216
7515
  async readDashboard(slug) {
@@ -7241,7 +7540,7 @@ ${rationale}`}`);
7241
7540
  async listRunners() {
7242
7541
  const runners2 = [];
7243
7542
  for (const file of await listFiles(this.paths.runners())) {
7244
- runners2.push((await readMarkdown(path6.join(this.paths.runners(), file), RunnerSchema)).data);
7543
+ runners2.push((await readMarkdown(path7.join(this.paths.runners(), file), RunnerSchema)).data);
7245
7544
  }
7246
7545
  return runners2;
7247
7546
  }
@@ -7293,7 +7592,7 @@ ${rationale}`}`);
7293
7592
  return this.runnerTokens.get(hashToken(token)) ?? null;
7294
7593
  }
7295
7594
  runnerTokensFile() {
7296
- return path6.join(this.paths.state(), "runners.json");
7595
+ return path7.join(this.paths.state(), "runners.json");
7297
7596
  }
7298
7597
  async readRunnerTokens() {
7299
7598
  const file = this.runnerTokensFile();
@@ -7342,26 +7641,26 @@ ${rationale}`}`);
7342
7641
  async listHomeConflicts(agent) {
7343
7642
  const home = this.paths.agent(agent);
7344
7643
  const found = [];
7345
- const walk = async (dir, prefix) => {
7346
- for (const entry of await readdir3(dir, { withFileTypes: true }).catch(() => [])) {
7644
+ const walk2 = async (dir, prefix) => {
7645
+ for (const entry of await readdir4(dir, { withFileTypes: true }).catch(() => [])) {
7347
7646
  if (prefix === "" && SKIPPED_IN_HOME.has(entry.name)) {
7348
7647
  continue;
7349
7648
  }
7350
7649
  const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
7351
7650
  if (entry.isDirectory()) {
7352
- await walk(path6.join(dir, entry.name), relative);
7651
+ await walk2(path7.join(dir, entry.name), relative);
7353
7652
  continue;
7354
7653
  }
7355
7654
  const file = entry.isFile() ? conflictCopyOf(relative) : null;
7356
7655
  if (file !== null) {
7357
- const since = await this.homes.changedAt(home, relative) ?? (await stat3(path6.join(home, relative)).catch(() => null))?.mtime.toISOString();
7656
+ const since = await this.homes.changedAt(home, relative) ?? (await stat3(path7.join(home, relative)).catch(() => null))?.mtime.toISOString();
7358
7657
  if (since !== void 0) {
7359
7658
  found.push({ path: relative, file, since });
7360
7659
  }
7361
7660
  }
7362
7661
  }
7363
7662
  };
7364
- await walk(home, "");
7663
+ await walk2(home, "");
7365
7664
  return found.toSorted((a, b) => a.path.localeCompare(b.path));
7366
7665
  }
7367
7666
  /** The newest `limit` changes to an agent's home, newest first; empty while it is no repository. */
@@ -7438,7 +7737,7 @@ ${rationale}`}`);
7438
7737
  async writeTurnRecord(record) {
7439
7738
  const dir = this.sessionDir(record.agent, record.project, conversationPart(record));
7440
7739
  await ensureDir(dir);
7441
- await writeJson(path6.join(dir, "last-turn.json"), record);
7740
+ await writeJson(path7.join(dir, "last-turn.json"), record);
7442
7741
  }
7443
7742
  // ---------------------------------------------------------------------------------------------
7444
7743
  // Internals
@@ -7557,8 +7856,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7557
7856
  async ensureAgentProject(name, slug) {
7558
7857
  const dir = this.paths.agentProject(name, slug);
7559
7858
  await ensureDir(dir);
7560
- if (!await exists(path6.join(dir, "sessions.json"))) {
7561
- await writeJson(path6.join(dir, "sessions.json"), {});
7859
+ if (!await exists(path7.join(dir, "sessions.json"))) {
7860
+ await writeJson(path7.join(dir, "sessions.json"), {});
7562
7861
  }
7563
7862
  }
7564
7863
  /**
@@ -7590,7 +7889,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7590
7889
  const at = turn?.endedAt ?? turn?.startedAt;
7591
7890
  if (turn !== null && at !== void 0 && (lastTurnAt === void 0 || at > lastTurnAt)) {
7592
7891
  lastTurnAt = at;
7593
- lastTurnOutcome = `${turn.trigger.kind} on ${scope}: ${turn.exitReason ?? "running"}${turn.status === null ? "" : `, ${turn.status.summary.slice(0, 160)}`}`;
7892
+ lastTurnOutcome = `${turn.trigger.kind} on ${scope}${turnConversation(turn)}: ${turn.exitReason ?? "running"}${turn.status === null ? "" : `, ${turn.status.summary.slice(0, 160)}`}`;
7594
7893
  lastModel = turn.model ?? void 0;
7595
7894
  }
7596
7895
  }
@@ -7810,7 +8109,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7810
8109
  const file = this.paths.societySkillFile(skill.name);
7811
8110
  const replaced = await exists(file);
7812
8111
  const taken = proposal.skillFiles;
7813
- const staged = path6.join(this.paths.proposalFiles(proposal.id), "SKILL.md");
8112
+ const staged = path7.join(this.paths.proposalFiles(proposal.id), "SKILL.md");
7814
8113
  if (skill.body === void 0 && (taken === void 0 || !await exists(staged))) {
7815
8114
  throw new BoardError("INVALID_STATE", `proposal ${proposal.id} holds no skill files`);
7816
8115
  }
@@ -7873,8 +8172,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7873
8172
  const dir = this.paths.proposalFiles(proposal.id);
7874
8173
  await rm2(dir, { recursive: true, force: true });
7875
8174
  for (const entry of taken.files) {
7876
- const target = path6.join(dir, ...entry.path.split("/"));
7877
- await ensureDir(path6.dirname(target));
8175
+ const target = path7.join(dir, ...entry.path.split("/"));
8176
+ await ensureDir(path7.dirname(target));
7878
8177
  await writeFile4(target, contents.get(entry.path) ?? Buffer.alloc(0));
7879
8178
  await chmod2(target, entry.executable ? 493 : 420);
7880
8179
  }
@@ -7932,7 +8231,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7932
8231
  const where = {
7933
8232
  scope: turn.project,
7934
8233
  ...turn.thread === void 0 ? {} : { thread: turn.thread },
7935
- ...turn.channel === void 0 ? {} : { channel: turn.channel }
8234
+ ...turn.channel === void 0 ? {} : { channel: turn.channel },
8235
+ ...turn.place === void 0 ? {} : { place: turn.place }
7936
8236
  };
7937
8237
  await this.mutex.run(async () => {
7938
8238
  for (const proposal of await this.listProposals()) {
@@ -7964,16 +8264,25 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7964
8264
  if (!isSafeRelativePath(entry.path)) {
7965
8265
  continue;
7966
8266
  }
7967
- const bytes = await readFile4(path6.join(dir, ...entry.path.split("/"))).catch(() => null);
7968
- const text = bytes === null || bytes.includes(0) ? null : bytes.toString("utf8");
8267
+ const bytes = await readFile4(path7.join(dir, ...entry.path.split("/"))).catch(() => null);
7969
8268
  files.push({
7970
8269
  ...entry,
7971
- text: text === null ? null : text.slice(0, SKILL_TEXT_LIMIT_CHARS),
7972
- truncated: text !== null && text.length > SKILL_TEXT_LIMIT_CHARS
8270
+ ...bytes === null ? { text: null, truncated: false } : skillFileText(bytes)
7973
8271
  });
7974
8272
  }
7975
8273
  return files;
7976
8274
  }
8275
+ /**
8276
+ * A society skill's folder as it is published, with each file's text, for the skill's view; an
8277
+ * unknown skill, or a folder without a SKILL.md, is not found.
8278
+ */
8279
+ async readSocietySkillFiles(name) {
8280
+ const files = await readSkillFolder(this.paths.societySkill(NameSchema.parse(name)));
8281
+ if (files === null) {
8282
+ throw new BoardError("NOT_FOUND", `the society has no skill ${name}`);
8283
+ }
8284
+ return files;
8285
+ }
7977
8286
  async validateAddAgent(input) {
7978
8287
  if (await exists(this.paths.agentFile(input.name))) {
7979
8288
  throw new BoardError("ALREADY_EXISTS", `agent ${input.name} already exists`);
@@ -8104,7 +8413,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
8104
8413
  this.assertNoTurnCut(actor, `${ref} cannot be archived`, (turn) => turn.channel === channel && turn.scope === scope || turn.thread !== void 0 && closing.has(turn.thread));
8105
8414
  const threadsClosed = [];
8106
8415
  for (const file of await listFiles(threads)) {
8107
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
8416
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
8108
8417
  if (doc.data.channel !== ref || doc.data.state !== "open") {
8109
8418
  continue;
8110
8419
  }
@@ -8210,7 +8519,8 @@ ${charter.purpose}
8210
8519
  }
8211
8520
  }
8212
8521
  /**
8213
- * Every member leaves, every open thread on its channels closes, and the project is marked; its
8522
+ * Every member leaves, every open thread on its channels closes, every citizen's conversation of
8523
+ * its channels in the society scope ends with the crons there, and the project is marked; its
8214
8524
  * files, repository, and history stay. The notice is the board's, since the reason is free text
8215
8525
  * a proposer wrote and a board-authored mention would wake whoever it names.
8216
8526
  */
@@ -8235,7 +8545,7 @@ ${charter.purpose}
8235
8545
  const threads = this.paths.threads(slug);
8236
8546
  const threadsClosed = [];
8237
8547
  for (const file of await listFiles(threads)) {
8238
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
8548
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
8239
8549
  if (doc.data.state !== "open") {
8240
8550
  continue;
8241
8551
  }
@@ -8253,7 +8563,7 @@ ${charter.purpose}
8253
8563
  });
8254
8564
  threadsClosed.push(doc.data.id);
8255
8565
  }
8256
- await this.endCronsUnlocked(by, `project ${slug} was archived`, (cron) => cron.scope === slug || cron.thread !== void 0 && threadsClosed.includes(cron.thread));
8566
+ await this.endCronsUnlocked(by, `project ${slug} was archived`, (cron) => cron.scope === slug || cron.place === slug || cron.thread !== void 0 && threadsClosed.includes(cron.thread));
8257
8567
  const project = await this.updateProject(slug, (p) => ({
8258
8568
  ...p,
8259
8569
  members: [],
@@ -8310,10 +8620,10 @@ ${charter.purpose}
8310
8620
  ...project,
8311
8621
  members: project.members.filter((member) => member !== name)
8312
8622
  }));
8313
- const sessions = path6.join(this.paths.agentProject(name, slug), "sessions.json");
8623
+ const sessions = path7.join(this.paths.agentProject(name, slug), "sessions.json");
8314
8624
  if (await exists(sessions)) {
8315
8625
  const archived = `sessions.archived.${ts.replace(/[:.]/g, "-")}.json`;
8316
- await rename3(sessions, path6.join(this.paths.agentProject(name, slug), archived));
8626
+ await rename3(sessions, path7.join(this.paths.agentProject(name, slug), archived));
8317
8627
  }
8318
8628
  }
8319
8629
  this.tokenIndex.delete(current.tokenHash);
@@ -8393,6 +8703,17 @@ ${charter.purpose}
8393
8703
  return false;
8394
8704
  }
8395
8705
  }
8706
+ /**
8707
+ * Whether a project exists and is not archived, so citizens' conversations of its channels in
8708
+ * the society scope are still going.
8709
+ */
8710
+ async projectLive(slug) {
8711
+ try {
8712
+ return (await this.readProject(slug)).archived === void 0;
8713
+ } catch {
8714
+ return false;
8715
+ }
8716
+ }
8396
8717
  /**
8397
8718
  * A channel that takes new posts, threads, tasks, and followers: it exists, it is not archived,
8398
8719
  * and its project is not archived.
@@ -8643,7 +8964,7 @@ ${charter.purpose}
8643
8964
  if (since !== null && id <= since || upto !== void 0 && id > upto) {
8644
8965
  continue;
8645
8966
  }
8646
- const doc = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
8967
+ const doc = await readMarkdown(path7.join(dir, file), MessageFrontmatterSchema);
8647
8968
  messages.push({ ...doc.data, body: doc.body });
8648
8969
  }
8649
8970
  return messages;
@@ -8739,11 +9060,21 @@ function besideFields(beside) {
8739
9060
  if (beside === void 0) {
8740
9061
  return {};
8741
9062
  }
8742
- return "thread" in beside ? { thread: beside.thread } : { channel: beside.channel };
9063
+ if ("thread" in beside) {
9064
+ return { thread: beside.thread };
9065
+ }
9066
+ return "channel" in beside ? { channel: beside.channel } : { place: beside.place };
9067
+ }
9068
+ function namedIn(dispatch) {
9069
+ return {
9070
+ ...dispatch.thread === void 0 ? {} : { thread: dispatch.thread.id },
9071
+ ...dispatch.channel === void 0 ? {} : { channel: dispatch.channel },
9072
+ ...dispatch.place === void 0 ? {} : { place: dispatch.place }
9073
+ };
8743
9074
  }
8744
9075
  function conversationKey2(agent, scope, beside) {
8745
9076
  const fields = besideFields(beside);
8746
- return sessionKey(agent, scope, conversationPart({ thread: fields.thread?.id, channel: fields.channel }));
9077
+ return sessionKey(agent, scope, conversationPart({ thread: fields.thread?.id, channel: fields.channel, place: fields.place }));
8747
9078
  }
8748
9079
  var DEFAULT_TIMINGS = Object.freeze({
8749
9080
  pollMs: 1e3,
@@ -9215,6 +9546,21 @@ var Scheduler = class {
9215
9546
  }
9216
9547
  return;
9217
9548
  }
9549
+ case "project.archived": {
9550
+ const slug = stringOf(payload["slug"]);
9551
+ if (slug === null) {
9552
+ return;
9553
+ }
9554
+ this.sweep({ kind: "place", project: slug });
9555
+ for (const [key, item] of this.pending) {
9556
+ const { dispatch } = item;
9557
+ if (dispatch.place === slug) {
9558
+ this.pending.delete(key);
9559
+ this.log.info({ agent: dispatch.agent, project: dispatch.project, place: slug }, "dropping project wake, the project is archived");
9560
+ }
9561
+ }
9562
+ return;
9563
+ }
9218
9564
  case "agent.left": {
9219
9565
  const name = stringOf(payload["name"]);
9220
9566
  const project = stringOf(payload["project"]);
@@ -9516,11 +9862,12 @@ var Scheduler = class {
9516
9862
  const scope = stringOf(payload["project"]);
9517
9863
  const thread = stringOf(payload["thread"]);
9518
9864
  const channel = stringOf(payload["channel"]);
9865
+ const place = stringOf(payload["place"]);
9519
9866
  const agent = name === null ? null : await this.tryReadAgent(name);
9520
9867
  if (agent === null || scope === null) {
9521
9868
  return;
9522
9869
  }
9523
- const beside = thread !== null ? { thread: await this.conversationOf(thread, scope) } : channel !== null ? { channel } : null;
9870
+ const beside = thread !== null ? { thread: await this.conversationOf(thread, scope) } : channel !== null ? { channel } : place !== null ? { place } : null;
9524
9871
  if (beside === null) {
9525
9872
  return;
9526
9873
  }
@@ -9565,7 +9912,7 @@ var Scheduler = class {
9565
9912
  }
9566
9913
  return {
9567
9914
  scope,
9568
- beside: conversation.kind === "thread" ? { thread: await this.conversationOf(conversation.id, scope) } : { channel: conversation.name }
9915
+ beside: conversation.kind === "thread" ? { thread: await this.conversationOf(conversation.id, scope) } : conversation.kind === "channel" ? { channel: conversation.name } : { place: conversation.project }
9569
9916
  };
9570
9917
  }
9571
9918
  /**
@@ -9583,14 +9930,23 @@ var Scheduler = class {
9583
9930
  }
9584
9931
  /**
9585
9932
  * The conversation a post wakes a citizen in, for a turn in `scope`: its thread's, else its
9586
- * channel's when that is a channel of the scope other than general, else the scope's home.
9933
+ * channel's when that is a channel of the scope other than general, else, for a turn in the
9934
+ * society scope, its project's when it is a project's channel, general included, else the
9935
+ * scope's home.
9587
9936
  */
9588
9937
  async postedIn(scope, channel, threadId) {
9589
9938
  if (threadId !== void 0) {
9590
9939
  return { thread: await this.conversationOf(threadId, scope) };
9591
9940
  }
9592
- const name = channel === null ? void 0 : channelConversation(scope, channel);
9593
- return name === void 0 ? void 0 : { channel: name };
9941
+ if (channel === null) {
9942
+ return void 0;
9943
+ }
9944
+ const name = channelConversation(scope, channel);
9945
+ if (name !== void 0) {
9946
+ return { channel: name };
9947
+ }
9948
+ const place = placeConversation(scope, channel);
9949
+ return place === void 0 ? void 0 : { place };
9594
9950
  }
9595
9951
  async tryReadAgent(name) {
9596
9952
  try {
@@ -9732,7 +10088,7 @@ var Scheduler = class {
9732
10088
  };
9733
10089
  for (const entry of unread) {
9734
10090
  if (entry.scope === project) {
9735
- const beside = entry.thread !== null ? { thread: await this.conversationOf(entry.thread, project) } : entry.channel !== null ? { channel: entry.channel } : void 0;
10091
+ const beside = entry.thread !== null ? { thread: await this.conversationOf(entry.thread, project) } : entry.channel !== null ? { channel: entry.channel } : entry.place !== null ? { place: entry.place } : void 0;
9736
10092
  reasonsIn(beside).digestSize += entry.count;
9737
10093
  }
9738
10094
  }
@@ -9819,7 +10175,10 @@ var Scheduler = class {
9819
10175
  if (cron.thread !== void 0) {
9820
10176
  return { thread: await this.conversationOf(cron.thread, cron.scope) };
9821
10177
  }
9822
- return cron.channel === void 0 ? void 0 : { channel: cron.channel };
10178
+ if (cron.channel !== void 0) {
10179
+ return { channel: cron.channel };
10180
+ }
10181
+ return cron.place === void 0 ? void 0 : { place: cron.place };
9823
10182
  }
9824
10183
  /** Whether a cron's wake waits in the queue or its turn is running. */
9825
10184
  cronQueuedOrRunning(id) {
@@ -10204,8 +10563,7 @@ var Scheduler = class {
10204
10563
  const attempt = steer({
10205
10564
  agent: dispatch.agent,
10206
10565
  project: dispatch.project,
10207
- ...dispatch.thread === void 0 ? {} : { thread: dispatch.thread.id },
10208
- ...dispatch.channel === void 0 ? {} : { channel: dispatch.channel }
10566
+ ...namedIn(dispatch)
10209
10567
  }).then((outcome) => {
10210
10568
  const current = this.pending.get(key);
10211
10569
  if (outcome !== "busy" && current !== void 0) {
@@ -10226,7 +10584,7 @@ var Scheduler = class {
10226
10584
  return false;
10227
10585
  }
10228
10586
  const { dispatch } = item;
10229
- const cursor = await this.board.digestCursor(dispatch.agent, dispatch.project, conversationPart({ thread: dispatch.thread?.id, channel: dispatch.channel }));
10587
+ const cursor = await this.board.digestCursor(dispatch.agent, dispatch.project, conversationPart(namedIn(dispatch)));
10230
10588
  return cursor !== null && cursor >= item.newestMessage;
10231
10589
  }
10232
10590
  async dispatchReady(now) {
@@ -10263,6 +10621,7 @@ var Scheduler = class {
10263
10621
  project: dispatch.project,
10264
10622
  thread: dispatch.thread?.id,
10265
10623
  channel: dispatch.channel,
10624
+ place: dispatch.place,
10266
10625
  trigger: dispatch.trigger.kind
10267
10626
  }, "dispatching turn");
10268
10627
  const crons = dispatch.crons ?? [];
@@ -10379,9 +10738,9 @@ import { randomUUID } from "node:crypto";
10379
10738
 
10380
10739
  // packages/turn-host/dist/render.js
10381
10740
  var TURN_CONTRACT = [
10382
- "- Every turn belongs to one conversation, named at the top of your prompt: your home conversation in a project or the society, for its general channel and for work tied to no other channel and no thread; the conversation of one other channel of the place, a workstream such as a release; or the conversation of one thread you take part in, a task's, a proposal's, or a topic's. Each has its own session and its own turns, which may run while this one does. The prompt carries this conversation only: in a thread, its new messages, or the whole thread when the conversation is new, and in a task's thread the task and its stage; in a channel's or at home, that channel's or general's messages that concern you. Read anything else you need with get_task, search, and the board's files, and leave other channels', other threads', and other projects' work to their own turns.",
10741
+ "- Every turn belongs to one conversation, named at the top of your prompt: your home conversation in a project or the society, for its general channel and for work tied to no other channel and no thread; the conversation of one other channel of the place, a workstream such as a release; outside projects, the conversation of one project's channels, general and its workstreams together, for posts there that ask something of you while you are not a member; or the conversation of one thread you take part in, a task's, a proposal's, or a topic's. Each has its own session and its own turns, which may run while this one does. The prompt carries this conversation only: in a thread, its new messages, or the whole thread when the conversation is new, and in a task's thread the task and its stage; in a channel's, a project's, or at home, that channel's, that project's channels', or general's messages that concern you. Read anything else you need with get_task, search, and the board's files, and leave other channels', other threads', and other projects' work to their own turns.",
10383
10742
  "- Act through the board tools (the `board` MCP server). Holding a stage is a lease; every turn that touches the task renews it.",
10384
- "- Work only inside your worktree. In a task's conversation it is a worktree of its own on the task's branch `task/<id>`: commit your work there. After each turn the runner commits what you left on the branch and lets it go for the next holder. Your home worktree is on your own branch, `agent/<name>`, for work tied to no task; nothing lands from it by itself. In a proposal's, a topic's, or a channel's conversation it is a worktree of its own, detached at the project's default branch, which goes when the thread closes or the channel is archived, so work that should last goes on a task's branch. Before each turn the runner fetches the project's remote and brings what holds no work of yours up to the default branch: your own branch when it has no commits of its own, a task's branch nobody has worked on, and a detached worktree with nothing uncommitted; what it leaves where it is, and why, is under Your workspace in the prompt. It never merges or commits for you.",
10743
+ "- Work only inside your worktree. In a task's conversation it is a worktree of its own on the task's branch `task/<id>`: commit your work there. The branch is checked out in one worktree at a time and kept for the turns of whoever holds the task's stage, so a turn of anyone else there, or one that starts while another turn has the branch, works detached at its tip, which Your workspace then says; its commits go on the branch by a fast-forward when a turn in the task ends with the branch free, and the task's thread is told of commits that no longer build on its tip. After each turn the runner commits what you left on the branch and lets it go for the next holder. Your home worktree is on your own branch, `agent/<name>`, for work tied to no task; nothing lands from it by itself. In a proposal's, a topic's, or a channel's conversation it is a worktree of its own, detached at the project's default branch, which goes when the thread closes or the channel is archived, so work that should last goes on a task's branch. Before each turn the runner fetches the project's remote and brings what holds no work of yours up to the default branch: your own branch when it has no commits of its own, a task's branch nobody has worked on, and a detached worktree with nothing uncommitted; what it leaves where it is, and why, is under Your workspace in the prompt. It never merges or commits for you.",
10385
10744
  "- Posts that would wake this conversation may arrive while you work, delivered into the turn under a heading that says so and formatted like the digest. They are part of this turn: act on them now, after the step in hand, or not at all, as with the digest. The user may also stop a turn; the next turn is told.",
10386
10745
  "- Silence is allowed. If the digest needs no reply, post nothing.",
10387
10746
  "- To come back to something later, set a cron with create_cron: a note on what to do then, and a five-field cron expression or one time with at. It wakes you in this conversation unless you name another, at most every fifteen minutes, and every fire is a paid turn, so end a cron with remove_cron once it has done its job. Never wait by holding a stage, sleeping, or leaving a process running: a turn's board token ends with the turn, so nothing it leaves behind can act on the board. The society's crons are under society/crons/ in the board projection.",
@@ -10454,7 +10813,7 @@ function renderInstructions(input) {
10454
10813
  if (norms.length > 0) {
10455
10814
  sections.push("", "## Society norms", "", norms);
10456
10815
  }
10457
- sections.push("", "## Core memory", "", memory.length === 0 ? "(empty)" : memory, "", "## Skills", "", ...renderSkillsIndex(input.skills ?? []), "", "## Where things are", "", `- Your home directory: ${input.homeDir}. memory/core.md is loaded every turn; memory/<topic>.md is your archive; skills/<name>/SKILL.md are your skills; projects/<slug>/notes.md are your notes. You may read and write all of it, and the board's search covers your archive and skills for you alone. The runner keeps it in git and shares it with the board after every turn, so whatever machine your next turn runs on has it; leave git there to the runner. Files over ${HOME_FILE_LIMIT_BYTES / (1024 * 1024)} MB are not kept.`, `- ${HOME_SCRATCH}/ in your home is your working folder in turns outside any project, and a thread's or a channel's conversation there works in a folder of its own inside it, which goes when the thread closes or the channel is archived: none of it is kept or shared, so put downloads, environments, and other working files there, and keep the rest of your home for what you mean to remember.`, `- The board projection: ${input.boardDir} (read-only markdown: society and project channels, tasks, threads; shared knowledge under projects/<slug>/knowledge/ and society/knowledge/; the society's skills under society/skills/; each project's dashboard, the status page the user reads on its view, at projects/<slug>/dashboard.md, which its members change with update_dashboard). Search it with your file tools; act through the board tools.`, "", "## Turn contract", "", TURN_CONTRACT, "", "## Planning", "", PLANNING, "", "## Governance", "", GOVERNANCE);
10816
+ sections.push("", "## Core memory", "", memory.length === 0 ? "(empty)" : memory, "", "## Skills", "", ...renderSkillsIndex(input.skills ?? []), "", "## Where things are", "", `- Your home directory: ${input.homeDir}. memory/core.md is loaded every turn; memory/<topic>.md is your archive; skills/<name>/SKILL.md are your skills; projects/<slug>/notes.md are your notes. You may read and write all of it, and the board's search covers your archive and skills for you alone. The runner keeps it in git and shares it with the board after every turn, so whatever machine your next turn runs on has it; leave git there to the runner. Files over ${HOME_FILE_LIMIT_BYTES / (1024 * 1024)} MB are not kept.`, `- ${HOME_SCRATCH}/ in your home is your working folder in turns outside any project, and a thread's, a channel's, or a project's channels' conversation there works in a folder of its own inside it, which goes when the thread closes or the channel or the project is archived: none of it is kept or shared, so put downloads, environments, and other working files there, and keep the rest of your home for what you mean to remember.`, `- The board projection: ${input.boardDir} (read-only markdown: society and project channels, tasks, threads; shared knowledge under projects/<slug>/knowledge/ and society/knowledge/; the society's skills under society/skills/; each project's dashboard, the status page the user reads on its view, at projects/<slug>/dashboard.md, which its members change with update_dashboard). Search it with your file tools; act through the board tools.`, "", "## Turn contract", "", TURN_CONTRACT, "", "## Planning", "", PLANNING, "", "## Governance", "", GOVERNANCE);
10458
10817
  if (input.onboarding !== void 0) {
10459
10818
  sections.push("", "## First turn", "", renderOnboardingPreamble(input.onboarding));
10460
10819
  }
@@ -10608,7 +10967,7 @@ function buildTurnPrompt(input) {
10608
10967
  lines.push("Operations signals arrived; they are listed under Operations signals below. Decide whether a proposal is warranted, and stay silent if not.");
10609
10968
  }
10610
10969
  if (dispatch.trigger.kind === "stage") {
10611
- lines.push("A stage is waiting for you: claim it with claim_task unless you hold it already, do the work in your worktree, which is on the task's branch, commit there, then advance_task. If the plan no longer fits, reshape it with plan_task.");
10970
+ lines.push("A stage is waiting for you: claim it with claim_task unless you hold it already, do the work in your worktree, which is on the task's branch unless Your workspace says otherwise, commit there, then advance_task. If the plan no longer fits, reshape it with plan_task.");
10612
10971
  }
10613
10972
  if (dispatch.trigger.kind === "task_done") {
10614
10973
  lines.push("A task you created is done. Tell whoever asked for it, if anyone did.");
@@ -10627,8 +10986,11 @@ function buildTurnPrompt(input) {
10627
10986
  if (conversation === null && dispatch.channel !== void 0) {
10628
10987
  const ref = channelRef(dispatch.project === SOCIETY_SCOPE ? null : dispatch.project, dispatch.channel);
10629
10988
  lines.push(`This turn is in your conversation of ${ref}, a workstream with turns of its own: its posts, and the work it asks for. Your home conversation for ${dispatch.project} carries its general channel, and each other channel and each thread you take part in is a conversation of its own; read one when you need it, and leave its work to its turn. Post here with post_message and channel ${ref}.`);
10989
+ } else if (conversation === null && dispatch.place !== void 0) {
10990
+ lines.push(`This turn is in your conversation of project ${dispatch.place}'s channels, from outside the project: the posts in its general and its other channels that concern you, and the work they ask for. Your home conversation for ${dispatch.project} carries its general channel, and each other project's channels, each other channel, and each thread you take part in is a conversation of its own with turns of its own; read one when you need it, and leave its work to its turn. Answer in the channel a post came from with post_message.`);
10630
10991
  } else if (conversation === null) {
10631
- lines.push(`This turn is in your home conversation for ${dispatch.project}: its general channel, and work tied to no other channel and no thread. Each other channel, a workstream, and each thread you take part in, a task's, a proposal's, or a topic's, is a conversation of its own with turns of its own; read one when you need it, and leave its work to its turn.`);
10992
+ const besides = dispatch.project === SOCIETY_SCOPE ? "Each other channel, each project's channels you are asked in from outside the project, and each thread you take part in, a task's, a proposal's, or a topic's," : "Each other channel, a workstream, and each thread you take part in, a task's, a proposal's, or a topic's,";
10993
+ lines.push(`This turn is in your home conversation for ${dispatch.project}: its general channel, and work tied to no other channel and no thread. ${besides} is a conversation of its own with turns of its own; read one when you need it, and leave its work to its turn.`);
10632
10994
  } else {
10633
10995
  const { thread, task } = conversation;
10634
10996
  const about = thread.subject === void 0 ? "a topic" : `${thread.subject.kind} ${thread.subject.id}`;
@@ -10663,7 +11025,9 @@ function buildTurnPrompt(input) {
10663
11025
  lines.push("", "## The user stopped your previous turn", "", `It was ended on purpose at ${last.endedAt ?? "its end"}, not by a failure. Do not pick its work up again unless this turn asks for it. The worktree may hold uncommitted changes; run git status before doing anything else.`);
10664
11026
  }
10665
11027
  if (last !== null && (last.exitReason === "error" || last.exitReason === "timeout")) {
10666
- lines.push("", "## Your previous turn did not finish", "", `It ended with ${last.exitReason}${last.error === null ? "" : `: ${last.error}`}.`, "The worktree may hold uncommitted changes. Run git status before doing anything else.");
11028
+ lines.push("", "## Your previous turn did not finish", "", `It ended with ${last.exitReason}${last.error === null ? "" : `: ${last.error}`}.`, ...last.sessionDropped === true ? [
11029
+ "Its CLI could not resume this conversation's session, so the session was dropped and this turn starts a new one: nothing of your earlier turns here carries over but what is on the board and in your home."
11030
+ ] : [], "The worktree may hold uncommitted changes. Run git status before doing anything else.");
10667
11031
  }
10668
11032
  const society = input.societyView;
10669
11033
  if (society !== void 0 && society !== null) {
@@ -10759,6 +11123,13 @@ function capitalized(text) {
10759
11123
  }
10760
11124
 
10761
11125
  // packages/turn-host/dist/host.js
11126
+ function conversationOfJob(job) {
11127
+ return {
11128
+ ...job.thread === void 0 ? {} : { thread: job.thread },
11129
+ ...job.channel === void 0 ? {} : { channel: job.channel },
11130
+ ...job.place === void 0 ? {} : { place: job.place }
11131
+ };
11132
+ }
10762
11133
  var SILENT = { info() {
10763
11134
  }, warn() {
10764
11135
  }, error() {
@@ -10769,6 +11140,12 @@ var DEFAULT_RESIDENT_IDLE_MS = 10 * 6e4;
10769
11140
  var LEASE_RENEW_MS = 10 * 6e4;
10770
11141
  var USER_POST_TRIGGER2 = "user_post";
10771
11142
  var OPS_TRIGGER2 = "ops_event";
11143
+ var RESUME_RETRIES = 3;
11144
+ function stuckNotice(stuck) {
11145
+ const more = stuck.total - stuck.commits.length;
11146
+ const commits = `${stuck.commits.map((commit) => `\`${commit}\``).join(", ")}${more > 0 ? `, and ${more} more` : ""}`;
11147
+ return `${stuck.agent}'s worktree for this task holds commits that \`${stuck.branch}\` lacks and that the runner cannot fast-forward it to, because they no longer build on its tip or another worktree's commits build on it too: ${commits}. Until someone puts them on the branch, it lacks them, and so does what lands from it; any worktree of the project's clone can cherry-pick or merge them by id.`;
11148
+ }
10772
11149
  var MAX_SIGNALS = 30;
10773
11150
  var TurnHost = class {
10774
11151
  board;
@@ -10813,13 +11190,15 @@ var TurnHost = class {
10813
11190
  const actor = { name: agent.name, role: agent.role };
10814
11191
  const thread = dispatch.thread;
10815
11192
  const channel = thread === void 0 ? dispatch.channel : void 0;
10816
- const part = conversationPart({ thread: thread?.id, channel });
11193
+ const place = thread === void 0 && channel === void 0 && dispatch.project === SOCIETY_SCOPE ? dispatch.place : void 0;
11194
+ const part = conversationPart({ thread: thread?.id, channel, place });
10817
11195
  const startedAt = this.now().toISOString();
10818
11196
  const base = {
10819
11197
  agent: agent.name,
10820
11198
  project: dispatch.project,
10821
11199
  ...thread === void 0 ? {} : { thread: thread.id },
10822
11200
  ...channel === void 0 ? {} : { channel },
11201
+ ...place === void 0 ? {} : { place },
10823
11202
  runner: runner.name,
10824
11203
  cli: agent.cli,
10825
11204
  session: null,
@@ -10844,23 +11223,25 @@ var TurnHost = class {
10844
11223
  const charter = await this.board.readRole(agent.role);
10845
11224
  const societyScope = dispatch.project === SOCIETY_SCOPE;
10846
11225
  const taskId = thread?.task === true ? thread.id : void 0;
11226
+ const task = taskId === void 0 ? null : await this.board.findTask(taskId).then((found) => found.task).catch(() => null);
10847
11227
  const project = societyScope ? null : await this.board.readProject(dispatch.project);
10848
- const workspace = await this.workspaceOf(project, thread, channel);
11228
+ const workspace = await this.workspaceOf(project, thread, channel, place, task);
10849
11229
  const sessions = await this.board.readSessions(agent.name, dispatch.project, part);
10850
11230
  const recorded = sessions.runner === runner.name ? sessions[agent.cli] : void 0;
10851
11231
  const newSession = recorded === void 0;
10852
11232
  const lastTurn = await this.board.readLastTurn(agent.name, dispatch.project, part);
10853
11233
  const costSoFarUsd = !newSession && lastTurn?.session === recorded ? lastTurn.sessionCostUsd ?? lastTurn.costUsd : 0;
11234
+ const resumeRetries = !newSession && lastTurn?.session === recorded && lastTurn.resumeFailed === "retry" ? lastTurn.resumeRetries ?? 1 : 0;
10854
11235
  const digest = await this.board.readDigest({
10855
11236
  ...actor,
10856
11237
  scope: dispatch.project,
10857
11238
  ...thread === void 0 ? {} : { thread: thread.id },
10858
- ...channel === void 0 ? {} : { channel }
11239
+ ...channel === void 0 ? {} : { channel },
11240
+ ...place === void 0 ? {} : { place }
10859
11241
  }, { advance: false, limit: thread === void 0 ? 50 : 100 });
10860
11242
  const threadSoFar = thread !== void 0 && newSession ? (await this.board.listThread(thread.id).catch(() => [])).slice(-100) : null;
10861
11243
  const messages = threadSoFar ?? digest.messages;
10862
11244
  const cursorAfter = threadSoFar?.at(-1)?.id ?? digest.cursor;
10863
- const task = taskId === void 0 ? null : await this.board.findTask(taskId).then((found) => found.task).catch(() => null);
10864
11245
  const held = task === null ? [] : (await this.board.heldClaims(agent.name)).filter((each) => each.id === task.id);
10865
11246
  const onboarding = dispatch.onboarding || newSession && part === void 0 ? {
10866
11247
  agentName: agent.name,
@@ -10919,7 +11300,7 @@ var TurnHost = class {
10919
11300
  fresh: threadSoFar !== null,
10920
11301
  ...threadRecord.state === "closed" ? { replyIn: await this.replyIn(threadRecord) } : {}
10921
11302
  };
10922
- const ending = dispatch.trigger.kind === "closing" ? await this.endingOf(dispatch.project, threadRecord, task, channel) : null;
11303
+ const ending = dispatch.trigger.kind === "closing" ? await this.endingOf(dispatch.project, threadRecord, task, channel, place) : null;
10923
11304
  const society = await this.board.society();
10924
11305
  const crons = [];
10925
11306
  for (const id of dispatch.crons ?? []) {
@@ -10968,6 +11349,7 @@ var TurnHost = class {
10968
11349
  scope: dispatch.project,
10969
11350
  ...thread === void 0 ? {} : { thread: thread.id },
10970
11351
  ...channel === void 0 ? {} : { channel },
11352
+ ...place === void 0 ? {} : { place },
10971
11353
  session: recorded ?? null,
10972
11354
  costSoFarUsd,
10973
11355
  instructions,
@@ -10994,6 +11376,7 @@ var TurnHost = class {
10994
11376
  stopRequestedBy: null,
10995
11377
  held: held.map((each) => each.id),
10996
11378
  costSoFarUsd,
11379
+ resumeRetries,
10997
11380
  configuredModel: agent.model ?? null,
10998
11381
  transcript: [],
10999
11382
  coldToken: residentKey === null ? token : null,
@@ -11007,6 +11390,7 @@ var TurnHost = class {
11007
11390
  project: dispatch.project,
11008
11391
  thread: thread?.id,
11009
11392
  channel,
11393
+ place,
11010
11394
  runner: runner.name,
11011
11395
  session: recorded,
11012
11396
  newSession,
@@ -11043,8 +11427,7 @@ var TurnHost = class {
11043
11427
  turnId,
11044
11428
  agent: turn.job.agent,
11045
11429
  project: turn.job.scope,
11046
- thread: turn.job.thread,
11047
- channel: turn.job.channel,
11430
+ ...conversationOfJob(turn.job),
11048
11431
  messages: sent.messages
11049
11432
  }).catch((error) => {
11050
11433
  this.log.warn({ turnId, error: String(error) }, "could not record a steer");
@@ -11052,17 +11435,14 @@ var TurnHost = class {
11052
11435
  }
11053
11436
  }
11054
11437
  turn.transcript.push(entry);
11055
- this.onEvent?.(turn.job.agent, turn.job.scope, entry.event, {
11056
- thread: turn.job.thread,
11057
- channel: turn.job.channel
11058
- });
11438
+ this.onEvent?.(turn.job.agent, turn.job.scope, entry.event, conversationOfJob(turn.job));
11059
11439
  }
11060
11440
  }
11061
11441
  /** The turn in flight in a conversation, if any. */
11062
11442
  turnIn(agent, scope, conversation = {}) {
11063
11443
  for (const turn of this.open.values()) {
11064
11444
  const { job } = turn;
11065
- if (job.agent === agent && job.scope === scope && job.thread === conversation.thread && job.channel === conversation.channel) {
11445
+ if (job.agent === agent && job.scope === scope && job.thread === conversation.thread && job.channel === conversation.channel && job.place === conversation.place) {
11066
11446
  return { turnId: job.turnId, runner: turn.runner, cli: job.cli };
11067
11447
  }
11068
11448
  }
@@ -11073,8 +11453,7 @@ var TurnHost = class {
11073
11453
  return [...this.open.values()].map(({ job }) => ({
11074
11454
  agent: job.agent,
11075
11455
  scope: job.scope,
11076
- ...job.thread === void 0 ? {} : { thread: job.thread },
11077
- ...job.channel === void 0 ? {} : { channel: job.channel }
11456
+ ...conversationOfJob(job)
11078
11457
  }));
11079
11458
  }
11080
11459
  /** The turns in flight, for the interface: their conversations and where a mention reaches them. */
@@ -11083,8 +11462,7 @@ var TurnHost = class {
11083
11462
  turnId: turn.job.turnId,
11084
11463
  agent: turn.job.agent,
11085
11464
  scope: turn.job.scope,
11086
- ...turn.job.thread === void 0 ? {} : { thread: turn.job.thread },
11087
- ...turn.job.channel === void 0 ? {} : { channel: turn.job.channel },
11465
+ ...conversationOfJob(turn.job),
11088
11466
  cli: turn.job.cli,
11089
11467
  ...turn.record.trigger.channel === void 0 ? {} : { askedIn: turn.record.trigger.channel },
11090
11468
  runner: turn.runner
@@ -11104,12 +11482,7 @@ var TurnHost = class {
11104
11482
  return "busy";
11105
11483
  }
11106
11484
  const { job } = turn;
11107
- const digest = await this.board.readDigest({
11108
- ...turn.actor,
11109
- scope: job.scope,
11110
- ...job.thread === void 0 ? {} : { thread: job.thread },
11111
- ...job.channel === void 0 ? {} : { channel: job.channel }
11112
- }, {
11485
+ const digest = await this.board.readDigest({ ...turn.actor, scope: job.scope, ...conversationOfJob(job) }, {
11113
11486
  ...turn.delivered === null ? {} : { since_cursor: turn.delivered },
11114
11487
  advance: false,
11115
11488
  limit: 50
@@ -11169,6 +11542,8 @@ var TurnHost = class {
11169
11542
  }
11170
11543
  const { job } = turn;
11171
11544
  const part = conversationPart(job);
11545
+ const resumeRetries = outcome.resumeFailed === "retry" ? turn.resumeRetries + 1 : void 0;
11546
+ const dropSession = job.session !== null && (outcome.resumeFailed === "lost" || (resumeRetries ?? 0) >= RESUME_RETRIES);
11172
11547
  const finished = {
11173
11548
  ...turn.record,
11174
11549
  session: outcome.session,
@@ -11182,20 +11557,35 @@ var TurnHost = class {
11182
11557
  toolCalls: turn.transcript.filter((entry) => entry.event.type === "tool_call").length,
11183
11558
  model: outcome.model ?? turn.configuredModel,
11184
11559
  ...outcome.work === null ? {} : { work: outcome.work },
11185
- ...outcome.exitReason === "stopped" ? { stoppedBy: turn.stopRequestedBy ?? USER_NAME } : {}
11560
+ ...outcome.exitReason === "stopped" ? { stoppedBy: turn.stopRequestedBy ?? USER_NAME } : {},
11561
+ ...outcome.resumeFailed === void 0 ? {} : { resumeFailed: outcome.resumeFailed },
11562
+ ...resumeRetries === void 0 ? {} : { resumeRetries },
11563
+ ...dropSession ? { sessionDropped: true } : {}
11186
11564
  };
11187
11565
  const ended = await this.conversationEnded(job);
11188
11566
  const closing = turn.record.trigger.kind === "closing";
11189
11567
  const decided = closing && outcome.exitReason !== "error" && outcome.exitReason !== "timeout";
11190
11568
  const recorded = await this.recorded(turn, async () => {
11191
- if (outcome.session !== job.session) {
11569
+ if (dropSession && job.session !== null) {
11570
+ await this.board.dropSession(job.agent, job.scope, job.cli, job.session, part);
11571
+ this.log.warn({
11572
+ agent: job.agent,
11573
+ project: job.scope,
11574
+ thread: job.thread,
11575
+ channel: job.channel,
11576
+ place: job.place,
11577
+ session: job.session,
11578
+ resumeFailed: outcome.resumeFailed,
11579
+ resumeRetries
11580
+ }, "dropped a session its CLI could not resume");
11581
+ } else if (outcome.session !== null && outcome.session !== job.session) {
11192
11582
  await this.board.writeSession(job.agent, job.scope, job.cli, outcome.session, turn.runner, part);
11193
11583
  }
11194
11584
  if (outcome.exitReason === "completed" || outcome.exitReason === "blocked" || outcome.exitReason === "stopped") {
11195
11585
  await this.board.setDigestCursor(job.agent, job.scope, turn.delivered, part);
11196
11586
  await this.renewLeases(turn.actor, turn.held);
11197
11587
  if (outcome.status?.needsUserDecision === true) {
11198
- await this.askUser(turn.actor, job, turn.record.startedAt, outcome.status.summary);
11588
+ await this.askUser(turn.actor, job, turn.record.trigger.channel, turn.record.startedAt, outcome.status.summary);
11199
11589
  }
11200
11590
  }
11201
11591
  await this.board.finishTurn(finished, turn.transcript);
@@ -11207,7 +11597,7 @@ var TurnHost = class {
11207
11597
  conversation
11208
11598
  });
11209
11599
  if (!again) {
11210
- const where = conversation.kind === "thread" ? `thread ${conversation.id}` : `channel ${conversation.name}`;
11600
+ const where = conversation.kind === "thread" ? `thread ${conversation.id}` : conversation.kind === "channel" ? `channel ${conversation.name}` : `project ${conversation.project}'s channels`;
11211
11601
  await this.board.publishSignal({
11212
11602
  kind: "stuck_workspace",
11213
11603
  key: `stuck_workspace:${job.agent}:${job.scope}:${conversationPart(job) ?? ""}`,
@@ -11224,12 +11614,18 @@ var TurnHost = class {
11224
11614
  this.log.warn({ runner: turn.runner, cli: job.cli, error: String(error) }, "could not record a quota");
11225
11615
  });
11226
11616
  }
11617
+ for (const stuck of outcome.stuck ?? []) {
11618
+ await this.board.postTaskNotice(stuck.branch.slice("task/".length), stuckNotice(stuck)).catch((error) => {
11619
+ this.log.warn({ branch: stuck.branch, agent: stuck.agent, error: String(error) }, "could not tell a task's thread of commits its branch cannot take");
11620
+ });
11621
+ }
11227
11622
  if (recorded) {
11228
11623
  this.log.info({
11229
11624
  agent: job.agent,
11230
11625
  project: job.scope,
11231
11626
  thread: job.thread,
11232
11627
  channel: job.channel,
11628
+ place: job.place,
11233
11629
  exitReason: finished.exitReason
11234
11630
  }, "turn finished");
11235
11631
  await this.takeSkillFiles(finished);
@@ -11296,6 +11692,7 @@ var TurnHost = class {
11296
11692
  project: dispatch.project,
11297
11693
  ...dispatch.thread === void 0 ? {} : { thread: dispatch.thread.id },
11298
11694
  ...dispatch.thread !== void 0 || dispatch.channel === void 0 ? {} : { channel: dispatch.channel },
11695
+ ...dispatch.thread !== void 0 || dispatch.channel !== void 0 || dispatch.project !== SOCIETY_SCOPE || dispatch.place === void 0 ? {} : { place: dispatch.place },
11299
11696
  runner,
11300
11697
  cli: agent?.cli ?? null,
11301
11698
  session: null,
@@ -11342,24 +11739,26 @@ var TurnHost = class {
11342
11739
  return token;
11343
11740
  }
11344
11741
  /**
11345
- * Where a conversation works: a task's on its branch, any other thread's and a channel's in a
11742
+ * Where a conversation works: a task's on its branch, which its stage's holder has to itself,
11743
+ * any other thread's, a channel's, and a project's channels' seen from the society scope in a
11346
11744
  * place of its own, and the home in the citizen's own.
11347
11745
  */
11348
- async workspaceOf(project, thread, channel) {
11746
+ async workspaceOf(project, thread, channel, place, task) {
11349
11747
  const own = thread !== void 0 ? thread.task ? {} : { thread: thread.id } : channel === void 0 ? {} : { channel };
11350
11748
  if (project === null) {
11351
- return { kind: "home", ...own };
11749
+ return { kind: "home", ...own, ...place === void 0 ? {} : { place } };
11352
11750
  }
11353
11751
  const repo = projectRepo(project);
11354
11752
  if (thread?.task === true) {
11355
- return { kind: "task", repo, taskId: thread.id };
11753
+ const holder = task?.status === "claimed" && task.claimedBy !== void 0 && task.claimedBy !== USER_NAME && (task.leaseExpiresAt === void 0 || new Date(task.leaseExpiresAt).getTime() > this.now().getTime()) ? task.claimedBy : void 0;
11754
+ return { kind: "task", repo, taskId: thread.id, ...holder === void 0 ? {} : { holder } };
11356
11755
  }
11357
- const branches = (await this.board.listTasks(project.slug)).filter((task) => task.status === "open" || task.status === "claimed").map((task) => `task/${task.id}`);
11756
+ const branches = (await this.board.listTasks(project.slug)).filter((each) => each.status === "open" || each.status === "claimed").map((each) => `task/${each.id}`);
11358
11757
  return { kind: "project", repo, branches, ...own };
11359
11758
  }
11360
11759
  /**
11361
- * Whether a thread conversation's task or thread has ended, or a channel conversation's channel
11362
- * was archived, which frees its workspace.
11760
+ * Whether a thread conversation's task or thread has ended, a channel conversation's channel was
11761
+ * archived, or a project conversation's project was, which frees its workspace.
11363
11762
  */
11364
11763
  async conversationEnded(job) {
11365
11764
  const conversation = workspaceConversationOf(job);
@@ -11373,14 +11772,14 @@ var TurnHost = class {
11373
11772
  * How a closing turn's conversation ended, and where its citizen asks someone else to take over
11374
11773
  * work that is not its own: the thread's channel while it is open, else the place's general.
11375
11774
  */
11376
- async endingOf(scope, thread, task, channel) {
11775
+ async endingOf(scope, thread, task, channel, project) {
11377
11776
  const place = scope === SOCIETY_SCOPE ? null : scope;
11378
11777
  if (thread !== void 0) {
11379
11778
  const closed = `the thread "${thread.title}" on ${thread.channel} was closed${thread.closedBy === void 0 ? "" : ` by ${thread.closedBy}`}${task === null ? "" : `, its task ${task.status}`}`;
11380
11779
  return { how: closed, askIn: await this.replyIn(thread) };
11381
11780
  }
11382
11781
  if (channel === void 0) {
11383
- return null;
11782
+ return project === void 0 ? null : { how: `project ${project} was archived`, askIn: channelRef(place, "general") };
11384
11783
  }
11385
11784
  const ref = channelRef(place, channel);
11386
11785
  const archived = (await this.board.listChannels()).find((each) => each.ref === ref)?.archived;
@@ -11394,13 +11793,15 @@ var TurnHost = class {
11394
11793
  * the user, which is what puts it before them. One that mentioned the user nowhere asks in its
11395
11794
  * own thread when it was in one that is still open, so the answer comes back to the same
11396
11795
  * conversation, and otherwise in a thread of its own on its conversation's channel, or its scope's
11397
- * general for the home, opened as the citizen with its summary. One that proposed something the
11398
- * user decides has put its decision before the user already, where deciding it clears it; a
11399
- * mention would stay until the user posted beside it.
11796
+ * general for the home, opened as the citizen with its summary; a project's conversation in the
11797
+ * society scope asks on the channel of the project its wake came from, else the project's general.
11798
+ * One that proposed something the user decides has put its decision before the user already,
11799
+ * where deciding it clears it; a mention would stay until the user posted beside it.
11400
11800
  */
11401
- async askUser(actor, job, since, summary) {
11801
+ async askUser(actor, job, askedIn, since, summary) {
11402
11802
  const threadId = job.thread;
11403
11803
  const place = job.scope === SOCIETY_SCOPE ? null : job.scope;
11804
+ const channel = job.place === void 0 ? channelRef(place, job.channel ?? "general") : askedIn !== void 0 && parseChannelRef(askedIn).project === job.place ? askedIn : channelRef(job.place, "general");
11404
11805
  try {
11405
11806
  if (await this.board.hasMentioned(actor.name, USER_NAME, since)) {
11406
11807
  return;
@@ -11418,7 +11819,7 @@ var TurnHost = class {
11418
11819
  return;
11419
11820
  }
11420
11821
  const thread = await this.board.openThread(actor, {
11421
- channel: channelRef(place, job.channel ?? "general"),
11822
+ channel,
11422
11823
  title: `${actor.name} asks for a decision: ${summary}`.slice(0, 200)
11423
11824
  });
11424
11825
  await this.board.postMessage(actor, {
@@ -11734,7 +12135,8 @@ var RunnerHub = class {
11734
12135
  async steer(conversation) {
11735
12136
  const turn = this.host.turnIn(conversation.agent, conversation.project, {
11736
12137
  thread: conversation.thread,
11737
- channel: conversation.channel
12138
+ channel: conversation.channel,
12139
+ place: conversation.place
11738
12140
  });
11739
12141
  const seat = turn === null ? void 0 : this.seats.get(turn.runner);
11740
12142
  if (turn === null || seat === void 0 || seat.send === null) {
@@ -11951,11 +12353,11 @@ var RunnerHub = class {
11951
12353
  * NOT_FOUND when the project has no runner yet or the branch or the path does not exist, and
11952
12354
  * `RunnerAwayError` while its runner is away.
11953
12355
  */
11954
- async readBranch(project, branch, path10) {
12356
+ async readBranch(project, branch, path11) {
11955
12357
  const { seat, repo } = await this.projectSeat(project, "branchReads", "read files");
11956
- const file = BranchFileSchema.nullable().parse(await this.request(seat, (request) => ({ type: "file", request, read: { repo, branch, path: path10 } }), FILE_TIMEOUT_MS));
12358
+ const file = BranchFileSchema.nullable().parse(await this.request(seat, (request) => ({ type: "file", request, read: { repo, branch, path: path11 } }), FILE_TIMEOUT_MS));
11957
12359
  if (file === null) {
11958
- throw new BoardError("NOT_FOUND", `${branch} has no ${path10 === "" ? "commit" : path10}`);
12360
+ throw new BoardError("NOT_FOUND", `${branch} has no ${path11 === "" ? "commit" : path11}`);
11959
12361
  }
11960
12362
  return file;
11961
12363
  }
@@ -11975,11 +12377,11 @@ var RunnerHub = class {
11975
12377
  * What a branch of a project's repository did to one file since it left the default branch, read
11976
12378
  * on the runner the project lives on, with the same refusals as `branchChanges`.
11977
12379
  */
11978
- async branchDiff(project, branch, path10) {
12380
+ async branchDiff(project, branch, path11) {
11979
12381
  const { seat, repo } = await this.projectSeat(project, "branchChanges", "list changes");
11980
- const diff = BranchFileDiffSchema.nullable().parse(await this.request(seat, (request) => ({ type: "diff", request, read: { repo, branch, path: path10 } }), FILE_TIMEOUT_MS));
12382
+ const diff = BranchFileDiffSchema.nullable().parse(await this.request(seat, (request) => ({ type: "diff", request, read: { repo, branch, path: path11 } }), FILE_TIMEOUT_MS));
11981
12383
  if (diff === null) {
11982
- throw new BoardError("NOT_FOUND", `${branch} did not change ${path10}`);
12384
+ throw new BoardError("NOT_FOUND", `${branch} did not change ${path11}`);
11983
12385
  }
11984
12386
  return diff;
11985
12387
  }
@@ -11990,7 +12392,7 @@ var RunnerHub = class {
11990
12392
  * back as a `BoardError` in the runner's words.
11991
12393
  */
11992
12394
  async readTurnImage(actor, file) {
11993
- const turn = this.host.runningTurns().find((each) => each.agent === actor.name && each.scope === actor.scope && each.thread === actor.thread && each.channel === actor.channel);
12395
+ const turn = this.host.runningTurns().find((each) => each.agent === actor.name && each.scope === actor.scope && each.thread === actor.thread && each.channel === actor.channel && each.place === actor.place);
11994
12396
  if (turn === void 0) {
11995
12397
  throw new BoardError("INVALID_STATE", `${actor.name} has no turn running in this conversation to read ${file} from`);
11996
12398
  }
@@ -12490,7 +12892,7 @@ function rateLimit(options) {
12490
12892
  }
12491
12893
 
12492
12894
  // apps/server/dist/runner-routes.js
12493
- import path7 from "node:path";
12895
+ import path8 from "node:path";
12494
12896
  import { Hono as Hono2 } from "hono";
12495
12897
  import { streamSSE } from "hono/streaming";
12496
12898
 
@@ -12683,7 +13085,7 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
12683
13085
  const marker = `/homes/${agent}/git`;
12684
13086
  const at = c.req.path.indexOf(marker);
12685
13087
  return homeGit.serve({
12686
- root: path7.dirname(repository),
13088
+ root: path8.dirname(repository),
12687
13089
  agent,
12688
13090
  rest: at === -1 ? "" : c.req.path.slice(at + marker.length),
12689
13091
  request: c.req.raw,
@@ -12696,7 +13098,7 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
12696
13098
 
12697
13099
  // apps/server/dist/static.js
12698
13100
  import { readFile as readFile5, stat as stat4 } from "node:fs/promises";
12699
- import path8 from "node:path";
13101
+ import path9 from "node:path";
12700
13102
  var CONTENT_TYPES = {
12701
13103
  ".html": "text/html; charset=utf-8",
12702
13104
  ".js": "text/javascript; charset=utf-8",
@@ -12724,11 +13126,11 @@ function within2(root, pathname) {
12724
13126
  } catch {
12725
13127
  return null;
12726
13128
  }
12727
- const candidate = path8.resolve(root, `.${decoded}`);
12728
- return candidate === root || candidate.startsWith(root + path8.sep) ? candidate : null;
13129
+ const candidate = path9.resolve(root, `.${decoded}`);
13130
+ return candidate === root || candidate.startsWith(root + path9.sep) ? candidate : null;
12729
13131
  }
12730
13132
  function spaHandler(staticDir) {
12731
- const root = path8.resolve(staticDir);
13133
+ const root = path9.resolve(staticDir);
12732
13134
  return async (c) => {
12733
13135
  const { pathname } = new URL(c.req.url);
12734
13136
  if (pathname.startsWith("/api/")) {
@@ -12739,14 +13141,14 @@ function spaHandler(staticDir) {
12739
13141
  if (file !== null && await isFile(file)) {
12740
13142
  return c.body(await readFile5(file), 200, {
12741
13143
  ...headers,
12742
- "content-type": CONTENT_TYPES[path8.extname(file)] ?? "application/octet-stream",
13144
+ "content-type": CONTENT_TYPES[path9.extname(file)] ?? "application/octet-stream",
12743
13145
  "cache-control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache"
12744
13146
  });
12745
13147
  }
12746
13148
  if (pathname.startsWith("/assets/")) {
12747
13149
  return c.text("not found", 404, headers);
12748
13150
  }
12749
- const index = path8.join(root, "index.html");
13151
+ const index = path9.join(root, "index.html");
12750
13152
  if (!await isFile(index)) {
12751
13153
  return c.text("The interface is not built: run pnpm build:web.", 404);
12752
13154
  }
@@ -12912,6 +13314,7 @@ function createApp(deps) {
12912
13314
  });
12913
13315
  api.get("/society/knowledge", async (c) => c.json(await board2.listKnowledge(null)));
12914
13316
  api.get("/skills", async (c) => c.json(await board2.listSocietySkills()));
13317
+ api.get("/skills/:name/files", async (c) => c.json(await board2.readSocietySkillFiles(NameSchema.parse(c.req.param("name")))));
12915
13318
  api.post("/agents/:name/retire", async (c) => {
12916
13319
  const body = RetireBodySchema.parse(await c.req.json());
12917
13320
  const { tokenHash: _hash, ...agent } = await board2.retireAgent(c.get("actor"), {
@@ -13355,7 +13758,10 @@ var TurnHub = class {
13355
13758
  get lastSeq() {
13356
13759
  return this.seq;
13357
13760
  }
13358
- /** Records one event of a turn, in its scope and its conversation there: a thread's, a channel's, or home. */
13761
+ /**
13762
+ * Records one event of a turn, in its scope and its conversation there: a thread's, a channel's,
13763
+ * a project's channels' in the society scope, or home.
13764
+ */
13359
13765
  push(agent, project, event, conversation = {}) {
13360
13766
  this.seq += 1;
13361
13767
  const item = {
@@ -13365,6 +13771,7 @@ var TurnHub = class {
13365
13771
  project,
13366
13772
  ...conversation.thread === void 0 ? {} : { thread: conversation.thread },
13367
13773
  ...conversation.channel === void 0 ? {} : { channel: conversation.channel },
13774
+ ...conversation.place === void 0 ? {} : { place: conversation.place },
13368
13775
  event
13369
13776
  };
13370
13777
  this.buffer.push(item);
@@ -13441,8 +13848,8 @@ var view = {
13441
13848
  return scheduler.activeSignals;
13442
13849
  }
13443
13850
  };
13444
- var webDir = process.env["STELLARIS_WEB_DIR"] ?? path9.resolve(import.meta.dirname, "../../web/dist");
13445
- var webBuilt = await access2(path9.join(webDir, "index.html")).then(() => true, () => false);
13851
+ var webDir = process.env["STELLARIS_WEB_DIR"] ?? path10.resolve(import.meta.dirname, "../../web/dist");
13852
+ var webBuilt = await access2(path10.join(webDir, "index.html")).then(() => true, () => false);
13446
13853
  var app = createApp({
13447
13854
  board,
13448
13855
  version: VERSION,