@kubitnodes/stellaris 0.3.22 → 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 (67) hide show
  1. package/README.md +4 -4
  2. package/dist/cli/index.js +1029 -571
  3. package/dist/server/index.js +1256 -656
  4. package/package.json +1 -1
  5. package/web/dist/assets/BackLink-DKMw_rHu.js +1 -0
  6. package/web/dist/assets/{Button-Yoe21NQ2.js → Button-BYJGqZ3Q.js} +1 -1
  7. package/web/dist/assets/{ChannelForms-55otlg-m.js → ChannelForms-CkYupEL2.js} +1 -1
  8. package/web/dist/assets/ChannelView-BIXouQMX.js +2 -0
  9. package/web/dist/assets/CitizenView-C3AMFixP.js +2 -0
  10. package/web/dist/assets/CitizensView-CYFS3f9U.js +1 -0
  11. package/web/dist/assets/{CliIcon-DbMYJdlU.js → CliIcon-DVX5iLhG.js} +1 -1
  12. package/web/dist/assets/CronsView-BGCcO7bH.js +1 -0
  13. package/web/dist/assets/KnowledgeView-DLLYjd1G.js +1 -0
  14. package/web/dist/assets/Markdown--whKJBYV.js +30 -0
  15. package/web/dist/assets/MessageItem-BvDOeJci.js +1 -0
  16. package/web/dist/assets/MetricsView-B-KUNOZm.js +1 -0
  17. package/web/dist/assets/NeedsYouView-Dee_bOKj.js +1 -0
  18. package/web/dist/assets/{Overview-BveU1yJj.js → Overview-DFiHDFnA.js} +1 -1
  19. package/web/dist/assets/{Pane-CLjdfBMM.js → Pane-DTmByqgA.js} +1 -1
  20. package/web/dist/assets/{Picker-CXOe5Xdu.js → Picker-DsHm4f7s.js} +1 -1
  21. package/web/dist/assets/{ProjectView-oa4Z46ED.js → ProjectView-DsRXW-qk.js} +1 -1
  22. package/web/dist/assets/ProposalView-B-4P1S-s.js +1 -0
  23. package/web/dist/assets/{ProposalsView-BZHivSEm.js → ProposalsView-uEuj-ooV.js} +1 -1
  24. package/web/dist/assets/Quota-D8WDWp5H.js +1 -0
  25. package/web/dist/assets/{RunnersView-BrJF-aLU.js → RunnersView-BB3q65rG.js} +1 -1
  26. package/web/dist/assets/SkillFileList-DhdEcFQA.js +2 -0
  27. package/web/dist/assets/SkillView-vAv7zvtJ.js +1 -0
  28. package/web/dist/assets/SocietyView-Dv4phSkz.js +1 -0
  29. package/web/dist/assets/{StickyList-7tAlUDCN.js → StickyList-C4fC_KJv.js} +1 -1
  30. package/web/dist/assets/SubjectThread-BEXt-tub.js +1 -0
  31. package/web/dist/assets/{TaskFileView-BEXQJQQ6.js → TaskFileView-B9sex0tS.js} +2 -2
  32. package/web/dist/assets/{TaskView-Cfhb7NuO.js → TaskView-BCT388kb.js} +2 -2
  33. package/web/dist/assets/{TasksView-3XljXcoi.js → TasksView-CuPay34-.js} +1 -1
  34. package/web/dist/assets/{ThreadForms-D2y5dXSC.js → ThreadForms-DzoRDeBh.js} +1 -1
  35. package/web/dist/assets/ThreadItem-CDVppcOZ.js +1 -0
  36. package/web/dist/assets/ThreadView-B4F10_j1.js +1 -0
  37. package/web/dist/assets/citizen-BPIJzKdn.js +1 -0
  38. package/web/dist/assets/{compose-DNc3_c5x.js → compose-BUhbVoI-.js} +1 -1
  39. package/web/dist/assets/format-CvfmDNoY.js +55 -0
  40. package/web/dist/assets/{governance-RBQ4Lam0.js → governance-Bba0OHvJ.js} +1 -1
  41. package/web/dist/assets/index-Bj8MlyAn.css +2 -0
  42. package/web/dist/assets/index-DD3J11PT.js +23 -0
  43. package/web/dist/assets/{mentions-B2Qchd_6.js → mentions-D838wlfQ.js} +1 -1
  44. package/web/dist/assets/{tasks-DCUj5w_d.js → tasks-CTaxXtGL.js} +1 -1
  45. package/web/dist/assets/useMatch-B8j0hm1q.js +1 -0
  46. package/web/dist/index.html +9 -9
  47. package/web/dist/assets/BackLink-CD-iuy-h.js +0 -1
  48. package/web/dist/assets/ChannelView-DJvLzg0N.js +0 -2
  49. package/web/dist/assets/CitizenView-m1K2aTvK.js +0 -2
  50. package/web/dist/assets/CitizensView-BeTgmrKE.js +0 -1
  51. package/web/dist/assets/CronsView-B4kXk5Ld.js +0 -1
  52. package/web/dist/assets/KnowledgeView-aSGxDDKy.js +0 -1
  53. package/web/dist/assets/Markdown-c9bCCLHg.js +0 -30
  54. package/web/dist/assets/MessageItem-DKJKMqs-.js +0 -1
  55. package/web/dist/assets/MetricsView-D2TTry5O.js +0 -1
  56. package/web/dist/assets/NeedsYouView-C3celRei.js +0 -1
  57. package/web/dist/assets/ProposalView-B4T-EoX9.js +0 -2
  58. package/web/dist/assets/Quota-BC7dpnwd.js +0 -1
  59. package/web/dist/assets/SocietyView-C1yosPuf.js +0 -1
  60. package/web/dist/assets/SubjectThread-CCq6ZAnR.js +0 -1
  61. package/web/dist/assets/ThreadItem-DeIhhmyu.js +0 -1
  62. package/web/dist/assets/ThreadView-CbUnt9WL.js +0 -1
  63. package/web/dist/assets/citizen--bqux6i_.js +0 -1
  64. package/web/dist/assets/format-B6fjC0_a.js +0 -55
  65. package/web/dist/assets/index-CyCty7a2.js +0 -23
  66. package/web/dist/assets/index-GRqxKN35.css +0 -2
  67. package/web/dist/assets/useMatch-CfbBQirn.js +0 -1
@@ -2,14 +2,14 @@
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
- import { z as z15 } from "zod";
12
+ import { z as z16 } from "zod";
13
13
 
14
14
  // packages/shared/dist/ids.js
15
15
  import { z } from "zod";
@@ -64,7 +64,8 @@ var VERB_NAMES = [
64
64
  "create_cron",
65
65
  "update_cron",
66
66
  "remove_cron",
67
- "remove_skill"
67
+ "remove_skill",
68
+ "upload_image"
68
69
  ];
69
70
  var VerbNameSchema = z2.enum(VERB_NAMES);
70
71
  var CharterTriggerSchema = z2.enum(["user_post", "ops_event", "heartbeat"]);
@@ -93,6 +94,7 @@ var USER_ROLE = "user";
93
94
  var USER_NAME = "user";
94
95
  var MEMBER_VERBS = [
95
96
  "post_message",
97
+ "upload_image",
96
98
  "read_inbox",
97
99
  "search",
98
100
  "open_thread",
@@ -233,6 +235,7 @@ var CronFrontmatterSchema = z3.object({
233
235
  scope: NameSchema,
234
236
  thread: UlidSchema.optional(),
235
237
  channel: NameSchema.optional(),
238
+ place: NameSchema.optional(),
236
239
  schedule: CronScheduleSchema,
237
240
  createdBy: NameSchema,
238
241
  createdAt: IsoDateTimeSchema,
@@ -382,15 +385,16 @@ function quotaLevel(quota, now) {
382
385
  var WINDOW_NAMES = {
383
386
  five_hour: "5h",
384
387
  seven_day: "7d",
385
- seven_day_opus: "7d Opus",
386
- seven_day_sonnet: "7d Sonnet",
388
+ seven_day_opus: "7d opus",
389
+ seven_day_sonnet: "7d sonnet",
387
390
  seven_day_oauth_apps: "7d apps",
388
391
  seven_day_overage_included: "7d extra",
389
392
  overage: "extra usage"
390
393
  };
391
394
  function windowName(window) {
392
395
  if (window.label !== void 0) {
393
- return window.minutes === 10080 ? `7d ${window.label}` : window.label;
396
+ const label = window.label.toLowerCase();
397
+ return window.minutes === 10080 ? `7d ${label}` : label;
394
398
  }
395
399
  const named = WINDOW_NAMES[window.key];
396
400
  if (named !== void 0) {
@@ -989,7 +993,8 @@ var DashboardSchema = z5.object({
989
993
  var ProposalOriginSchema = z5.object({
990
994
  scope: NameSchema,
991
995
  thread: UlidSchema.optional(),
992
- channel: NameSchema.optional()
996
+ channel: NameSchema.optional(),
997
+ place: NameSchema.optional()
993
998
  });
994
999
  var SKILL_FILES_LIMIT = 100;
995
1000
  var SKILL_BYTES_LIMIT = 10 * 1024 * 1024;
@@ -1037,130 +1042,175 @@ var DecisionSchema = z5.object({
1037
1042
  });
1038
1043
 
1039
1044
  // packages/shared/dist/verbs.js
1045
+ import { z as z7 } from "zod";
1046
+
1047
+ // packages/shared/dist/images.js
1040
1048
  import { z as z6 } from "zod";
1041
- var MomentSchema = z6.iso.datetime({ offset: true });
1042
- var CronTitleSchema = z6.string().trim().min(1).max(120);
1043
- var CronNoteSchema = z6.string().trim().min(1).max(4e3);
1049
+ var IMAGE_LIMIT_BYTES = 8 * 1024 * 1024;
1050
+ var ImageTypeSchema = z6.enum(["png", "jpeg", "gif", "webp"]);
1051
+ var IMAGE_MEDIA_TYPES = {
1052
+ png: "image/png",
1053
+ jpeg: "image/jpeg",
1054
+ gif: "image/gif",
1055
+ webp: "image/webp"
1056
+ };
1057
+ function startsWith(bytes, prefix, at = 0) {
1058
+ for (let index = 0; index < prefix.length; index += 1) {
1059
+ if (bytes[at + index] !== prefix[index]) {
1060
+ return false;
1061
+ }
1062
+ }
1063
+ return true;
1064
+ }
1065
+ var ascii = (text) => new TextEncoder().encode(text);
1066
+ function imageTypeOf(bytes) {
1067
+ if (startsWith(bytes, [137, 80, 78, 71, 13, 10, 26, 10])) {
1068
+ return "png";
1069
+ }
1070
+ if (startsWith(bytes, [255, 216, 255])) {
1071
+ return "jpeg";
1072
+ }
1073
+ if (startsWith(bytes, ascii("GIF87a")) || startsWith(bytes, ascii("GIF89a"))) {
1074
+ return "gif";
1075
+ }
1076
+ if (startsWith(bytes, ascii("RIFF")) && startsWith(bytes, ascii("WEBP"), 8)) {
1077
+ return "webp";
1078
+ }
1079
+ return null;
1080
+ }
1081
+ var ImagePathSchema = z6.string().min(1).max(4096).refine((value) => !value.includes("\0"), { message: "a path with no NUL character" });
1082
+ var IMAGE_SCHEME = "image:";
1083
+ var UploadedImageSchema = z6.object({
1084
+ id: UlidSchema,
1085
+ type: ImageTypeSchema,
1086
+ size: z6.number().int().positive(),
1087
+ markdown: z6.string()
1088
+ });
1089
+
1090
+ // packages/shared/dist/verbs.js
1091
+ var MomentSchema = z7.iso.datetime({ offset: true });
1092
+ var CronTitleSchema = z7.string().trim().min(1).max(120);
1093
+ var CronNoteSchema = z7.string().trim().min(1).max(4e3);
1044
1094
  var VerbInputs = {
1045
- post_message: z6.object({
1095
+ post_message: z7.object({
1046
1096
  /** Required outside a thread; in one it may be left out, and must otherwise be the thread's. */
1047
1097
  channel: ChannelRefSchema.optional(),
1048
- body: z6.string().min(1),
1098
+ body: z7.string().min(1),
1049
1099
  thread_id: UlidSchema.optional()
1050
1100
  }),
1051
- read_inbox: z6.object({
1101
+ read_inbox: z7.object({
1052
1102
  since_cursor: UlidSchema.nullable().optional(),
1053
- limit: z6.number().int().positive().max(200).default(50),
1054
- advance: z6.boolean().default(true)
1103
+ limit: z7.number().int().positive().max(200).default(50),
1104
+ advance: z7.boolean().default(true)
1055
1105
  }),
1056
- search: z6.object({
1057
- query: z6.string().min(1),
1106
+ search: z7.object({
1107
+ query: z7.string().min(1),
1058
1108
  project: NameSchema.optional(),
1059
1109
  channel: ChannelRefSchema.optional(),
1060
- limit: z6.number().int().positive().max(100).default(20)
1110
+ limit: z7.number().int().positive().max(100).default(20)
1061
1111
  }),
1062
1112
  /** On a task or a proposal, whose id the thread takes, or on a channel with a title. */
1063
- open_thread: z6.object({
1113
+ open_thread: z7.object({
1064
1114
  task_id: UlidSchema.optional(),
1065
1115
  proposal_id: UlidSchema.optional(),
1066
1116
  channel: ChannelRefSchema.optional(),
1067
- title: z6.string().min(1).max(200).optional()
1117
+ title: z7.string().min(1).max(200).optional()
1068
1118
  }),
1069
- close_thread: z6.object({ thread_id: UlidSchema, summary: z6.string().min(1) }),
1070
- create_task: z6.object({
1119
+ close_thread: z7.object({ thread_id: UlidSchema, summary: z7.string().min(1) }),
1120
+ create_task: z7.object({
1071
1121
  project: NameSchema,
1072
- title: z6.string().min(1).max(200),
1073
- body: z6.string().default(""),
1122
+ title: z7.string().min(1).max(200),
1123
+ body: z7.string().default(""),
1074
1124
  parent_id: UlidSchema.optional(),
1075
- required_capabilities: z6.array(z6.string()).default([]),
1125
+ required_capabilities: z7.array(z7.string()).default([]),
1076
1126
  /** The plan; without it the task gets one stage, work, that anyone in the project may take. */
1077
- stages: z6.array(PlanStageSchema).min(1).optional(),
1127
+ stages: z7.array(PlanStageSchema).min(1).optional(),
1078
1128
  /** A channel of the project the task belongs to, such as a release's; its thread opens there. */
1079
1129
  channel: NameSchema.default("general")
1080
1130
  }),
1081
- claim_task: z6.object({ task_id: UlidSchema }),
1082
- release_task: z6.object({ task_id: UlidSchema }),
1083
- update_task: z6.object({
1131
+ claim_task: z7.object({ task_id: UlidSchema }),
1132
+ release_task: z7.object({ task_id: UlidSchema }),
1133
+ update_task: z7.object({
1084
1134
  task_id: UlidSchema,
1085
1135
  /** Move the task back to this earlier stage. */
1086
1136
  stage: StageIdSchema.optional(),
1087
- status: z6.enum(["abandoned"]).optional(),
1137
+ status: z7.enum(["abandoned"]).optional(),
1088
1138
  /** Posted to the task's thread as the caller, marked with the step when the task moved. */
1089
- note: z6.string().min(1).optional(),
1090
- blocked_by: z6.array(UlidSchema).optional(),
1139
+ note: z7.string().min(1).optional(),
1140
+ blocked_by: z7.array(UlidSchema).optional(),
1091
1141
  /**
1092
1142
  * The pull request a `ghpr` task lands through, on its project's repository, with the merge
1093
1143
  * commit's subject and body; null unlinks it.
1094
1144
  */
1095
- pull_request: z6.object({
1145
+ pull_request: z7.object({
1096
1146
  url: PullRequestUrlSchema,
1097
1147
  merge_subject: MergeSubjectSchema.optional(),
1098
- merge_body: z6.string().max(2e4).optional()
1148
+ merge_body: z7.string().max(2e4).optional()
1099
1149
  }).nullable().optional()
1100
1150
  }),
1101
- get_task: z6.object({ task_id: UlidSchema }),
1102
- subscribe: z6.object({ channel: ChannelRefSchema }),
1103
- unsubscribe: z6.object({ channel: ChannelRefSchema }),
1104
- propose: z6.object({
1151
+ get_task: z7.object({ task_id: UlidSchema }),
1152
+ subscribe: z7.object({ channel: ChannelRefSchema }),
1153
+ unsubscribe: z7.object({ channel: ChannelRefSchema }),
1154
+ propose: z7.object({
1105
1155
  kind: ProposalKindSchema,
1106
- charter: z6.record(z6.string(), z6.unknown()),
1107
- rationale: z6.string().default("")
1156
+ charter: z7.record(z7.string(), z7.unknown()),
1157
+ rationale: z7.string().default("")
1108
1158
  }),
1109
- approve: z6.object({ proposal_id: UlidSchema, reason: z6.string().optional() }),
1110
- reject: z6.object({ proposal_id: UlidSchema, reason: z6.string().min(1) }),
1111
- create_project: z6.object({
1159
+ approve: z7.object({ proposal_id: UlidSchema, reason: z7.string().optional() }),
1160
+ reject: z7.object({ proposal_id: UlidSchema, reason: z7.string().min(1) }),
1161
+ create_project: z7.object({
1112
1162
  slug: NameSchema,
1113
- name: z6.string().min(1).optional(),
1114
- repo: z6.string().min(1).nullable().default(null),
1163
+ name: z7.string().min(1).optional(),
1164
+ repo: z7.string().min(1).nullable().default(null),
1115
1165
  default_branch: DefaultBranchSchema.default("main"),
1116
1166
  on_done: CompletionEffectSchema.optional()
1117
1167
  }),
1118
- join_project: z6.object({ project: NameSchema, agent: NameSchema.optional() }),
1119
- leave_project: z6.object({ project: NameSchema, agent: NameSchema.optional() }),
1120
- write_knowledge: z6.object({
1168
+ join_project: z7.object({ project: NameSchema, agent: NameSchema.optional() }),
1169
+ leave_project: z7.object({ project: NameSchema, agent: NameSchema.optional() }),
1170
+ write_knowledge: z7.object({
1121
1171
  /** A project you belong to, or null for society knowledge, which the steward and the user curate. */
1122
1172
  project: NameSchema.nullable().default(null),
1123
1173
  topic: NameSchema,
1124
- body: z6.string().min(1),
1174
+ body: z7.string().min(1),
1125
1175
  /** The `revision` in the frontmatter of the topic you read, 0 when it names none or for a new topic. */
1126
- revision: z6.number().int().min(0)
1176
+ revision: z7.number().int().min(0)
1127
1177
  }),
1128
- remove_knowledge: z6.object({
1178
+ remove_knowledge: z7.object({
1129
1179
  /** As for write_knowledge: a project you belong to, or null for society knowledge. */
1130
1180
  project: NameSchema.nullable().default(null),
1131
1181
  topic: NameSchema
1132
1182
  }),
1133
- plan_task: z6.object({
1183
+ plan_task: z7.object({
1134
1184
  task_id: UlidSchema,
1135
- stages: z6.array(PlanEditStageSchema),
1185
+ stages: z7.array(PlanEditStageSchema),
1136
1186
  on_done: CompletionEffectSchema.optional()
1137
1187
  }),
1138
1188
  /** The note is posted to the task's thread as the caller, marked with the step. */
1139
- advance_task: z6.object({ task_id: UlidSchema, note: z6.string().min(1).optional() }),
1140
- configure_project: z6.object({
1189
+ advance_task: z7.object({ task_id: UlidSchema, note: z7.string().min(1).optional() }),
1190
+ configure_project: z7.object({
1141
1191
  project: NameSchema,
1142
1192
  on_done: CompletionEffectSchema.optional(),
1143
1193
  /** A new display name; the slug, which everything refers to the project by, never changes. */
1144
- name: z6.string().trim().min(1).max(120).optional(),
1194
+ name: z7.string().trim().min(1).max(120).optional(),
1145
1195
  /** The branch tasks start from and land on from now; tasks in play keep their branches. */
1146
1196
  default_branch: DefaultBranchSchema.optional()
1147
1197
  }),
1148
- archive_project: z6.object({ project: NameSchema, reason: z6.string().min(1) }),
1149
- create_channel: z6.object({
1198
+ archive_project: z7.object({ project: NameSchema, reason: z7.string().min(1) }),
1199
+ create_channel: z7.object({
1150
1200
  /** A project, or null for a channel of the society. */
1151
1201
  project: NameSchema.nullable().default(null),
1152
1202
  name: NameSchema,
1153
- purpose: z6.string().min(1)
1203
+ purpose: z7.string().min(1)
1154
1204
  }),
1155
- archive_channel: z6.object({ channel: ChannelRefSchema, reason: z6.string().min(1) }),
1156
- update_dashboard: z6.object({
1205
+ archive_channel: z7.object({ channel: ChannelRefSchema, reason: z7.string().min(1) }),
1206
+ update_dashboard: z7.object({
1157
1207
  project: NameSchema,
1158
1208
  /** The whole dashboard as markdown; the board keeps its frontmatter. */
1159
- body: z6.string().min(1),
1209
+ body: z7.string().min(1),
1160
1210
  /** The `revision` in the frontmatter of the dashboard you read, 0 when it names none. */
1161
- revision: z6.number().int().min(0)
1211
+ revision: z7.number().int().min(0)
1162
1212
  }),
1163
- create_cron: z6.object({
1213
+ create_cron: z7.object({
1164
1214
  title: CronTitleSchema,
1165
1215
  /** What to do when it fires; quoted in the prompt of every turn it starts. */
1166
1216
  note: CronNoteSchema,
@@ -1179,10 +1229,10 @@ var VerbInputs = {
1179
1229
  /** An open channel of that place other than general, whose conversation it fires in. */
1180
1230
  channel: NameSchema.optional()
1181
1231
  }),
1182
- update_cron: z6.object({
1232
+ update_cron: z7.object({
1183
1233
  cron_id: UlidSchema,
1184
1234
  /** Pause it, or resume it, which starts its schedule afresh from now. */
1185
- paused: z6.boolean().optional(),
1235
+ paused: z7.boolean().optional(),
1186
1236
  title: CronTitleSchema.optional(),
1187
1237
  note: CronNoteSchema.optional(),
1188
1238
  /** A new expression, or `at` for one time; either starts the schedule afresh. */
@@ -1191,8 +1241,12 @@ var VerbInputs = {
1191
1241
  timezone: TimeZoneSchema.nullable().optional(),
1192
1242
  at: MomentSchema.optional()
1193
1243
  }),
1194
- remove_cron: z6.object({ cron_id: UlidSchema, reason: z6.string().trim().min(1) }),
1195
- remove_skill: z6.object({ name: NameSchema, reason: z6.string().trim().min(1) })
1244
+ remove_cron: z7.object({ cron_id: UlidSchema, reason: z7.string().trim().min(1) }),
1245
+ remove_skill: z7.object({ name: NameSchema, reason: z7.string().trim().min(1) }),
1246
+ upload_image: z7.object({
1247
+ /** The image file on the machine your turn runs on: absolute, or relative to your working directory. */
1248
+ path: ImagePathSchema
1249
+ })
1196
1250
  };
1197
1251
  var VERB_DESCRIPTIONS = {
1198
1252
  post_message: "Post a markdown message to a channel, or into a thread when thread_id is given; a thread's message goes to the thread's channel.",
@@ -1203,7 +1257,7 @@ var VERB_DESCRIPTIONS = {
1203
1257
  create_task: "Create a task in a project with its plan: stages of {name, role or agent, gate}. Without stages it gets one stage, work, that anyone in the project may take. File it in a channel of the project, such as a release's, or general by default; its thread opens there with it, under the task's id, for everything said about the work.",
1204
1258
  claim_task: "Hold the task's current stage. Claims are leases renewed by every turn that touches the task.",
1205
1259
  release_task: "Let go of the stage you hold so someone else can take it.",
1206
- update_task: "Move a task back to an earlier stage, abandon it, set the tasks it is blocked by (blocked_by: its stage wakes nobody until they have all ended), or link its pull request (pull_request: url, with the merge commit's merge_subject and merge_body), which a ghpr task needs before its last stage completes. A note is posted to the task's thread.",
1260
+ update_task: "Move a task back to an earlier stage (the current stage's holder, the user, the steward, or the concierge; anyone else asks the concierge in the task's thread), abandon it, set the tasks it is blocked by (blocked_by: its stage wakes nobody until they have all ended), or link its pull request (pull_request: url, with the merge commit's merge_subject and merge_body), which a ghpr task needs before its last stage completes. A note is posted to the task's thread.",
1207
1261
  get_task: "Read a task with its plan, its brief, and the messages of its thread.",
1208
1262
  subscribe: "Subscribe to a channel. Subscriptions feed your digest; they never wake you.",
1209
1263
  unsubscribe: "Unsubscribe from a channel.",
@@ -1225,11 +1279,12 @@ var VERB_DESCRIPTIONS = {
1225
1279
  create_cron: "Set a cron that wakes you later: a title, a note on what to do then, and either cron, a five-field expression (minute hour day-of-month month day-of-week) read in timezone or the society's, or at, one time. It fires in this turn's conversation unless you name a project, thread_id, or channel, at most every 15 minutes, and every fire is a turn. Only the user, the steward, and the concierge set one that wakes another citizen (agent).",
1226
1280
  update_cron: "Pause or resume a cron, or change its title, note, or schedule; resuming or rescheduling starts it afresh from now. Yours, or anyone's for the user, the steward, and the concierge.",
1227
1281
  remove_cron: "End a cron for good, with a reason. Yours, or anyone's for the user, the steward, and the concierge. A cron also ends with its conversation, and a one-time cron once it fires.",
1228
- remove_skill: "Remove a skill from the society's skills, with a reason, when it is wrong, superseded, or no longer used: no skills index lists it from the next turn, and a notice in general says so with the reason. Its proposer's own copy stays in its home. Steward and user only; the board keeps its text aside for the user to restore."
1282
+ remove_skill: "Remove a skill from the society's skills, with a reason, when it is wrong, superseded, or no longer used: no skills index lists it from the next turn, and a notice in general says so with the reason. Its proposer's own copy stays in its home. Steward and user only; the board keeps its text aside for the user to restore.",
1283
+ upload_image: "Upload an image file, such as a screenshot or a chart, from the machine your turn runs on: a PNG, JPEG, GIF, or WebP of at most 8 MB, in your worktree, your home, or the system temp folder. Returns its id and the markdown that shows it in a post, `![what it shows](image:<id>)`; write what it shows as the alt text."
1229
1284
  };
1230
1285
 
1231
1286
  // packages/shared/dist/events.js
1232
- import { z as z7 } from "zod";
1287
+ import { z as z8 } from "zod";
1233
1288
  var BOARD_EVENT_TYPES = [
1234
1289
  "society.initialized",
1235
1290
  "project.added",
@@ -1293,21 +1348,22 @@ var BOARD_EVENT_TYPES = [
1293
1348
  "cron.updated",
1294
1349
  "cron.fired",
1295
1350
  "cron.skipped",
1296
- "cron.ended"
1351
+ "cron.ended",
1352
+ "image.uploaded"
1297
1353
  ];
1298
- var BoardEventTypeSchema = z7.enum(BOARD_EVENT_TYPES);
1299
- var BoardEventSchema = z7.object({
1354
+ var BoardEventTypeSchema = z8.enum(BOARD_EVENT_TYPES);
1355
+ var BoardEventSchema = z8.object({
1300
1356
  id: UlidSchema,
1301
1357
  ts: IsoDateTimeSchema,
1302
1358
  type: BoardEventTypeSchema,
1303
1359
  actor: NameSchema,
1304
- payload: z7.record(z7.string(), z7.unknown())
1360
+ payload: z8.record(z8.string(), z8.unknown())
1305
1361
  });
1306
- var UsageSchema = z7.object({
1307
- inputTokens: z7.number().int().nonnegative(),
1308
- outputTokens: z7.number().int().nonnegative(),
1309
- cacheReadTokens: z7.number().int().nonnegative().default(0),
1310
- cacheWriteTokens: z7.number().int().nonnegative().default(0)
1362
+ var UsageSchema = z8.object({
1363
+ inputTokens: z8.number().int().nonnegative(),
1364
+ outputTokens: z8.number().int().nonnegative(),
1365
+ cacheReadTokens: z8.number().int().nonnegative().default(0),
1366
+ cacheWriteTokens: z8.number().int().nonnegative().default(0)
1311
1367
  });
1312
1368
  function addUsage(a, b) {
1313
1369
  return {
@@ -1317,14 +1373,14 @@ function addUsage(a, b) {
1317
1373
  cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens
1318
1374
  };
1319
1375
  }
1320
- var TurnStatusSchema = z7.object({
1321
- summary: z7.string().min(1),
1322
- claimsHeld: z7.array(UlidSchema).default([]),
1323
- blockedOn: z7.array(z7.string()).default([]),
1324
- needsUserDecision: z7.boolean().default(false),
1325
- memoryUpdated: z7.boolean().default(false)
1376
+ var TurnStatusSchema = z8.object({
1377
+ summary: z8.string().min(1),
1378
+ claimsHeld: z8.array(UlidSchema).default([]),
1379
+ blockedOn: z8.array(z8.string()).default([]),
1380
+ needsUserDecision: z8.boolean().default(false),
1381
+ memoryUpdated: z8.boolean().default(false)
1326
1382
  });
1327
- var TurnExitReasonSchema = z7.enum([
1383
+ var TurnExitReasonSchema = z8.enum([
1328
1384
  "completed",
1329
1385
  "timeout",
1330
1386
  "error",
@@ -1332,50 +1388,51 @@ var TurnExitReasonSchema = z7.enum([
1332
1388
  "blocked",
1333
1389
  "stopped"
1334
1390
  ]);
1335
- var AgentEventSchema = z7.discriminatedUnion("type", [
1336
- z7.object({
1337
- type: z7.literal("turn_started"),
1391
+ var ResumeFailureSchema = z8.enum(["lost", "retry"]);
1392
+ var AgentEventSchema = z8.discriminatedUnion("type", [
1393
+ z8.object({
1394
+ type: z8.literal("turn_started"),
1338
1395
  agent: NameSchema,
1339
- session: z7.string(),
1396
+ session: z8.string(),
1340
1397
  runner: NameSchema,
1341
1398
  /** The model the CLI reports running, when it reports one. */
1342
- model: z7.string().optional()
1399
+ model: z8.string().optional()
1343
1400
  }),
1344
1401
  /**
1345
1402
  * The CLI took a steer into the running turn. Adapters name the steer; the server adds the
1346
1403
  * messages it delivered and the text the CLI was given.
1347
1404
  */
1348
- z7.object({
1349
- type: z7.literal("steered"),
1350
- steer: z7.string().min(1),
1351
- messages: z7.array(UlidSchema).optional(),
1352
- text: z7.string().optional()
1405
+ z8.object({
1406
+ type: z8.literal("steered"),
1407
+ steer: z8.string().min(1),
1408
+ messages: z8.array(UlidSchema).optional(),
1409
+ text: z8.string().optional()
1353
1410
  }),
1354
- z7.object({ type: z7.literal("text"), delta: z7.string() }),
1355
- z7.object({ type: z7.literal("tool_call"), name: z7.string(), input: z7.unknown() }),
1356
- z7.object({
1357
- type: z7.literal("tool_result"),
1358
- name: z7.string(),
1359
- ok: z7.boolean(),
1411
+ z8.object({ type: z8.literal("text"), delta: z8.string() }),
1412
+ z8.object({ type: z8.literal("tool_call"), name: z8.string(), input: z8.unknown() }),
1413
+ z8.object({
1414
+ type: z8.literal("tool_result"),
1415
+ name: z8.string(),
1416
+ ok: z8.boolean(),
1360
1417
  /** What the tool returned, cut to `OUTPUT_LIMIT` characters by `capOutput`. */
1361
- output: z7.string().optional()
1418
+ output: z8.string().optional()
1362
1419
  }),
1363
- z7.object({ type: z7.literal("approval_requested"), kind: z7.string(), detail: z7.unknown() }),
1364
- z7.object({
1365
- type: z7.literal("turn_completed"),
1420
+ z8.object({ type: z8.literal("approval_requested"), kind: z8.string(), detail: z8.unknown() }),
1421
+ z8.object({
1422
+ type: z8.literal("turn_completed"),
1366
1423
  usage: UsageSchema,
1367
- costUsd: z7.number().nonnegative(),
1424
+ costUsd: z8.number().nonnegative(),
1368
1425
  status: TurnStatusSchema.nullable(),
1369
1426
  exitReason: TurnExitReasonSchema
1370
1427
  }),
1371
- z7.object({ type: z7.literal("error"), message: z7.string() })
1428
+ z8.object({ type: z8.literal("error"), message: z8.string() })
1372
1429
  ]);
1373
- var TranscriptEntrySchema = z7.object({
1430
+ var TranscriptEntrySchema = z8.object({
1374
1431
  ts: IsoDateTimeSchema,
1375
1432
  event: AgentEventSchema
1376
1433
  });
1377
- var LiveTurnEventSchema = z7.object({
1378
- seq: z7.number().int().positive(),
1434
+ var LiveTurnEventSchema = z8.object({
1435
+ seq: z8.number().int().positive(),
1379
1436
  ts: IsoDateTimeSchema,
1380
1437
  agent: NameSchema,
1381
1438
  project: NameSchema,
@@ -1383,14 +1440,16 @@ var LiveTurnEventSchema = z7.object({
1383
1440
  thread: UlidSchema.optional(),
1384
1441
  /** The channel, never general, whose conversation the turn is in. */
1385
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(),
1386
1445
  event: AgentEventSchema
1387
1446
  });
1388
1447
 
1389
1448
  // packages/shared/dist/turns.js
1390
- import { z as z9 } from "zod";
1449
+ import { z as z10 } from "zod";
1391
1450
 
1392
1451
  // packages/shared/dist/runner.js
1393
- import { z as z8 } from "zod";
1452
+ import { z as z9 } from "zod";
1394
1453
  var RUNNER_PROTOCOL = 1;
1395
1454
  var PATH_TOKENS = {
1396
1455
  home: "{{stellaris:home}}",
@@ -1401,75 +1460,77 @@ var SERVER_TOKEN = "{{stellaris:server}}";
1401
1460
  var WORKSPACE_TOKEN = "{{stellaris:workspace}}";
1402
1461
  var HOME_SCRATCH = "scratch";
1403
1462
  var HOME_FILE_LIMIT_BYTES = 8 * 1024 * 1024;
1404
- var RunnerHelloSchema = z8.object({
1405
- protocol: z8.number().int().positive(),
1406
- version: z8.string().min(1),
1463
+ var RunnerHelloSchema = z9.object({
1464
+ protocol: z9.number().int().positive(),
1465
+ version: z9.string().min(1),
1407
1466
  os: RunnerOsSchema,
1408
- clis: z8.array(CliKindSchema),
1467
+ clis: z9.array(CliKindSchema),
1409
1468
  /** The CLIs whose adapter can keep a session warm between turns. */
1410
- residentClis: z8.array(CliKindSchema).default([]),
1469
+ residentClis: z9.array(CliKindSchema).default([]),
1411
1470
  /** The CLIs whose adapter can take input into a running turn; the server steers only these. */
1412
- steerableClis: z8.array(CliKindSchema).default([]),
1471
+ steerableClis: z9.array(CliKindSchema).default([]),
1413
1472
  /** The CLIs whose adapter can stop a running turn; the server stops only these. */
1414
- stoppableClis: z8.array(CliKindSchema).default([]),
1473
+ stoppableClis: z9.array(CliKindSchema).default([]),
1415
1474
  /** Whether the runner answers file reads on its projects' branches; one from before never does. */
1416
- branchReads: z8.boolean().default(false),
1475
+ branchReads: z9.boolean().default(false),
1417
1476
  /** Whether the runner lists what a branch changed; one from before never does. */
1418
- branchChanges: z8.boolean().default(false),
1477
+ branchChanges: z9.boolean().default(false),
1419
1478
  /** Whether the runner answers a read of a CLI's quota; one from before never does. */
1420
- quotaReads: z8.boolean().default(false),
1421
- capabilities: z8.array(z8.string()).default([]),
1479
+ quotaReads: z9.boolean().default(false),
1480
+ /** Whether the runner reads an image a turn running there uploads; one from before never does. */
1481
+ imageReads: z9.boolean().default(false),
1482
+ capabilities: z9.array(z9.string()).default([]),
1422
1483
  /**
1423
1484
  * What the runner's account lacks of the git identity citizens' own commits carry, checked
1424
1485
  * before each hello outside any repository; empty when it has both. Absent from a runner from
1425
1486
  * before the check, which no default fills in, so the board marks only what a runner reported.
1426
1487
  */
1427
- gitIdentityMissing: z8.array(GitIdentityPartSchema).optional(),
1488
+ gitIdentityMissing: z9.array(GitIdentityPartSchema).optional(),
1428
1489
  /** Turns the runner runs at once, or null for no limit. */
1429
- slots: z8.number().int().positive().nullable(),
1490
+ slots: z9.number().int().positive().nullable(),
1430
1491
  /** Turns the runner is still running, when it registers again; the server fails the rest of its turns. */
1431
- turns: z8.array(UlidSchema).default([])
1492
+ turns: z9.array(UlidSchema).default([])
1432
1493
  });
1433
- var RunnerWelcomeSchema = z8.object({ name: NameSchema, version: z8.string() });
1434
- var RunnerEnrollRequestSchema = z8.object({
1435
- protocol: z8.number().int().positive(),
1436
- version: z8.string().min(1).max(64),
1437
- hostname: z8.string().min(1).max(253),
1494
+ var RunnerWelcomeSchema = z9.object({ name: NameSchema, version: z9.string() });
1495
+ var RunnerEnrollRequestSchema = z9.object({
1496
+ protocol: z9.number().int().positive(),
1497
+ version: z9.string().min(1).max(64),
1498
+ hostname: z9.string().min(1).max(253),
1438
1499
  os: RunnerOsSchema,
1439
- clis: z8.array(CliKindSchema).max(8),
1440
- capabilities: z8.array(z8.string().min(1).max(64)).max(32).default([])
1500
+ clis: z9.array(CliKindSchema).max(8),
1501
+ capabilities: z9.array(z9.string().min(1).max(64)).max(32).default([])
1441
1502
  });
1442
- var RunnerEnrollmentSchema = z8.object({
1443
- deviceCode: z8.string().min(1),
1444
- userCode: z8.string().min(1),
1503
+ var RunnerEnrollmentSchema = z9.object({
1504
+ deviceCode: z9.string().min(1),
1505
+ userCode: z9.string().min(1),
1445
1506
  expiresAt: IsoDateTimeSchema,
1446
- intervalMs: z8.number().int().positive()
1507
+ intervalMs: z9.number().int().positive()
1447
1508
  });
1448
- var RunnerEnrollPollSchema = z8.object({ deviceCode: z8.string().min(1) });
1449
- var RunnerEnrollStatusSchema = z8.discriminatedUnion("status", [
1450
- z8.object({ status: z8.literal("pending") }),
1451
- z8.object({ status: z8.literal("approved"), name: NameSchema, token: z8.string().min(1) }),
1452
- z8.object({ status: z8.literal("denied") })
1509
+ var RunnerEnrollPollSchema = z9.object({ deviceCode: z9.string().min(1) });
1510
+ var RunnerEnrollStatusSchema = z9.discriminatedUnion("status", [
1511
+ z9.object({ status: z9.literal("pending") }),
1512
+ z9.object({ status: z9.literal("approved"), name: NameSchema, token: z9.string().min(1) }),
1513
+ z9.object({ status: z9.literal("denied") })
1453
1514
  ]);
1454
- var PendingEnrollmentSchema = z8.object({
1455
- userCode: z8.string().min(1),
1456
- hostname: z8.string(),
1515
+ var PendingEnrollmentSchema = z9.object({
1516
+ userCode: z9.string().min(1),
1517
+ hostname: z9.string(),
1457
1518
  os: RunnerOsSchema,
1458
- clis: z8.array(CliKindSchema),
1459
- capabilities: z8.array(z8.string()),
1460
- version: z8.string(),
1519
+ clis: z9.array(CliKindSchema),
1520
+ capabilities: z9.array(z9.string()),
1521
+ version: z9.string(),
1461
1522
  requestedAt: IsoDateTimeSchema,
1462
1523
  expiresAt: IsoDateTimeSchema
1463
1524
  });
1464
- var ProjectRepoSchema = z8.object({
1525
+ var ProjectRepoSchema = z9.object({
1465
1526
  slug: NameSchema,
1466
- origin: z8.string().nullable(),
1467
- defaultBranch: z8.string().min(1),
1527
+ origin: z9.string().nullable(),
1528
+ defaultBranch: z9.string().min(1),
1468
1529
  /**
1469
1530
  * The board lands finished tasks on the default branch, itself or through their pull requests,
1470
1531
  * so the runner lets nothing else move it.
1471
1532
  */
1472
- boardLands: z8.boolean()
1533
+ boardLands: z9.boolean()
1473
1534
  });
1474
1535
  function projectRepo(project) {
1475
1536
  return {
@@ -1479,163 +1540,196 @@ function projectRepo(project) {
1479
1540
  boardLands: project.onDone !== "none"
1480
1541
  };
1481
1542
  }
1482
- var TurnWorkspaceSchema = z8.discriminatedUnion("kind", [
1483
- z8.object({
1484
- kind: z8.literal("home"),
1543
+ var TurnWorkspaceSchema = z9.discriminatedUnion("kind", [
1544
+ z9.object({
1545
+ kind: z9.literal("home"),
1485
1546
  thread: UlidSchema.optional(),
1486
- channel: NameSchema.optional()
1547
+ channel: NameSchema.optional(),
1548
+ place: NameSchema.optional()
1487
1549
  }),
1488
- z8.object({
1489
- kind: z8.literal("project"),
1550
+ z9.object({
1551
+ kind: z9.literal("project"),
1490
1552
  repo: ProjectRepoSchema,
1491
- branches: z8.array(z8.string().min(1)),
1553
+ branches: z9.array(z9.string().min(1)),
1492
1554
  thread: UlidSchema.optional(),
1493
1555
  channel: NameSchema.optional()
1494
1556
  }),
1495
- z8.object({ kind: z8.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
+ })
1496
1567
  ]);
1497
- var TurnJobSchema = z8.object({
1568
+ var TurnJobSchema = z9.object({
1498
1569
  turnId: UlidSchema,
1499
1570
  agent: NameSchema,
1500
1571
  role: NameSchema,
1501
1572
  cli: CliKindSchema,
1502
- model: z8.string().optional(),
1573
+ model: z9.string().optional(),
1503
1574
  /** The reasoning effort set on the agent; absent for the model's own default. */
1504
- effort: z8.string().optional(),
1575
+ effort: z9.string().optional(),
1505
1576
  /** A project slug, or the society scope. */
1506
1577
  scope: NameSchema,
1507
1578
  /** The thread whose conversation the turn is in; absent for the home conversation. */
1508
1579
  thread: UlidSchema.optional(),
1509
1580
  /** The channel, never general, whose conversation the turn is in. */
1510
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(),
1511
1584
  /**
1512
1585
  * The session to resume, or null to start one: the CLI's adapter picks a fresh session's id, and
1513
1586
  * the outcome reports it.
1514
1587
  */
1515
- session: z8.string().min(1).nullable(),
1588
+ session: z9.string().min(1).nullable(),
1516
1589
  /** The running total the session reported after its last turn, which a resumed session starts from. */
1517
- costSoFarUsd: z8.number().nonnegative(),
1590
+ costSoFarUsd: z9.number().nonnegative(),
1518
1591
  /** The rendered charter, norms, memory core, and skills index, with `PATH_TOKENS` for places. */
1519
- instructions: z8.string(),
1520
- prompt: z8.string(),
1521
- mcp: z8.object({ url: z8.string().min(1), token: z8.string().min(1) }),
1522
- limits: z8.object({
1523
- timeoutMs: z8.number().int().positive().nullable(),
1524
- maxTurns: z8.number().int().positive().nullable()
1592
+ instructions: z9.string(),
1593
+ prompt: z9.string(),
1594
+ mcp: z9.object({ url: z9.string().min(1), token: z9.string().min(1) }),
1595
+ limits: z9.object({
1596
+ timeoutMs: z9.number().int().positive().nullable(),
1597
+ maxTurns: z9.number().int().positive().nullable()
1525
1598
  }),
1526
1599
  /** Present when the runner keeps the conversation's session warm between turns, under `key`. */
1527
- resident: z8.object({ key: z8.string().min(1), idleMs: z8.number().int().positive() }).optional(),
1600
+ resident: z9.object({ key: z9.string().min(1), idleMs: z9.number().int().positive() }).optional(),
1528
1601
  workspace: TurnWorkspaceSchema
1529
1602
  });
1530
- var TurnWorkSchema = z8.object({ branch: z8.string().min(1), head: z8.string().min(1) });
1531
- var TurnEventsSchema = z8.object({ entries: z8.array(TranscriptEntrySchema) });
1532
- var TurnOutcomeSchema = z8.object({
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
+ });
1610
+ var TurnEventsSchema = z9.object({ entries: z9.array(TranscriptEntrySchema) });
1611
+ var TurnOutcomeSchema = z9.object({
1533
1612
  exitReason: TurnExitReasonSchema,
1534
1613
  status: TurnStatusSchema.nullable(),
1535
- error: z8.string().nullable(),
1614
+ error: z9.string().nullable(),
1536
1615
  usage: UsageSchema,
1537
- costUsd: z8.number().nonnegative(),
1616
+ costUsd: z9.number().nonnegative(),
1538
1617
  /** The session's running total after this turn, for CLIs that report one. */
1539
- sessionCostUsd: z8.number().nonnegative().optional(),
1540
- /** The session the CLI used, which differs from the job's for CLIs that assign their own ids. */
1541
- session: z8.string().min(1),
1618
+ sessionCostUsd: z9.number().nonnegative().optional(),
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(),
1542
1631
  /** The model the CLI reported, when it reported one. */
1543
- model: z8.string().nullable(),
1632
+ model: z9.string().nullable(),
1544
1633
  work: TurnWorkSchema.nullable(),
1545
1634
  /**
1546
1635
  * Whether the turn's own workspace, a thread's, a channel's, or a task's, holds work on no
1547
1636
  * branch after the hand-back: uncommitted changes, or commits no branch contains.
1548
1637
  */
1549
- leftovers: z8.boolean().default(false),
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(),
1550
1644
  /**
1551
1645
  * The CLI's plan quota as the turn's stream last reported it, when it reported any; a runner from
1552
1646
  * before never sends it.
1553
1647
  */
1554
1648
  quota: CliQuotaSchema.optional()
1555
1649
  });
1556
- var TurnAckSchema = z8.object({ dropWorkspace: z8.boolean() });
1557
- var LandRequestSchema = z8.object({
1650
+ var TurnAckSchema = z9.object({ dropWorkspace: z9.boolean() });
1651
+ var LandRequestSchema = z9.object({
1558
1652
  repo: ProjectRepoSchema,
1559
- branch: z8.string().min(1),
1653
+ branch: z9.string().min(1),
1560
1654
  /**
1561
1655
  * For a `ghpr` task, the pull request to merge in place of the branch, which must hold the
1562
1656
  * branch's files, and the merge commit's subject and body, the pull request's title and an
1563
1657
  * empty body when left out.
1564
1658
  */
1565
- pullRequest: z8.object({
1566
- url: z8.string().min(1),
1567
- subject: z8.string().min(1).optional(),
1568
- body: z8.string().optional()
1659
+ pullRequest: z9.object({
1660
+ url: z9.string().min(1),
1661
+ subject: z9.string().min(1).optional(),
1662
+ body: z9.string().optional()
1569
1663
  }).optional()
1570
1664
  });
1571
- var CloseRequestSchema = z8.object({
1665
+ var CloseRequestSchema = z9.object({
1572
1666
  repo: ProjectRepoSchema,
1573
- url: z8.string().min(1),
1574
- comment: z8.string().min(1)
1667
+ url: z9.string().min(1),
1668
+ comment: z9.string().min(1)
1575
1669
  });
1576
- var MergeOutcomeSchema = z8.object({ ok: z8.boolean(), detail: z8.string() });
1577
- var ModelListSchema = z8.array(ModelOptionSchema);
1670
+ var MergeOutcomeSchema = z9.object({ ok: z9.boolean(), detail: z9.string() });
1671
+ var ModelListSchema = z9.array(ModelOptionSchema);
1578
1672
  var BRANCH_FILE_LIMIT_BYTES = 8 * 1024 * 1024;
1579
- var BranchPathSchema = z8.string().max(4096).refine((value) => value === "" || value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".." && !segment.includes("\0")), { message: "a path inside the branch, with no empty, `.`, or `..` segment" });
1580
- var BranchNameSchema = z8.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/);
1581
- var BranchReadSchema = z8.object({
1673
+ var BranchPathSchema = z9.string().max(4096).refine((value) => value === "" || value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".." && !segment.includes("\0")), { message: "a path inside the branch, with no empty, `.`, or `..` segment" });
1674
+ var BranchNameSchema = z9.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/);
1675
+ var BranchReadSchema = z9.object({
1582
1676
  repo: ProjectRepoSchema,
1583
1677
  branch: BranchNameSchema,
1584
1678
  path: BranchPathSchema
1585
1679
  });
1586
- var BranchCommitSchema = z8.object({
1587
- id: z8.string().min(1),
1680
+ var BranchCommitSchema = z9.object({
1681
+ id: z9.string().min(1),
1588
1682
  at: IsoDateTimeSchema,
1589
- author: z8.string()
1683
+ author: z9.string()
1590
1684
  });
1591
- var BranchFileSchema = z8.discriminatedUnion("kind", [
1592
- z8.object({
1593
- kind: z8.literal("file"),
1685
+ var BranchFileSchema = z9.discriminatedUnion("kind", [
1686
+ z9.object({
1687
+ kind: z9.literal("file"),
1594
1688
  path: BranchPathSchema,
1595
1689
  commit: BranchCommitSchema,
1596
- size: z8.number().int().nonnegative(),
1597
- content: z8.string().nullable()
1690
+ size: z9.number().int().nonnegative(),
1691
+ content: z9.string().nullable()
1598
1692
  }),
1599
- z8.object({
1600
- kind: z8.literal("dir"),
1693
+ z9.object({
1694
+ kind: z9.literal("dir"),
1601
1695
  path: BranchPathSchema,
1602
1696
  commit: BranchCommitSchema,
1603
- entries: z8.array(z8.object({
1604
- name: z8.string().min(1),
1605
- kind: z8.enum(["file", "dir"]),
1606
- size: z8.number().int().nonnegative().nullable()
1697
+ entries: z9.array(z9.object({
1698
+ name: z9.string().min(1),
1699
+ kind: z9.enum(["file", "dir"]),
1700
+ size: z9.number().int().nonnegative().nullable()
1607
1701
  }))
1608
1702
  })
1609
1703
  ]);
1610
- var BranchChangesReadSchema = z8.object({
1704
+ var BranchChangesReadSchema = z9.object({
1611
1705
  repo: ProjectRepoSchema,
1612
1706
  branch: BranchNameSchema
1613
1707
  });
1614
- var BranchFileChangeSchema = z8.object({
1615
- path: z8.string().min(1),
1616
- status: z8.enum(["added", "modified", "deleted"]),
1708
+ var BranchFileChangeSchema = z9.object({
1709
+ path: z9.string().min(1),
1710
+ status: z9.enum(["added", "modified", "deleted"]),
1617
1711
  /** Lines added and removed, or null for a binary file. */
1618
- added: z8.number().int().nonnegative().nullable(),
1619
- removed: z8.number().int().nonnegative().nullable(),
1712
+ added: z9.number().int().nonnegative().nullable(),
1713
+ removed: z9.number().int().nonnegative().nullable(),
1620
1714
  /** Absent when the file changed only through a merge of another branch. */
1621
- lastChange: z8.object({ author: z8.string(), at: IsoDateTimeSchema }).optional()
1715
+ lastChange: z9.object({ author: z9.string(), at: IsoDateTimeSchema }).optional()
1622
1716
  });
1623
- var BranchChangesSchema = z8.object({
1717
+ var BranchChangesSchema = z9.object({
1624
1718
  head: BranchCommitSchema,
1625
- files: z8.array(BranchFileChangeSchema),
1626
- total: z8.number().int().nonnegative()
1719
+ files: z9.array(BranchFileChangeSchema),
1720
+ total: z9.number().int().nonnegative()
1627
1721
  });
1628
- var BranchDiffReadSchema = z8.object({
1722
+ var BranchDiffReadSchema = z9.object({
1629
1723
  repo: ProjectRepoSchema,
1630
1724
  branch: BranchNameSchema,
1631
1725
  path: BranchPathSchema.min(1)
1632
1726
  });
1633
- var BranchFileDiffSchema = z8.object({
1727
+ var BranchFileDiffSchema = z9.object({
1634
1728
  head: BranchCommitSchema,
1635
1729
  path: BranchPathSchema,
1636
1730
  status: BranchFileChangeSchema.shape.status,
1637
- patch: z8.string().nullable(),
1638
- truncated: z8.boolean()
1731
+ patch: z9.string().nullable(),
1732
+ truncated: z9.boolean()
1639
1733
  });
1640
1734
  function patchHunks(patch, limit2) {
1641
1735
  if (/^Binary files .* differ$/m.test(patch)) {
@@ -1649,75 +1743,98 @@ function patchHunks(patch, limit2) {
1649
1743
  const cut = hunks.lastIndexOf("\n", limit2);
1650
1744
  return { patch: hunks.slice(0, cut === -1 ? limit2 : cut), truncated: true };
1651
1745
  }
1652
- var WorkspaceConversationSchema = z8.discriminatedUnion("kind", [
1653
- z8.object({ kind: z8.literal("thread"), id: UlidSchema }),
1654
- z8.object({ kind: z8.literal("channel"), scope: NameSchema, name: NameSchema })
1746
+ var TurnImageReadSchema = z9.object({
1747
+ turnId: UlidSchema,
1748
+ path: ImagePathSchema
1749
+ });
1750
+ var ImageRefusalSchema = z9.enum(["missing", "outside", "not_image", "too_big"]);
1751
+ var TurnImageSchema = z9.discriminatedUnion("kind", [
1752
+ z9.object({
1753
+ kind: z9.literal("image"),
1754
+ type: ImageTypeSchema,
1755
+ size: z9.number().int().positive(),
1756
+ content: z9.string()
1757
+ }),
1758
+ z9.object({ kind: z9.literal("refused"), why: ImageRefusalSchema, detail: z9.string() })
1759
+ ]);
1760
+ var WorkspaceConversationSchema = z9.discriminatedUnion("kind", [
1761
+ z9.object({ kind: z9.literal("thread"), id: UlidSchema }),
1762
+ z9.object({ kind: z9.literal("channel"), scope: NameSchema, name: NameSchema }),
1763
+ z9.object({ kind: z9.literal("place"), project: NameSchema })
1655
1764
  ]);
1656
1765
  function workspaceConversationOf(turn) {
1657
1766
  if (turn.thread !== void 0) {
1658
1767
  return { kind: "thread", id: turn.thread };
1659
1768
  }
1660
- 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 };
1661
1773
  }
1662
- var HeldWorkspaceSchema = z8.object({
1774
+ var HeldWorkspaceSchema = z9.object({
1663
1775
  agent: NameSchema,
1664
1776
  scope: NameSchema,
1665
1777
  conversation: WorkspaceConversationSchema
1666
1778
  });
1667
- var HeldWorkspacesSchema = z8.object({ workspaces: z8.array(HeldWorkspaceSchema) });
1668
- var SweepResultSchema = z8.object({ kept: z8.array(HeldWorkspaceSchema) });
1669
- var TurnSteerSchema = z8.object({ id: z8.uuid(), text: z8.string().min(1) });
1670
- var RunnerMessageSchema = z8.discriminatedUnion("type", [
1671
- z8.object({ type: z8.literal("turn"), job: TurnJobSchema }),
1672
- z8.object({ type: z8.literal("land"), request: z8.string().min(1), land: LandRequestSchema }),
1779
+ var HeldWorkspacesSchema = z9.object({ workspaces: z9.array(HeldWorkspaceSchema) });
1780
+ var SweepResultSchema = z9.object({ kept: z9.array(HeldWorkspaceSchema) });
1781
+ var TurnSteerSchema = z9.object({ id: z9.uuid(), text: z9.string().min(1) });
1782
+ var RunnerMessageSchema = z9.discriminatedUnion("type", [
1783
+ z9.object({ type: z9.literal("turn"), job: TurnJobSchema }),
1784
+ z9.object({ type: z9.literal("land"), request: z9.string().min(1), land: LandRequestSchema }),
1673
1785
  /** Answered with a `MergeOutcome`. */
1674
- z8.object({ type: z8.literal("close"), request: z8.string().min(1), close: CloseRequestSchema }),
1675
- z8.object({ type: z8.literal("models"), request: z8.string().min(1), cli: CliKindSchema }),
1786
+ z9.object({ type: z9.literal("close"), request: z9.string().min(1), close: CloseRequestSchema }),
1787
+ z9.object({ type: z9.literal("models"), request: z9.string().min(1), cli: CliKindSchema }),
1676
1788
  /**
1677
1789
  * Answered with a `CliQuota` read from the CLI without a turn, or null when its adapter cannot
1678
1790
  * read one. Sent only to runners whose hello says `quotaReads`.
1679
1791
  */
1680
- z8.object({ type: z8.literal("quota"), request: z8.string().min(1), cli: CliKindSchema }),
1792
+ z9.object({ type: z9.literal("quota"), request: z9.string().min(1), cli: CliKindSchema }),
1681
1793
  /** Answered with a `BranchFile`, or null when the branch or the path does not exist. */
1682
- z8.object({ type: z8.literal("file"), request: z8.string().min(1), read: BranchReadSchema }),
1794
+ z9.object({ type: z9.literal("file"), request: z9.string().min(1), read: BranchReadSchema }),
1683
1795
  /** Answered with `BranchChanges`, or null when the repository or the branch does not exist. */
1684
- z8.object({
1685
- type: z8.literal("changes"),
1686
- request: z8.string().min(1),
1796
+ z9.object({
1797
+ type: z9.literal("changes"),
1798
+ request: z9.string().min(1),
1687
1799
  read: BranchChangesReadSchema
1688
1800
  }),
1689
1801
  /** Answered with a `BranchFileDiff`, or null when the branch does not exist or left the path alone. */
1690
- z8.object({ type: z8.literal("diff"), request: z8.string().min(1), read: BranchDiffReadSchema }),
1802
+ z9.object({ type: z9.literal("diff"), request: z9.string().min(1), read: BranchDiffReadSchema }),
1803
+ /**
1804
+ * Answered with a `TurnImage`, or null when the turn is not running there. Sent only to runners
1805
+ * whose hello says `imageReads`.
1806
+ */
1807
+ z9.object({ type: z9.literal("image"), request: z9.string().min(1), read: TurnImageReadSchema }),
1691
1808
  /** Answered with whether the CLI accepted it; that it took it comes as a `steered` event. */
1692
- z8.object({
1693
- type: z8.literal("steer"),
1694
- request: z8.string().min(1),
1809
+ z9.object({
1810
+ type: z9.literal("steer"),
1811
+ request: z9.string().min(1),
1695
1812
  turnId: UlidSchema,
1696
1813
  steer: TurnSteerSchema
1697
1814
  }),
1698
1815
  /** Answered with whether the turn was there to stop; it ends `stopped`. */
1699
- z8.object({ type: z8.literal("stop"), request: z8.string().min(1), turnId: UlidSchema }),
1816
+ z9.object({ type: z9.literal("stop"), request: z9.string().min(1), turnId: UlidSchema }),
1700
1817
  /**
1701
1818
  * A conversation ended: the runner removes every citizen's workspace for it but those a turn
1702
1819
  * still uses, and answers with a `SweepResult` naming those it kept for holding work on no branch.
1703
1820
  */
1704
- z8.object({
1705
- type: z8.literal("sweep"),
1706
- request: z8.string().min(1),
1821
+ z9.object({
1822
+ type: z9.literal("sweep"),
1823
+ request: z9.string().min(1),
1707
1824
  conversation: WorkspaceConversationSchema
1708
1825
  })
1709
1826
  ]);
1710
- var RunnerAnswerSchema = z8.discriminatedUnion("ok", [
1711
- z8.object({ ok: z8.literal(true), value: z8.unknown() }),
1712
- z8.object({ ok: z8.literal(false), error: z8.string() })
1827
+ var RunnerAnswerSchema = z9.discriminatedUnion("ok", [
1828
+ z9.object({ ok: z9.literal(true), value: z9.unknown() }),
1829
+ z9.object({ ok: z9.literal(false), error: z9.string() })
1713
1830
  ]);
1714
- var WarmSessionsSchema = z8.object({ warm: z8.array(z8.string()) });
1715
- var FileManifestSchema = z8.object({ files: z8.record(z8.string(), z8.string()) });
1831
+ var WarmSessionsSchema = z9.object({ warm: z9.array(z9.string()) });
1832
+ var FileManifestSchema = z9.object({ files: z9.record(z9.string(), z9.string()) });
1716
1833
  function fileVersion(sha, executable) {
1717
1834
  return executable ? `${sha}+x` : sha;
1718
1835
  }
1719
- var FileReadSchema = z8.object({ paths: z8.array(z8.string()) });
1720
- var FileContentsSchema = z8.object({ files: z8.record(z8.string(), z8.string()) });
1836
+ var FileReadSchema = z9.object({ paths: z9.array(z9.string()) });
1837
+ var FileContentsSchema = z9.object({ files: z9.record(z9.string(), z9.string()) });
1721
1838
  function isSafeRelativePath(file) {
1722
1839
  if (file.length === 0 || file.startsWith("/") || file.includes("\\") || file.includes("\0")) {
1723
1840
  return false;
@@ -1726,7 +1843,7 @@ function isSafeRelativePath(file) {
1726
1843
  }
1727
1844
 
1728
1845
  // packages/shared/dist/turns.js
1729
- var TriggerKindSchema = z9.enum([
1846
+ var TriggerKindSchema = z10.enum([
1730
1847
  "mention",
1731
1848
  "stage",
1732
1849
  "task_done",
@@ -1740,11 +1857,11 @@ var TriggerKindSchema = z9.enum([
1740
1857
  "closing",
1741
1858
  "cron"
1742
1859
  ]);
1743
- var TriggerSchema = z9.object({
1860
+ var TriggerSchema = z10.object({
1744
1861
  kind: TriggerKindSchema,
1745
1862
  from: NameSchema.optional(),
1746
- fromUser: z9.boolean().default(false),
1747
- reason: z9.string().default(""),
1863
+ fromUser: z10.boolean().default(false),
1864
+ reason: z10.string().default(""),
1748
1865
  taskId: UlidSchema.optional(),
1749
1866
  messageId: UlidSchema.optional(),
1750
1867
  /** The channel the waking message was posted in. */
@@ -1752,20 +1869,27 @@ var TriggerSchema = z9.object({
1752
1869
  /** The cron that came due, for a `cron` wake. */
1753
1870
  cronId: UlidSchema.optional()
1754
1871
  });
1755
- var TurnThreadSchema = z9.object({ id: UlidSchema, task: z9.boolean() });
1756
- var TurnDispatchSchema = z9.object({
1872
+ var TurnThreadSchema = z10.object({ id: UlidSchema, task: z10.boolean() });
1873
+ var TurnDispatchSchema = z10.object({
1757
1874
  agent: NameSchema,
1758
1875
  project: NameSchema,
1759
1876
  thread: TurnThreadSchema.optional(),
1760
1877
  channel: NameSchema.optional(),
1878
+ place: NameSchema.optional(),
1761
1879
  trigger: TriggerSchema,
1762
- priority: z9.number().int().min(0).max(2),
1763
- onboarding: z9.boolean().default(false),
1880
+ priority: z10.number().int().min(0).max(2),
1881
+ onboarding: z10.boolean().default(false),
1764
1882
  /** Every cron that came due for this turn, whatever wake it merged into; its prompt quotes each. */
1765
- crons: z9.array(UlidSchema).optional()
1883
+ crons: z10.array(UlidSchema).optional()
1766
1884
  });
1767
1885
  function conversationPart(conversation) {
1768
- 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}`;
1769
1893
  }
1770
1894
  function sessionKey(agent, scope, part) {
1771
1895
  return part === void 0 ? `${agent}/${scope}` : `${agent}/${scope}/${part}`;
@@ -1778,7 +1902,10 @@ function parseSessionKey(key) {
1778
1902
  if (part === void 0) {
1779
1903
  return { agent, scope };
1780
1904
  }
1781
- 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 };
1782
1909
  }
1783
1910
  function channelConversation(scope, channel) {
1784
1911
  const at = channel.indexOf("/");
@@ -1786,7 +1913,11 @@ function channelConversation(scope, channel) {
1786
1913
  const name = at < 0 ? channel : channel.slice(at + 1);
1787
1914
  return place === scope && name !== "general" ? name : void 0;
1788
1915
  }
1789
- var TurnRecordSchema = z9.object({
1916
+ function placeConversation(scope, channel) {
1917
+ const at = channel.indexOf("/");
1918
+ return scope === SOCIETY_SCOPE && at >= 0 ? channel.slice(0, at) : void 0;
1919
+ }
1920
+ var TurnRecordSchema = z10.object({
1790
1921
  /** Assigned by the board when the turn begins; the turn's transcript is filed under it. */
1791
1922
  id: UlidSchema.optional(),
1792
1923
  agent: NameSchema,
@@ -1795,83 +1926,94 @@ var TurnRecordSchema = z9.object({
1795
1926
  thread: UlidSchema.optional(),
1796
1927
  /** The channel, never general, whose conversation the turn was in. */
1797
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(),
1798
1931
  runner: NameSchema,
1799
1932
  cli: CliKindSchema.nullable(),
1800
- session: z9.string().nullable(),
1933
+ session: z10.string().nullable(),
1801
1934
  trigger: TriggerSchema,
1802
1935
  startedAt: IsoDateTimeSchema,
1803
1936
  endedAt: IsoDateTimeSchema.nullable(),
1804
1937
  exitReason: TurnExitReasonSchema.nullable(),
1805
1938
  status: TurnStatusSchema.nullable(),
1806
- error: z9.string().nullable(),
1939
+ error: z10.string().nullable(),
1807
1940
  usage: UsageSchema.nullable(),
1808
- costUsd: z9.number().nonnegative().default(0),
1941
+ costUsd: z10.number().nonnegative().default(0),
1809
1942
  /**
1810
1943
  * The running total the CLI reported for the session after this turn, when it reports one. A
1811
1944
  * resumed session starts from it, so the next turn's own cost is measured from here.
1812
1945
  */
1813
- sessionCostUsd: z9.number().nonnegative().optional(),
1814
- toolCalls: z9.number().int().nonnegative().default(0),
1946
+ sessionCostUsd: z10.number().nonnegative().optional(),
1947
+ toolCalls: z10.number().int().nonnegative().default(0),
1815
1948
  /** The model the CLI reported for this turn, else the model configured for the agent, else null. */
1816
- model: z9.string().nullable().default(null),
1949
+ model: z10.string().nullable().default(null),
1817
1950
  /** The task branch the turn left work on, with its commit, as the runner reported it. */
1818
1951
  work: TurnWorkSchema.optional(),
1819
1952
  /** Who stopped the turn, for one that ended `stopped`. */
1820
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(),
1821
1960
  /** The crons whose fire started the turn, which record how it ended. */
1822
- crons: z9.array(UlidSchema).optional()
1961
+ crons: z10.array(UlidSchema).optional()
1823
1962
  });
1824
- var RunningTurnSchema = z9.object({
1963
+ var RunningTurnSchema = z10.object({
1825
1964
  turnId: UlidSchema,
1826
1965
  agent: NameSchema,
1827
1966
  scope: NameSchema,
1828
1967
  thread: UlidSchema.optional(),
1829
1968
  channel: NameSchema.optional(),
1969
+ place: NameSchema.optional(),
1830
1970
  cli: CliKindSchema,
1831
1971
  askedIn: ChannelRefSchema.optional(),
1832
- steerable: z9.boolean(),
1833
- stoppable: z9.boolean()
1972
+ steerable: z10.boolean(),
1973
+ stoppable: z10.boolean()
1834
1974
  });
1835
- var SessionsFileSchema = z9.object({
1836
- claude: z9.string().optional(),
1837
- codex: z9.string().optional(),
1975
+ var SessionsFileSchema = z10.object({
1976
+ claude: z10.string().optional(),
1977
+ codex: z10.string().optional(),
1838
1978
  runner: NameSchema.optional()
1839
1979
  });
1840
- var WakeRequestSchema = z9.object({
1980
+ var WakeRequestSchema = z10.object({
1841
1981
  agent: NameSchema,
1842
1982
  project: NameSchema,
1843
- reason: z9.string().default("manual wake"),
1983
+ reason: z10.string().default("manual wake"),
1844
1984
  /** A manual wake, or a reflection turn requested ahead of the cadence. */
1845
- kind: z9.enum(["manual", "reflection"]).default("manual")
1985
+ kind: z10.enum(["manual", "reflection"]).default("manual")
1846
1986
  });
1847
- var TurnHistoryEntrySchema = z9.object({
1987
+ var TurnHistoryEntrySchema = z10.object({
1848
1988
  /** The id of the event that ended the turn. */
1849
1989
  id: UlidSchema,
1850
1990
  /** The turn's own id, when it has a transcript to open; turns before transcripts have none. */
1851
1991
  turnId: UlidSchema.optional(),
1852
1992
  ts: IsoDateTimeSchema,
1853
- outcome: z9.enum(["completed", "failed"]),
1993
+ outcome: z10.enum(["completed", "failed"]),
1854
1994
  /** A project slug, or the society scope. */
1855
1995
  project: NameSchema,
1856
1996
  /** The thread whose conversation the turn was in; absent for the home conversation. */
1857
1997
  thread: UlidSchema.optional(),
1858
1998
  /** The channel, never general, whose conversation the turn was in. */
1859
1999
  channel: NameSchema.optional(),
1860
- trigger: z9.string(),
1861
- exitReason: z9.string().nullable(),
1862
- costUsd: z9.number(),
2000
+ /** The project whose channels' conversation the turn was in, for a turn in the society scope. */
2001
+ place: NameSchema.optional(),
2002
+ trigger: z10.string(),
2003
+ exitReason: z10.string().nullable(),
2004
+ costUsd: z10.number(),
1863
2005
  /** The tokens the turn used; absent for turns logged before turns recorded them. */
1864
2006
  usage: UsageSchema.optional(),
1865
- model: z9.string().nullable(),
1866
- summary: z9.string().nullable(),
1867
- error: z9.string().nullable(),
2007
+ model: z10.string().nullable(),
2008
+ summary: z10.string().nullable(),
2009
+ error: z10.string().nullable(),
1868
2010
  /** When the log recorded the turn's start; absent when the start is missing from the log. */
1869
2011
  startedAt: IsoDateTimeSchema.optional(),
1870
- toolCalls: z9.number().int().nonnegative().optional()
2012
+ toolCalls: z10.number().int().nonnegative().optional()
1871
2013
  });
1872
2014
 
1873
2015
  // packages/shared/dist/ops.js
1874
- import { z as z10 } from "zod";
2016
+ import { z as z11 } from "zod";
1875
2017
  var OPS_SIGNAL_KINDS = [
1876
2018
  "waiting_stage",
1877
2019
  "backlog",
@@ -1887,7 +2029,7 @@ var OPS_SIGNAL_KINDS = [
1887
2029
  "stuck_workspace",
1888
2030
  "cron_failing"
1889
2031
  ];
1890
- var OpsSignalKindSchema = z10.enum(OPS_SIGNAL_KINDS);
2032
+ var OpsSignalKindSchema = z11.enum(OPS_SIGNAL_KINDS);
1891
2033
  var WAKING_SIGNAL_KINDS = [
1892
2034
  "backlog",
1893
2035
  "role_gap",
@@ -1897,15 +2039,15 @@ var WAKING_SIGNAL_KINDS = [
1897
2039
  "blocked_capability",
1898
2040
  "scaled"
1899
2041
  ];
1900
- var OpsSignalSchema = z10.object({
2042
+ var OpsSignalSchema = z11.object({
1901
2043
  kind: OpsSignalKindSchema,
1902
2044
  /** Identifies the condition, for example `backlog:demo:engineer`, so a persisting one is not logged again every pass. */
1903
- key: z10.string().min(1),
2045
+ key: z11.string().min(1),
1904
2046
  /** One line a reader can act on. */
1905
- summary: z10.string().min(1),
2047
+ summary: z11.string().min(1),
1906
2048
  /** The counter or timer value behind the signal. */
1907
- value: z10.number(),
1908
- threshold: z10.number().optional(),
2049
+ value: z11.number(),
2050
+ threshold: z11.number().optional(),
1909
2051
  project: NameSchema.optional(),
1910
2052
  agent: NameSchema.optional(),
1911
2053
  role: NameSchema.optional(),
@@ -1913,8 +2055,8 @@ var OpsSignalSchema = z10.object({
1913
2055
  });
1914
2056
 
1915
2057
  // packages/shared/dist/metrics.js
1916
- import { z as z11 } from "zod";
1917
- var MetricsWindowSchema = z11.enum(["24h", "7d", "all"]);
2058
+ import { z as z12 } from "zod";
2059
+ var MetricsWindowSchema = z12.enum(["24h", "7d", "all"]);
1918
2060
  var METRICS_WINDOW_MS = {
1919
2061
  "24h": 24 * 36e5,
1920
2062
  "7d": 7 * 24 * 36e5,
@@ -1925,9 +2067,9 @@ function isBoardAction(tool) {
1925
2067
  const verb = /^mcp__board__(.+)$/.exec(tool)?.[1];
1926
2068
  return verb !== void 0 && !READ_VERBS.some((read2) => read2 === verb);
1927
2069
  }
1928
- var CountSchema = z11.number().int().nonnegative();
1929
- var MsSchema = z11.number().nonnegative();
1930
- var MetricsSchema = z11.object({
2070
+ var CountSchema = z12.number().int().nonnegative();
2071
+ var MsSchema = z12.number().nonnegative();
2072
+ var MetricsSchema = z12.object({
1931
2073
  window: MetricsWindowSchema,
1932
2074
  since: IsoDateTimeSchema.nullable(),
1933
2075
  until: IsoDateTimeSchema,
@@ -1935,12 +2077,12 @@ var MetricsSchema = z11.object({
1935
2077
  * Finished turns that changed nothing on the board. Reflections are left out, since they write
1936
2078
  * memory, not the board; turns without a transcript are counted apart, their actions unknown.
1937
2079
  */
1938
- idle: z11.object({
2080
+ idle: z12.object({
1939
2081
  turns: CountSchema,
1940
2082
  idle: CountSchema,
1941
2083
  unknown: CountSchema,
1942
- byTrigger: z11.array(z11.object({ trigger: z11.string(), turns: CountSchema, idle: CountSchema })),
1943
- byAgent: z11.array(z11.object({
2084
+ byTrigger: z12.array(z12.object({ trigger: z12.string(), turns: CountSchema, idle: CountSchema })),
2085
+ byAgent: z12.array(z12.object({
1944
2086
  agent: NameSchema,
1945
2087
  role: NameSchema.nullable(),
1946
2088
  turns: CountSchema,
@@ -1948,33 +2090,33 @@ var MetricsSchema = z11.object({
1948
2090
  }))
1949
2091
  }),
1950
2092
  /** Work sent back to an earlier stage: over the tasks finished in the window, and by where. */
1951
- sentBack: z11.object({
2093
+ sentBack: z12.object({
1952
2094
  finished: CountSchema,
1953
2095
  sendBacks: CountSchema,
1954
2096
  tasksSentBack: CountSchema,
1955
- byStage: z11.array(z11.object({ project: NameSchema, stage: z11.string(), count: CountSchema })),
1956
- bySender: z11.array(z11.object({ agent: NameSchema, count: CountSchema }))
2097
+ byStage: z12.array(z12.object({ project: NameSchema, stage: z12.string(), count: CountSchema })),
2098
+ bySender: z12.array(z12.object({ agent: NameSchema, count: CountSchema }))
1957
2099
  }),
1958
2100
  /** Posts in the threads of the tasks finished in the window, the board's own notices left out. */
1959
- messages: z11.object({
2101
+ messages: z12.object({
1960
2102
  finished: CountSchema,
1961
2103
  messages: CountSchema,
1962
- byProject: z11.array(z11.object({ project: NameSchema, finished: CountSchema, messages: CountSchema })),
1963
- busiest: z11.array(z11.object({
2104
+ byProject: z12.array(z12.object({ project: NameSchema, finished: CountSchema, messages: CountSchema })),
2105
+ busiest: z12.array(z12.object({
1964
2106
  taskId: UlidSchema,
1965
- title: z11.string(),
2107
+ title: z12.string(),
1966
2108
  project: NameSchema,
1967
2109
  messages: CountSchema
1968
2110
  }))
1969
2111
  }),
1970
2112
  /** Time from a mention to the start of the turn it woke, in the conversation it was made in. */
1971
- latency: z11.object({
2113
+ latency: z12.object({
1972
2114
  mentions: CountSchema,
1973
2115
  /** Mentions no turn has followed yet. */
1974
2116
  unanswered: CountSchema,
1975
2117
  medianMs: MsSchema.nullable(),
1976
2118
  slowestMs: MsSchema.nullable(),
1977
- byAgent: z11.array(z11.object({
2119
+ byAgent: z12.array(z12.object({
1978
2120
  agent: NameSchema,
1979
2121
  mentions: CountSchema,
1980
2122
  medianMs: MsSchema,
@@ -1982,73 +2124,73 @@ var MetricsSchema = z11.object({
1982
2124
  }))
1983
2125
  }),
1984
2126
  /** The user's decisions: proposals decided, questions answered, and stages passed or sent back. */
1985
- decisions: z11.object({
2127
+ decisions: z12.object({
1986
2128
  total: CountSchema,
1987
- byDay: z11.array(z11.object({
1988
- day: z11.string(),
2129
+ byDay: z12.array(z12.object({
2130
+ day: z12.string(),
1989
2131
  proposals: CountSchema,
1990
2132
  answers: CountSchema,
1991
2133
  stages: CountSchema
1992
2134
  }))
1993
2135
  }),
1994
2136
  /** Tasks that needed a capability no connected runner offered, and whether they still do. */
1995
- blocked: z11.array(z11.object({
2137
+ blocked: z12.array(z12.object({
1996
2138
  taskId: UlidSchema,
1997
- title: z11.string().nullable(),
2139
+ title: z12.string().nullable(),
1998
2140
  project: NameSchema.nullable(),
1999
- summary: z11.string(),
2141
+ summary: z12.string(),
2000
2142
  firstAt: IsoDateTimeSchema,
2001
2143
  lastAt: IsoDateTimeSchema,
2002
- holds: z11.boolean()
2144
+ holds: z12.boolean()
2003
2145
  }))
2004
2146
  });
2005
- var CitizenTokensSchema = z11.object({
2147
+ var CitizenTokensSchema = z12.object({
2006
2148
  agent: NameSchema,
2007
2149
  turns: CountSchema,
2008
2150
  withUsage: CountSchema,
2009
2151
  usage: UsageSchema.nullable()
2010
2152
  });
2011
- var TokenUseSchema = z11.object({
2153
+ var TokenUseSchema = z12.object({
2012
2154
  window: MetricsWindowSchema,
2013
2155
  since: IsoDateTimeSchema.nullable(),
2014
2156
  until: IsoDateTimeSchema,
2015
- citizens: z11.array(CitizenTokensSchema)
2157
+ citizens: z12.array(CitizenTokensSchema)
2016
2158
  });
2017
2159
 
2018
2160
  // packages/shared/dist/config.js
2019
- import { z as z12 } from "zod";
2020
- var LogLevelSchema = z12.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
2021
- var GithubSignInSchema = z12.object({
2022
- clientId: z12.string().min(1),
2023
- clientSecret: z12.string().min(1),
2161
+ import { z as z13 } from "zod";
2162
+ var LogLevelSchema = z13.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
2163
+ var GithubSignInSchema = z13.object({
2164
+ clientId: z13.string().min(1),
2165
+ clientSecret: z13.string().min(1),
2024
2166
  /** Logins, lowercased; GitHub logins are case-insensitive. */
2025
- users: z12.array(z12.string().min(1)).min(1)
2167
+ users: z13.array(z13.string().min(1)).min(1)
2026
2168
  });
2027
- var ServerConfigSchema = z12.object({
2028
- dataDir: z12.string().min(1),
2029
- host: z12.string().min(1),
2030
- port: z12.number().int().positive().max(65535),
2169
+ var ServerConfigSchema = z13.object({
2170
+ dataDir: z13.string().min(1),
2171
+ host: z13.string().min(1),
2172
+ port: z13.number().int().positive().max(65535),
2031
2173
  /**
2032
2174
  * An address every runner's agents reach this server at, for example https://board.example, or
2033
2175
  * null for each runner to use the address it reaches the server at itself.
2034
2176
  */
2035
- publicUrl: z12.string().min(1).nullable(),
2177
+ publicUrl: z13.string().min(1).nullable(),
2036
2178
  logLevel: LogLevelSchema,
2037
2179
  /** Turns running at once across every runner, or null for no limit beyond each runner's own. */
2038
- concurrency: z12.number().int().positive().nullable(),
2180
+ concurrency: z13.number().int().positive().nullable(),
2039
2181
  /** How long one turn may run before it is stopped, or null for no limit. */
2040
- turnTimeoutMs: z12.number().int().positive().nullable(),
2182
+ turnTimeoutMs: z13.number().int().positive().nullable(),
2041
2183
  /** Rounds of tool calls one Claude turn may take, or null for no limit. */
2042
- toolRounds: z12.number().int().positive().nullable(),
2184
+ toolRounds: z13.number().int().positive().nullable(),
2043
2185
  /** How long a resident session stays warm after its last turn. */
2044
- residentIdleMs: z12.number().int().positive(),
2186
+ residentIdleMs: z13.number().int().positive(),
2045
2187
  /** Signing in to the board with GitHub, or null when only a token lets anyone in. */
2046
2188
  github: GithubSignInSchema.nullable(),
2047
2189
  /**
2048
2190
  * Reverse proxies in front of the server whose X-Forwarded-For entries name the client, for the
2049
2191
  * rate limits on what anyone may ask without a token: 0 when clients connect directly.
2050
2192
  */
2051
- trustedProxies: z12.number().int().min(0).max(16)
2193
+ trustedProxies: z13.number().int().min(0).max(16)
2052
2194
  });
2053
2195
  function limit(value, fallback) {
2054
2196
  if (value === void 0 || value.trim() === "") {
@@ -2086,25 +2228,25 @@ function loadServerConfig(env) {
2086
2228
  trustedProxies: Number(env["STELLARIS_TRUSTED_PROXIES"] ?? "0")
2087
2229
  });
2088
2230
  }
2089
- var RunnerConfigSchema = z12.object({
2231
+ var RunnerConfigSchema = z13.object({
2090
2232
  /** The board server, for example http://127.0.0.1:4700. */
2091
- serverUrl: z12.string().url(),
2233
+ serverUrl: z13.string().url(),
2092
2234
  /**
2093
2235
  * The runner's token, from `runner add`, or null for the one it saved when it was enrolled, or
2094
2236
  * to enroll it.
2095
2237
  */
2096
- token: z12.string().min(1).nullable(),
2238
+ token: z13.string().min(1).nullable(),
2097
2239
  /** The runner's own data directory: the board mirror, agent homes, repositories, worktrees. */
2098
- dataDir: z12.string().min(1),
2240
+ dataDir: z13.string().min(1),
2099
2241
  logLevel: LogLevelSchema,
2100
2242
  /** Turns this machine runs at once, or null for no limit. */
2101
- slots: z12.number().int().positive().nullable(),
2102
- clis: z12.array(CliKindSchema).min(1),
2103
- capabilities: z12.array(z12.string())
2243
+ slots: z13.number().int().positive().nullable(),
2244
+ clis: z13.array(CliKindSchema).min(1),
2245
+ capabilities: z13.array(z13.string())
2104
2246
  });
2105
2247
 
2106
2248
  // packages/shared/dist/version.js
2107
- var STELLARIS_VERSION = "0.3.22";
2249
+ var STELLARIS_VERSION = "0.3.24";
2108
2250
 
2109
2251
  // packages/shared/dist/mentions.js
2110
2252
  import { parse, postprocess, preprocess } from "micromark";
@@ -2488,6 +2630,7 @@ var HOME_GITIGNORE = [
2488
2630
  "/projects/*/last-turn.json",
2489
2631
  "/projects/*/threads/",
2490
2632
  "/projects/*/channels/",
2633
+ "/projects/*/places/",
2491
2634
  `/${HOME_SCRATCH}/`,
2492
2635
  "*.tmp",
2493
2636
  "node_modules/",
@@ -2505,7 +2648,7 @@ var HOME_GITIGNORE = [
2505
2648
  var IGNORE_FILE = `${HOME_GITIGNORE.join("\n")}
2506
2649
  `;
2507
2650
  var PRE_RECEIVE = `#!/bin/sh
2508
- 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/))'
2509
2652
  while read old new ref; do
2510
2653
  if [ "$ref" != "refs/heads/main" ]; then
2511
2654
  echo "a home takes pushes to main only" >&2
@@ -2825,32 +2968,33 @@ function postMentions(body) {
2825
2968
  }
2826
2969
 
2827
2970
  // packages/board-core/dist/metrics.js
2828
- import { z as z13 } from "zod";
2829
- var TurnPayload = z13.object({
2830
- turnId: z13.string().optional(),
2831
- project: z13.string(),
2832
- thread: z13.string().optional(),
2833
- channel: z13.string().optional(),
2834
- trigger: z13.string()
2971
+ import { z as z14 } from "zod";
2972
+ var TurnPayload = z14.object({
2973
+ turnId: z14.string().optional(),
2974
+ project: z14.string(),
2975
+ thread: z14.string().optional(),
2976
+ channel: z14.string().optional(),
2977
+ place: z14.string().optional(),
2978
+ trigger: z14.string()
2835
2979
  });
2836
- var TaskPayload = z13.object({
2837
- taskId: z13.string(),
2838
- project: z13.string(),
2839
- from: z13.string().optional()
2980
+ var TaskPayload = z14.object({
2981
+ taskId: z14.string(),
2982
+ project: z14.string(),
2983
+ from: z14.string().optional()
2840
2984
  });
2841
- var MessagePayload = z13.object({
2842
- id: z13.string().optional(),
2843
- channel: z13.string(),
2844
- thread: z13.string().nullable().optional(),
2845
- mentions: z13.array(z13.string()).default([])
2985
+ var MessagePayload = z14.object({
2986
+ id: z14.string().optional(),
2987
+ channel: z14.string(),
2988
+ thread: z14.string().nullable().optional(),
2989
+ mentions: z14.array(z14.string()).default([])
2846
2990
  });
2847
- var SteeredPayload = z13.object({ messages: z13.array(z13.string()) });
2848
- var SignalPayload = z13.object({
2849
- kind: z13.string(),
2850
- key: z13.string(),
2851
- summary: z13.string(),
2852
- taskId: z13.string().optional(),
2853
- project: z13.string().optional()
2991
+ var SteeredPayload = z14.object({ messages: z14.array(z14.string()) });
2992
+ var SignalPayload = z14.object({
2993
+ kind: z14.string(),
2994
+ key: z14.string(),
2995
+ summary: z14.string(),
2996
+ taskId: z14.string().optional(),
2997
+ project: z14.string().optional()
2854
2998
  });
2855
2999
  function median(values) {
2856
3000
  const sorted = values.toSorted((a, b) => a - b);
@@ -2876,11 +3020,16 @@ function answers(mention, turn) {
2876
3020
  if (!mention.scopes.includes(turn.project)) {
2877
3021
  return false;
2878
3022
  }
2879
- 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;
2880
3026
  if (turn.channel !== null) {
2881
- return mention.thread === null && own === turn.channel;
3027
+ return topLevel && own === turn.channel;
3028
+ }
3029
+ if (turn.place !== null) {
3030
+ return topLevel && place === turn.place;
2882
3031
  }
2883
- return mention.thread === null && own === void 0 || turn.trigger === "mention";
3032
+ return topLevel && own === void 0 && place === void 0 || turn.trigger === "mention";
2884
3033
  }
2885
3034
  function computeMetrics(input) {
2886
3035
  const { events, now, window } = input;
@@ -2946,6 +3095,7 @@ function computeMetrics(input) {
2946
3095
  at,
2947
3096
  thread: turn.data.thread ?? null,
2948
3097
  channel: turn.data.channel ?? null,
3098
+ place: turn.data.place ?? null,
2949
3099
  project: turn.data.project,
2950
3100
  trigger: turn.data.trigger
2951
3101
  };
@@ -3140,19 +3290,20 @@ function computeMetrics(input) {
3140
3290
  }
3141
3291
 
3142
3292
  // packages/board-core/dist/turn-history.js
3143
- import { z as z14 } from "zod";
3144
- var TurnHistoryPayloadSchema = z14.object({
3293
+ import { z as z15 } from "zod";
3294
+ var TurnHistoryPayloadSchema = z15.object({
3145
3295
  project: NameSchema,
3146
3296
  thread: UlidSchema.optional(),
3147
3297
  channel: NameSchema.optional(),
3148
- trigger: z14.string().default("manual"),
3149
- exitReason: z14.string().nullable().default(null),
3150
- costUsd: z14.number().default(0),
3298
+ place: NameSchema.optional(),
3299
+ trigger: z15.string().default("manual"),
3300
+ exitReason: z15.string().nullable().default(null),
3301
+ costUsd: z15.number().default(0),
3151
3302
  usage: UsageSchema.optional(),
3152
- model: z14.string().nullable().default(null),
3153
- summary: z14.string().nullable().default(null),
3154
- error: z14.string().nullable().default(null),
3155
- toolCalls: z14.number().int().nonnegative().optional(),
3303
+ model: z15.string().nullable().default(null),
3304
+ summary: z15.string().nullable().default(null),
3305
+ error: z15.string().nullable().default(null),
3306
+ toolCalls: z15.number().int().nonnegative().optional(),
3156
3307
  turnId: UlidSchema.optional()
3157
3308
  });
3158
3309
  function tokenUse(events, window, now) {
@@ -3383,11 +3534,14 @@ var BoardPaths = class {
3383
3534
  return path5.join(this.agentProjects(name), slug);
3384
3535
  }
3385
3536
  /**
3386
- * A thread's or a channel's conversation's session and last turn, beside the scope's home
3387
- * 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`.
3388
3539
  */
3389
3540
  agentConversation(name, slug, part) {
3390
- 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);
3391
3545
  }
3392
3546
  agentCursors(name) {
3393
3547
  return path5.join(this.agent(name), "cursors.json");
@@ -3398,6 +3552,13 @@ var BoardPaths = class {
3398
3552
  eventLog() {
3399
3553
  return path5.join(this.events(), "log.jsonl");
3400
3554
  }
3555
+ /** Images citizens uploaded, by id: outside the projection, which every runner mirrors. */
3556
+ images() {
3557
+ return path5.join(this.dataDir, "images");
3558
+ }
3559
+ image(id) {
3560
+ return path5.join(this.images(), id);
3561
+ }
3401
3562
  state() {
3402
3563
  return path5.join(this.dataDir, "state");
3403
3564
  }
@@ -3414,6 +3575,101 @@ var BoardPaths = class {
3414
3575
  }
3415
3576
  };
3416
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
+
3417
3673
  // packages/board-core/dist/tokens.js
3418
3674
  import { createHash as createHash2, randomBytes } from "node:crypto";
3419
3675
  function mintToken() {
@@ -3427,48 +3683,62 @@ function hashToken(token) {
3427
3683
  var SYSTEM_ACTOR = { name: "board", role: USER_ROLE };
3428
3684
  var CLOSING_ATTEMPTS = 3;
3429
3685
  function ownTurn(actor, turn) {
3430
- 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;
3431
3687
  }
3432
3688
  function namesList(names) {
3433
3689
  return names.length <= 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}`;
3434
3690
  }
3435
3691
  var SKIPPED_IN_HOME = /* @__PURE__ */ new Set([".git", "turns", ".claude", ".codex"]);
3436
- var RunnerTokensSchema = z15.object({
3437
- runners: z15.record(z15.string(), z15.object({ tokenHash: z15.string().min(1) }))
3692
+ var RunnerTokensSchema = z16.object({
3693
+ runners: z16.record(z16.string(), z16.object({ tokenHash: z16.string().min(1) }))
3438
3694
  });
3439
- var SignInsSchema = z15.object({
3440
- signIns: z15.array(z15.object({
3441
- tokenHash: z15.string().min(1),
3442
- login: z15.string().min(1),
3695
+ var SignInsSchema = z16.object({
3696
+ signIns: z16.array(z16.object({
3697
+ tokenHash: z16.string().min(1),
3698
+ login: z16.string().min(1),
3443
3699
  /** The account's picture at the identity provider; sign-ins from before it have none. */
3444
- avatarUrl: z15.url().optional(),
3700
+ avatarUrl: z16.url().optional(),
3445
3701
  createdAt: IsoDateTimeSchema,
3446
3702
  expiresAt: IsoDateTimeSchema
3447
3703
  })).default([])
3448
3704
  });
3449
- var TurnStartPayloadSchema = z15.object({ project: NameSchema, turnId: UlidSchema.optional() });
3450
- var CursorsSchema = z15.object({
3451
- digest: z15.string().nullable(),
3452
- scopes: z15.record(z15.string(), z15.string()).default({}),
3453
- threadsFrom: z15.record(z15.string(), z15.string().nullable()).default({}),
3454
- channelsFrom: z15.record(z15.string(), z15.string().nullable()).default({}),
3705
+ var TurnStartPayloadSchema = z16.object({ project: NameSchema, turnId: UlidSchema.optional() });
3706
+ var CursorsSchema = z16.object({
3707
+ digest: z16.string().nullable(),
3708
+ scopes: z16.record(z16.string(), z16.string()).default({}),
3709
+ threadsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3710
+ channelsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3711
+ placesFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3455
3712
  /** How far a work role has read news, which goes to whichever of its general conversations reads first. */
3456
- news: z15.string().optional()
3713
+ news: z16.string().optional()
3457
3714
  });
3458
- var NO_CURSORS = { digest: null, scopes: {}, threadsFrom: {}, channelsFrom: {} };
3715
+ var NO_CURSORS = {
3716
+ digest: null,
3717
+ scopes: {},
3718
+ threadsFrom: {},
3719
+ channelsFrom: {},
3720
+ placesFrom: {}
3721
+ };
3459
3722
  function conversationKey(scope, part) {
3460
3723
  return part === null ? scope : `${scope}/${part}`;
3461
3724
  }
3462
3725
  function channelOfPart(part) {
3463
3726
  return part?.startsWith("#") === true ? part.slice(1) : void 0;
3464
3727
  }
3728
+ function placeOfPart(part) {
3729
+ return part?.startsWith("~") === true ? part.slice(1) : void 0;
3730
+ }
3465
3731
  function threadOfPart(part) {
3466
- 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;
3467
3733
  }
3468
3734
  function channelStart(cursors, scope) {
3469
3735
  const from = cursors.channelsFrom[scope];
3470
3736
  return from === void 0 ? cursors.digest : from;
3471
3737
  }
3738
+ function placeStart(cursors, scope) {
3739
+ const from = cursors.placesFrom[scope];
3740
+ return from === void 0 ? cursors.digest : from;
3741
+ }
3472
3742
  function threadStart(cursors, scope) {
3473
3743
  const from = cursors.threadsFrom[scope];
3474
3744
  return from === void 0 ? cursors.digest : from;
@@ -3484,7 +3754,7 @@ function cursorOf(cursors, scope, part = null) {
3484
3754
  if (part === null) {
3485
3755
  return cursors.scopes[scope] ?? cursors.digest;
3486
3756
  }
3487
- 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));
3488
3758
  }
3489
3759
  function byId(a, b) {
3490
3760
  return a.message.id < b.message.id ? -1 : a.message.id > b.message.id ? 1 : 0;
@@ -3492,12 +3762,14 @@ function byId(a, b) {
3492
3762
  function turnTime(record) {
3493
3763
  return record.endedAt ?? record.startedAt;
3494
3764
  }
3495
- var PausedSchema = z15.object({ paused: z15.boolean() });
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
+ }
3768
+ var PausedSchema = z16.object({ paused: z16.boolean() });
3496
3769
  var DEFAULT_LEASE_MS = 30 * 60 * 1e3;
3497
3770
  function withoutMentions(text) {
3498
3771
  return text.replaceAll(MENTION_PATTERN, "$1$2");
3499
3772
  }
3500
- var SKILL_TEXT_LIMIT_CHARS = 1e5;
3501
3773
  function skillFolderOf(proposal) {
3502
3774
  if (proposal.kind !== "skill") {
3503
3775
  return null;
@@ -3506,7 +3778,7 @@ function skillFolderOf(proposal) {
3506
3778
  return parsed.success && parsed.data.body === void 0 ? parsed.data.name : null;
3507
3779
  }
3508
3780
  function proposedFrom(origin, where) {
3509
- 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;
3510
3782
  }
3511
3783
  function megabytes(bytes) {
3512
3784
  return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "");
@@ -3517,7 +3789,7 @@ var OPS_WAKE_TRIGGER = "ops_event";
3517
3789
  var FRONT_DESK_TRIGGER = "user_post";
3518
3790
  var OPS_CHANNELS = ["governance"];
3519
3791
  var FRONT_DESK_CHANNELS = [ASK_CHANNEL];
3520
- var AlignmentsSchema = z15.object({ applied: z15.array(z15.string()).default([]) });
3792
+ var AlignmentsSchema = z16.object({ applied: z16.array(z16.string()).default([]) });
3521
3793
  var FOLLOW_DECISIONS = "ops-readers-follow-decisions";
3522
3794
  var THREAD_RECORDS = "threads-as-records";
3523
3795
  var DIGEST_CURSORS = "digest-replaces-inbox";
@@ -3528,13 +3800,15 @@ var THREAD_CONVERSATIONS = "thread-conversations";
3528
3800
  var ASKS_CHANNEL_OPENED = "asks-channel-opened";
3529
3801
  var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
3530
3802
  var CHANNEL_CONVERSATIONS = "channel-conversations";
3803
+ var PLACE_CONVERSATIONS = "place-conversations";
3531
3804
  var DASHBOARD_UPDATES = "dashboard-updates-granted";
3532
3805
  var CRON_VERBS = "cron-verbs-granted";
3533
3806
  var KNOWLEDGE_NOTICES = "knowledge-notices";
3534
- var KnowledgeEventSchema = z15.object({
3807
+ var IMAGE_UPLOADS = "image-uploads-granted";
3808
+ var KnowledgeEventSchema = z16.object({
3535
3809
  topic: NameSchema,
3536
3810
  project: NameSchema.nullable(),
3537
- replaced: z15.boolean().optional()
3811
+ replaced: z16.boolean().optional()
3538
3812
  });
3539
3813
  var CRON_VERB_NAMES = ["create_cron", "update_cron", "remove_cron"];
3540
3814
  var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
@@ -3601,8 +3875,8 @@ function restage(existing, edit) {
3601
3875
  };
3602
3876
  }
3603
3877
  function reopenStage(stage) {
3604
- const { completedBy: _by, completedAt: _at, ...open2 } = stage;
3605
- return open2;
3878
+ const { completedBy: _by, completedAt: _at, ...open3 } = stage;
3879
+ return open3;
3606
3880
  }
3607
3881
  function gateChanges(before, after) {
3608
3882
  const changes = [];
@@ -3666,6 +3940,7 @@ var Board = class _Board {
3666
3940
  mutex = new Mutex();
3667
3941
  /** The turns in flight, which the server hosting them supplies; none until it does. */
3668
3942
  turnsInFlight = () => [];
3943
+ imageSource = null;
3669
3944
  newId = monotonicFactory();
3670
3945
  events;
3671
3946
  actionCounts = /* @__PURE__ */ new Map();
@@ -3682,7 +3957,7 @@ var Board = class _Board {
3682
3957
  homes;
3683
3958
  constructor(dataDir, options) {
3684
3959
  this.paths = new BoardPaths(dataDir);
3685
- this.homes = new HomeRepos(path6.join(this.paths.state(), "home-hooks"));
3960
+ this.homes = new HomeRepos(path7.join(this.paths.state(), "home-hooks"));
3686
3961
  this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
3687
3962
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
3688
3963
  this.events = new EventLog(this.paths.eventLog(), () => this.newId(), this.now);
@@ -3715,7 +3990,7 @@ var Board = class _Board {
3715
3990
  * repeated, so a member that later chooses otherwise keeps its choice.
3716
3991
  */
3717
3992
  async applyAlignments() {
3718
- const file = path6.join(this.paths.state(), "alignments.json");
3993
+ const file = path7.join(this.paths.state(), "alignments.json");
3719
3994
  const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
3720
3995
  const pending = [
3721
3996
  FOLLOW_DECISIONS,
@@ -3730,7 +4005,9 @@ var Board = class _Board {
3730
4005
  CHANNEL_CONVERSATIONS,
3731
4006
  DASHBOARD_UPDATES,
3732
4007
  CRON_VERBS,
3733
- KNOWLEDGE_NOTICES
4008
+ KNOWLEDGE_NOTICES,
4009
+ IMAGE_UPLOADS,
4010
+ PLACE_CONVERSATIONS
3734
4011
  ].filter((name) => !applied.includes(name));
3735
4012
  if (pending.length === 0) {
3736
4013
  return;
@@ -3760,13 +4037,13 @@ var Board = class _Board {
3760
4037
  await this.openAsksChannel();
3761
4038
  }
3762
4039
  if (pending.includes(KNOWLEDGE_REMOVAL)) {
3763
- await this.grantBesideKnowledge("remove_knowledge");
4040
+ await this.grantBeside("write_knowledge", "remove_knowledge");
3764
4041
  }
3765
4042
  if (pending.includes(CHANNEL_CONVERSATIONS)) {
3766
4043
  await this.startChannelCursors();
3767
4044
  }
3768
4045
  if (pending.includes(DASHBOARD_UPDATES)) {
3769
- await this.grantBesideKnowledge("update_dashboard");
4046
+ await this.grantBeside("write_knowledge", "update_dashboard");
3770
4047
  await this.rewordDashboards();
3771
4048
  }
3772
4049
  if (pending.includes(CRON_VERBS)) {
@@ -3775,6 +4052,12 @@ var Board = class _Board {
3775
4052
  if (pending.includes(KNOWLEDGE_NOTICES)) {
3776
4053
  await this.noticeKnowledgeNotes();
3777
4054
  }
4055
+ if (pending.includes(IMAGE_UPLOADS)) {
4056
+ await this.grantBeside("post_message", "upload_image");
4057
+ }
4058
+ if (pending.includes(PLACE_CONVERSATIONS)) {
4059
+ await this.startPlaceCursors();
4060
+ }
3778
4061
  await writeJson(file, { applied: [...applied, ...pending] });
3779
4062
  }
3780
4063
  /**
@@ -3816,12 +4099,12 @@ var Board = class _Board {
3816
4099
  }
3817
4100
  }
3818
4101
  /**
3819
- * Grants `verb` once to every charter granting `write_knowledge`, the work roles' charters in
3820
- * practice; seed roles have it from their seed.
4102
+ * Grants `verb` once to every charter granting `beside`, the work roles' charters in practice;
4103
+ * seed roles have it from their seed.
3821
4104
  */
3822
- async grantBesideKnowledge(verb) {
4105
+ async grantBeside(beside, verb) {
3823
4106
  for (const charter of await this.listRoles()) {
3824
- if (charter.verbs.includes("write_knowledge") && !charter.verbs.includes(verb)) {
4107
+ if (charter.verbs.includes(beside) && !charter.verbs.includes(verb)) {
3825
4108
  await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
3826
4109
  }
3827
4110
  }
@@ -3979,7 +4262,7 @@ var Board = class _Board {
3979
4262
  }
3980
4263
  }
3981
4264
  for (const file of await listFiles(this.paths.tasks(slug))) {
3982
- const raw = await readMarkdown(path6.join(this.paths.tasks(slug), file), z15.record(z15.string(), z15.unknown()));
4265
+ const raw = await readMarkdown(path7.join(this.paths.tasks(slug), file), z16.record(z16.string(), z16.unknown()));
3983
4266
  const legacy = raw.data["thread"];
3984
4267
  if (legacy === void 0) {
3985
4268
  continue;
@@ -3987,7 +4270,7 @@ var Board = class _Board {
3987
4270
  const task = { ...TaskFrontmatterSchema.parse(raw.data), body: raw.body };
3988
4271
  if ((legacy === "open" || legacy === "closed") && !await exists(this.paths.threadFile(threads, task.id))) {
3989
4272
  const ended = task.status === "done" || task.status === "abandoned";
3990
- const open2 = legacy === "open" && !ended;
4273
+ const open3 = legacy === "open" && !ended;
3991
4274
  const openedBy = opened.get(task.id);
3992
4275
  const closedBy = closed.get(task.id);
3993
4276
  await this.writeThread(threads, {
@@ -3995,10 +4278,10 @@ var Board = class _Board {
3995
4278
  channel: channelRef(slug, "general"),
3996
4279
  title: task.title,
3997
4280
  subject: { kind: "task", id: task.id },
3998
- state: open2 ? "open" : "closed",
4281
+ state: open3 ? "open" : "closed",
3999
4282
  openedBy: openedBy?.actor ?? task.createdBy,
4000
4283
  openedAt: openedBy?.ts ?? task.createdAt,
4001
- ...open2 ? {} : {
4284
+ ...open3 ? {} : {
4002
4285
  closedBy: closedBy?.actor ?? SYSTEM_ACTOR.name,
4003
4286
  closedAt: closedBy?.ts ?? this.now().toISOString()
4004
4287
  },
@@ -4030,6 +4313,24 @@ var Board = class _Board {
4030
4313
  await writeJson(file, { ...cursors, channelsFrom });
4031
4314
  }
4032
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
+ }
4033
4334
  /**
4034
4335
  * Threads were once read in the scope's single conversation, so a member had read every thread
4035
4336
  * up to its scope's cursor. Each scope's threads start from that cursor now, as each thread's
@@ -4057,7 +4358,7 @@ var Board = class _Board {
4057
4358
  * turns, so it follows nothing now.
4058
4359
  */
4059
4360
  async renameInboxCursors() {
4060
- const Legacy = z15.object({ inbox: z15.string().nullable() });
4361
+ const Legacy = z16.object({ inbox: z16.string().nullable() });
4061
4362
  for (const agent of await this.listAgents()) {
4062
4363
  const file = this.paths.agentCursors(agent.name);
4063
4364
  if (await exists(file)) {
@@ -4103,7 +4404,7 @@ ${charter.purpose}
4103
4404
  */
4104
4405
  async alignSeedRole(seed) {
4105
4406
  const file = this.paths.role(seed.name);
4106
- const raw = await readMarkdown(file, z15.record(z15.string(), z15.unknown()));
4407
+ const raw = await readMarkdown(file, z16.record(z16.string(), z16.unknown()));
4107
4408
  const current = RoleCharterSchema.parse(raw.data);
4108
4409
  const verbsAdded = seed.verbs.filter((verb) => !current.verbs.includes(verb));
4109
4410
  const fieldsAligned = RoleCharterSchema.keyof().options.filter((key) => key === "verbs" || seed[key] === void 0 ? false : seed.name === USER_ROLE ? JSON.stringify(seed[key]) !== JSON.stringify(current[key]) : !(key in raw.data));
@@ -4170,13 +4471,15 @@ ${charter.purpose}
4170
4471
  const token = mintToken();
4171
4472
  const thread = threadOfPart(part);
4172
4473
  const channel = channelOfPart(part);
4474
+ const place = placeOfPart(part);
4173
4475
  this.turnTokens.set(hashToken(token), {
4174
4476
  actor: {
4175
4477
  name: agent,
4176
4478
  role,
4177
4479
  ...scope === void 0 ? {} : { scope },
4178
4480
  ...thread === void 0 ? {} : { thread },
4179
- ...channel === void 0 ? {} : { channel }
4481
+ ...channel === void 0 ? {} : { channel },
4482
+ ...place === void 0 ? {} : { place }
4180
4483
  },
4181
4484
  expiresAt: this.now().getTime() + ttlMs
4182
4485
  });
@@ -4265,7 +4568,7 @@ ${charter.purpose}
4265
4568
  });
4266
4569
  }
4267
4570
  signInsFile() {
4268
- return path6.join(this.paths.state(), "sign-ins.json");
4571
+ return path7.join(this.paths.state(), "sign-ins.json");
4269
4572
  }
4270
4573
  async readSignIns() {
4271
4574
  const file = this.signInsFile();
@@ -4362,7 +4665,7 @@ ${charter.purpose}
4362
4665
  const files = await listFiles(this.paths.tasks(project));
4363
4666
  const tasks = [];
4364
4667
  for (const file of files) {
4365
- 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);
4366
4669
  tasks.push({ ...doc.data, body: doc.body });
4367
4670
  }
4368
4671
  return tasks;
@@ -4381,7 +4684,7 @@ ${charter.purpose}
4381
4684
  const files = await listFiles(this.paths.proposals());
4382
4685
  const proposals = [];
4383
4686
  for (const file of files) {
4384
- const doc = await readMarkdown(path6.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
4687
+ const doc = await readMarkdown(path7.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
4385
4688
  proposals.push({ ...doc.data, body: doc.body });
4386
4689
  }
4387
4690
  return proposals;
@@ -4408,7 +4711,7 @@ ${charter.purpose}
4408
4711
  async listMembers() {
4409
4712
  const members = [];
4410
4713
  for (const file of await listFiles(this.paths.members())) {
4411
- const doc = await readMarkdown(path6.join(this.paths.members(), file), MemberSchema);
4714
+ const doc = await readMarkdown(path7.join(this.paths.members(), file), MemberSchema);
4412
4715
  members.push({ ...doc.data, profile: doc.body });
4413
4716
  }
4414
4717
  return members;
@@ -4420,14 +4723,14 @@ ${charter.purpose}
4420
4723
  /** The society's norms, the one knowledge topic every turn loads. Empty until the steward writes it. */
4421
4724
  async readSocietyNorms() {
4422
4725
  const file = this.paths.societyKnowledgeFile("norms");
4423
- return await exists(file) ? (await readMarkdown(file, z15.record(z15.string(), z15.unknown()))).body : "";
4726
+ return await exists(file) ? (await readMarkdown(file, z16.record(z16.string(), z16.unknown()))).body : "";
4424
4727
  }
4425
4728
  /** Knowledge topics of a project, or of the society when the project is null. */
4426
4729
  async listKnowledge(project) {
4427
4730
  const dir = project === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project);
4428
4731
  const topics = [];
4429
4732
  for (const file of await listFiles(dir)) {
4430
- 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$/, "")));
4431
4734
  }
4432
4735
  return topics;
4433
4736
  }
@@ -4440,7 +4743,7 @@ ${charter.purpose}
4440
4743
  */
4441
4744
  async knowledgeAt(file, project, topic) {
4442
4745
  const { data, body } = await readLooseMarkdown(file);
4443
- const revision = z15.number().int().min(0).safeParse(data["revision"]);
4746
+ const revision = z16.number().int().min(0).safeParse(data["revision"]);
4444
4747
  const updatedBy = NameSchema.safeParse(data["updatedBy"]);
4445
4748
  const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
4446
4749
  return {
@@ -4457,7 +4760,7 @@ ${charter.purpose}
4457
4760
  const aside = this.paths.removedKnowledge(project ?? SOCIETY_SCOPE);
4458
4761
  let highest = 0;
4459
4762
  for (const file of (await listFiles(aside)).filter((name) => name.startsWith(`${topic}.`))) {
4460
- const copy = await this.knowledgeAt(path6.join(aside, file), project, topic);
4763
+ const copy = await this.knowledgeAt(path7.join(aside, file), project, topic);
4461
4764
  highest = Math.max(highest, copy.revision);
4462
4765
  }
4463
4766
  return highest;
@@ -4473,7 +4776,7 @@ ${charter.purpose}
4473
4776
  async listSkillsIn(dir, scope) {
4474
4777
  const skills = [];
4475
4778
  for (const entry of await listDirs(dir)) {
4476
- const file = path6.join(dir, entry, "SKILL.md");
4779
+ const file = path7.join(dir, entry, "SKILL.md");
4477
4780
  if (!await exists(file)) {
4478
4781
  continue;
4479
4782
  }
@@ -4563,7 +4866,7 @@ ${charter.purpose}
4563
4866
  if (!wanted.delete(file.slice(0, 26))) {
4564
4867
  continue;
4565
4868
  }
4566
- const { data } = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
4869
+ const { data } = await readMarkdown(path7.join(dir, file), MessageFrontmatterSchema);
4567
4870
  found.push(MessageRefSchema.parse(data));
4568
4871
  }
4569
4872
  if (wanted.size === 0) {
@@ -4595,7 +4898,7 @@ ${charter.purpose}
4595
4898
  if (!file.endsWith(".md")) {
4596
4899
  continue;
4597
4900
  }
4598
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
4901
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
4599
4902
  if (doc.data.state !== "open") {
4600
4903
  continue;
4601
4904
  }
@@ -4622,7 +4925,7 @@ ${charter.purpose}
4622
4925
  const all = [];
4623
4926
  for (const threads of await this.threadScopes()) {
4624
4927
  for (const file of await listFiles(threads)) {
4625
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
4928
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
4626
4929
  const messages = await listFiles(this.paths.threadMessages(threads, doc.data.id));
4627
4930
  const last = messages.at(-1);
4628
4931
  all.push({
@@ -4989,16 +5292,20 @@ ${DASHBOARD_LINE}
4989
5292
  }
4990
5293
  /**
4991
5294
  * How many unread digest messages a member has in each conversation: a scope's home, for its
4992
- * 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
4993
5297
  * messages are and nowhere else. Society channels count only for a member woken in the society
4994
5298
  * scope; a project member reads them at its next turn. Two kinds of post are in the digest but not
4995
5299
  * counted, and are read when the member wakes for something else: a task step's, which wakes
4996
5300
  * whoever it hands work to, and the board's own, operations signals and landing announcements,
4997
- * 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.
4998
5304
  */
4999
5305
  async unreadByConversation(actor) {
5000
5306
  const { cursors, upto } = await this.digestSnapshot(actor.name);
5001
5307
  const counts = /* @__PURE__ */ new Map();
5308
+ const closed = /* @__PURE__ */ new Map();
5002
5309
  const unscoped = { name: actor.name, role: actor.role };
5003
5310
  for (const { scope, part, message, news } of await this.digestSince(unscoped, cursors, upto)) {
5004
5311
  if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
@@ -5008,11 +5315,23 @@ ${DASHBOARD_LINE}
5008
5315
  if (posted !== scope && scope !== SOCIETY_SCOPE) {
5009
5316
  continue;
5010
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
+ }
5011
5329
  const key = conversationKey(scope, part);
5012
5330
  const entry = counts.get(key) ?? {
5013
5331
  scope,
5014
- thread: threadOfPart(part) ?? null,
5332
+ thread: thread ?? null,
5015
5333
  channel: channelOfPart(part) ?? null,
5334
+ place: placeOfPart(part) ?? null,
5016
5335
  count: 0
5017
5336
  };
5018
5337
  counts.set(key, { ...entry, count: entry.count + 1 });
@@ -5070,8 +5389,9 @@ ${DASHBOARD_LINE}
5070
5389
  * thread it takes part in, and for the front desk, which every post by the user wakes, every post
5071
5390
  * by the user wherever it is. With a conversation, only what is filed there: for a thread, every
5072
5391
  * message of the thread, since the conversation is about it; for a channel's, that channel's
5073
- * top-level messages; for a scope's home, general's and those of a project the member is not in,
5074
- * 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
5075
5395
  * `upto` counts (see `digestSnapshot`).
5076
5396
  */
5077
5397
  async digestSince(actor, cursors, upto, conversation) {
@@ -5091,11 +5411,17 @@ ${DASHBOARD_LINE}
5091
5411
  const frontDesk = charter.wakeTriggers.includes(FRONT_DESK_TRIGGER);
5092
5412
  const threadParticipation = /* @__PURE__ */ new Map();
5093
5413
  const openChannels = /* @__PURE__ */ new Map();
5414
+ const liveProjects = /* @__PURE__ */ new Map();
5094
5415
  const collected = [];
5095
5416
  const newsFrom = charter.societyScope ? void 0 : newsStart(cursors);
5096
5417
  const scopes = conversation !== void 0 ? [conversation.scope] : [...agent.memberships, SOCIETY_SCOPE];
5097
5418
  const starts = [
5098
- ...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)]),
5099
5425
  ...newsFrom === void 0 ? [] : [newsFrom]
5100
5426
  ];
5101
5427
  const known = starts.filter((start) => start !== null);
@@ -5105,7 +5431,7 @@ ${DASHBOARD_LINE}
5105
5431
  const thread = message.thread ?? null;
5106
5432
  const asked = message.mentions.includes(actor.name) || frontDesk && message.author === USER_NAME;
5107
5433
  if (newsFrom !== void 0 && thread === null && filedIn === SOCIETY_SCOPE && !asked) {
5108
- 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) {
5109
5435
  collected.push({
5110
5436
  scope: conversation?.scope ?? SOCIETY_SCOPE,
5111
5437
  part: null,
@@ -5116,12 +5442,18 @@ ${DASHBOARD_LINE}
5116
5442
  continue;
5117
5443
  }
5118
5444
  const named = thread === null ? channelConversation(filedIn, message.channel) : void 0;
5119
- let open2 = named === void 0 ? void 0 : openChannels.get(message.channel);
5120
- if (named !== void 0 && open2 === void 0) {
5121
- open2 = await this.channelOpen(message.channel);
5122
- openChannels.set(message.channel, open2);
5123
- }
5124
- 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);
5125
5457
  if (conversation !== void 0 && (filedIn !== conversation.scope || part !== conversation.part)) {
5126
5458
  continue;
5127
5459
  }
@@ -5186,7 +5518,7 @@ ${task.body}`;
5186
5518
  }
5187
5519
  }
5188
5520
  for (const file of await listFiles(this.paths.projectKnowledge(project))) {
5189
- 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");
5190
5522
  if (content.toLowerCase().includes(needle)) {
5191
5523
  hits.push({
5192
5524
  kind: "knowledge",
@@ -5207,7 +5539,7 @@ ${task.body}`;
5207
5539
  shared.push({
5208
5540
  kind: "knowledge",
5209
5541
  ref: `society/knowledge/${file}`,
5210
- file: path6.join(this.paths.societyKnowledge(), file)
5542
+ file: path7.join(this.paths.societyKnowledge(), file)
5211
5543
  });
5212
5544
  }
5213
5545
  for (const skill of await this.listSocietySkills()) {
@@ -5220,7 +5552,7 @@ ${task.body}`;
5220
5552
  shared.push({
5221
5553
  kind: "memory",
5222
5554
  ref: `memory/${file}`,
5223
- file: path6.join(this.paths.agentMemory(actor.name), file)
5555
+ file: path7.join(this.paths.agentMemory(actor.name), file)
5224
5556
  });
5225
5557
  }
5226
5558
  for (const entry of shared) {
@@ -5310,7 +5642,7 @@ ${current.body}`);
5310
5642
  });
5311
5643
  const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
5312
5644
  await mkdir4(aside, { recursive: true });
5313
- 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`));
5314
5646
  await this.events.append("knowledge.removed", actor.name, {
5315
5647
  topic: args.topic,
5316
5648
  project: args.project
@@ -5358,7 +5690,7 @@ ${current.body}`);
5358
5690
  });
5359
5691
  const aside = this.paths.removedSkills();
5360
5692
  await mkdir4(aside, { recursive: true });
5361
- 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(":", "-")}`));
5362
5694
  await this.events.append("skill.removed", actor.name, {
5363
5695
  name: args.name,
5364
5696
  reason: args.reason
@@ -5403,11 +5735,65 @@ ${current.body}`);
5403
5735
  return updated;
5404
5736
  });
5405
5737
  }
5738
+ /**
5739
+ * Keeps an image a citizen's turn names by its path, read on the runner the turn runs on, which
5740
+ * refuses a file outside the turn's worktree, the citizen's home, and the system temp folder.
5741
+ * Only the bytes of a PNG, JPEG, GIF, or WebP of at most `IMAGE_LIMIT_BYTES` are kept, checked
5742
+ * here again whatever the runner said, under a new id outside the projection, so no runner
5743
+ * mirrors them and no prompt carries them: a post shows the image by its `image:` address.
5744
+ */
5745
+ async uploadImage(actor, input) {
5746
+ const args = VerbInputs.upload_image.parse(input);
5747
+ await this.authorize(actor, "upload_image");
5748
+ if (actor.scope === void 0) {
5749
+ throw new BoardError("INVALID_STATE", "upload_image reads the file on the machine a turn runs on, so only a turn can call it");
5750
+ }
5751
+ if (this.imageSource === null) {
5752
+ throw new BoardError("NOT_FOUND", "this board has no runners to read an image from");
5753
+ }
5754
+ const bytes = await this.imageSource(actor, args.path);
5755
+ const type = imageTypeOf(bytes);
5756
+ if (type === null) {
5757
+ throw new BoardError("VALIDATION", `${args.path} is not a PNG, JPEG, GIF, or WebP image by its first bytes`);
5758
+ }
5759
+ if (bytes.byteLength > IMAGE_LIMIT_BYTES) {
5760
+ throw new BoardError("VALIDATION", `${args.path} is ${bytes.byteLength} bytes, over the ${IMAGE_LIMIT_BYTES} an image may be`);
5761
+ }
5762
+ return this.mutex.run(async () => {
5763
+ const id = this.newId();
5764
+ await writeFileAtomic(this.paths.image(id), bytes);
5765
+ await this.events.append("image.uploaded", actor.name, {
5766
+ id,
5767
+ type,
5768
+ size: bytes.byteLength
5769
+ });
5770
+ const name = path7.basename(args.path).replaceAll(/[[\]\\\r\n]/g, "");
5771
+ return {
5772
+ id,
5773
+ type,
5774
+ size: bytes.byteLength,
5775
+ markdown: `![${name}](${IMAGE_SCHEME}${id})`
5776
+ };
5777
+ });
5778
+ }
5779
+ /** An uploaded image's bytes and kind, which its first bytes tell. */
5780
+ async readImage(id) {
5781
+ const file = this.paths.image(UlidSchema.parse(id));
5782
+ if (!await exists(file)) {
5783
+ throw new BoardError("NOT_FOUND", `image ${id} not found`);
5784
+ }
5785
+ const bytes = await readFile4(file);
5786
+ const type = imageTypeOf(bytes);
5787
+ if (type === null) {
5788
+ throw new BoardError("NOT_FOUND", `image ${id} is not an image the board shows`);
5789
+ }
5790
+ return { type, bytes };
5791
+ }
5406
5792
  /** Every cron, ended ones included, oldest first. */
5407
5793
  async listCrons() {
5408
5794
  const crons = [];
5409
5795
  for (const file of await listFiles(this.paths.crons())) {
5410
- crons.push(await this.readCronFile(path6.join(this.paths.crons(), file)));
5796
+ crons.push(await this.readCronFile(path7.join(this.paths.crons(), file)));
5411
5797
  }
5412
5798
  return crons;
5413
5799
  }
@@ -5529,11 +5915,16 @@ ${current.body}`);
5529
5915
  }
5530
5916
  /**
5531
5917
  * Where a cron fires for its citizen: the thread, the channel, or the scope's home it names, else
5532
- * 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.
5533
5920
  */
5534
5921
  async cronConversation(actor, agent, named) {
5535
5922
  const own = actor.name === agent.name;
5536
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
+ }
5537
5928
  return own && actor.scope !== void 0 ? this.cronConversation(actor, agent, {
5538
5929
  project: actor.scope,
5539
5930
  thread_id: actor.thread,
@@ -5609,7 +6000,8 @@ ${current.body}`);
5609
6000
  agent: cron.agent,
5610
6001
  project: cron.scope,
5611
6002
  ...cron.thread === void 0 ? {} : { thread: cron.thread },
5612
- ...cron.channel === void 0 ? {} : { channel: cron.channel }
6003
+ ...cron.channel === void 0 ? {} : { channel: cron.channel },
6004
+ ...cron.place === void 0 ? {} : { place: cron.place }
5613
6005
  };
5614
6006
  }
5615
6007
  async readCronFile(file) {
@@ -5794,6 +6186,13 @@ ${current.body}`);
5794
6186
  watchTurns(source) {
5795
6187
  this.turnsInFlight = source;
5796
6188
  }
6189
+ /**
6190
+ * Where `upload_image` reads a file from: the runner the calling turn runs on, through the server
6191
+ * that hosts the runners. Without one, as in the admin CLI, nothing can be uploaded.
6192
+ */
6193
+ readImagesWith(source) {
6194
+ this.imageSource = source;
6195
+ }
5797
6196
  /**
5798
6197
  * Refuses to end conversations while another citizen's turn is in one of them: `cut` picks the
5799
6198
  * turns that would lose theirs. The caller's own turn is exempt, since ending a conversation
@@ -5813,17 +6212,20 @@ ${current.body}`);
5813
6212
  async openThreadIds(dir, keep) {
5814
6213
  const ids = /* @__PURE__ */ new Set();
5815
6214
  for (const file of await listFiles(dir)) {
5816
- const doc = await readMarkdown(path6.join(dir, file), ThreadFrontmatterSchema);
6215
+ const doc = await readMarkdown(path7.join(dir, file), ThreadFrontmatterSchema);
5817
6216
  if (doc.data.state === "open" && keep({ ...doc.data, body: doc.body })) {
5818
6217
  ids.add(doc.data.id);
5819
6218
  }
5820
6219
  }
5821
6220
  return ids;
5822
6221
  }
5823
- /** 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
+ */
5824
6226
  async assertProjectQuiet(actor, slug) {
5825
6227
  const threads = await this.openThreadIds(this.paths.threads(slug), () => true);
5826
- 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));
5827
6229
  }
5828
6230
  async closeThread(actor, input) {
5829
6231
  const args = VerbInputs.close_thread.parse(input);
@@ -6364,7 +6766,8 @@ ${args.summary}`);
6364
6766
  filedFrom: {
6365
6767
  scope: actor.scope,
6366
6768
  ...actor.thread === void 0 ? {} : { thread: actor.thread },
6367
- ...actor.channel === void 0 ? {} : { channel: actor.channel }
6769
+ ...actor.channel === void 0 ? {} : { channel: actor.channel },
6770
+ ...actor.place === void 0 ? {} : { place: actor.place }
6368
6771
  }
6369
6772
  },
6370
6773
  charter: parsed.data
@@ -6623,6 +7026,8 @@ ${rationale}`}`);
6623
7026
  return this.updateCron(actor, VerbInputs.update_cron.parse(input));
6624
7027
  case "remove_cron":
6625
7028
  return this.removeCron(actor, VerbInputs.remove_cron.parse(input));
7029
+ case "upload_image":
7030
+ return this.uploadImage(actor, VerbInputs.upload_image.parse(input));
6626
7031
  default:
6627
7032
  throw new BoardError("VALIDATION", `unknown verb ${String(verb)}`);
6628
7033
  }
@@ -6669,7 +7074,7 @@ ${rationale}`}`);
6669
7074
  }
6670
7075
  async readAgentRoleBody(agent) {
6671
7076
  const file = this.paths.agentRole(agent);
6672
- return await exists(file) ? (await readMarkdown(file, z15.record(z15.string(), z15.unknown()))).body : "";
7077
+ return await exists(file) ? (await readMarkdown(file, z16.record(z16.string(), z16.unknown()))).body : "";
6673
7078
  }
6674
7079
  /** The steps of one finished turn, as the runner handed them over when it ended. */
6675
7080
  /**
@@ -6774,19 +7179,20 @@ ${rationale}`}`);
6774
7179
  project: input.project,
6775
7180
  ...input.thread === void 0 ? {} : { thread: input.thread },
6776
7181
  ...input.channel === void 0 ? {} : { channel: input.channel },
7182
+ ...input.place === void 0 ? {} : { place: input.place },
6777
7183
  messages: [...input.messages]
6778
7184
  });
6779
7185
  });
6780
7186
  }
6781
7187
  /**
6782
7188
  * Where a conversation's session ids and last turn are kept: the scope's home, or beside it a
6783
- * 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.
6784
7190
  */
6785
7191
  sessionDir(agent, scope, part) {
6786
7192
  return part === void 0 ? this.paths.agentProject(agent, scope) : this.paths.agentConversation(agent, scope, part);
6787
7193
  }
6788
7194
  async readSessions(agent, project, part) {
6789
- const file = path6.join(this.sessionDir(agent, project, part), "sessions.json");
7195
+ const file = path7.join(this.sessionDir(agent, project, part), "sessions.json");
6790
7196
  return await exists(file) ? readJson(file, SessionsFileSchema) : {};
6791
7197
  }
6792
7198
  /**
@@ -6797,15 +7203,34 @@ ${rationale}`}`);
6797
7203
  await this.mutex.run(async () => {
6798
7204
  const dir = this.sessionDir(agent, project, part);
6799
7205
  await ensureDir(dir);
6800
- const file = path6.join(dir, "sessions.json");
7206
+ const file = path7.join(dir, "sessions.json");
6801
7207
  const current = await exists(file) ? await readJson(file, SessionsFileSchema) : {};
6802
7208
  const kept = current.runner === void 0 || current.runner === runner ? current : {};
6803
7209
  await writeJson(file, { ...kept, [cli]: sessionId, runner });
6804
7210
  });
6805
7211
  }
6806
- /** 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. */
6807
7232
  async readLastTurn(agent, project, part) {
6808
- 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");
6809
7234
  return await exists(file) ? readJson(file, TurnRecordSchema) : null;
6810
7235
  }
6811
7236
  /** The latest turn a citizen took in a scope, in any of its conversations there. */
@@ -6813,8 +7238,9 @@ ${rationale}`}`);
6813
7238
  let latest = await this.readLastTurn(agent, project);
6814
7239
  const place = this.paths.agentProject(agent, project);
6815
7240
  const parts = [
6816
- ...await listDirs(path6.join(place, "threads")),
6817
- ...(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}`)
6818
7244
  ];
6819
7245
  for (const part of parts) {
6820
7246
  const turn = await this.readLastTurn(agent, project, part);
@@ -6835,6 +7261,7 @@ ${rationale}`}`);
6835
7261
  project: parsed.project,
6836
7262
  ...parsed.thread === void 0 ? {} : { thread: parsed.thread },
6837
7263
  ...parsed.channel === void 0 ? {} : { channel: parsed.channel },
7264
+ ...parsed.place === void 0 ? {} : { place: parsed.place },
6838
7265
  trigger: parsed.trigger.kind,
6839
7266
  session: parsed.session,
6840
7267
  runner: parsed.runner
@@ -6864,7 +7291,8 @@ ${rationale}`}`);
6864
7291
  agent: parsed.agent,
6865
7292
  project: parsed.project,
6866
7293
  ...parsed.thread === void 0 ? {} : { thread: parsed.thread },
6867
- ...parsed.channel === void 0 ? {} : { channel: parsed.channel }
7294
+ ...parsed.channel === void 0 ? {} : { channel: parsed.channel },
7295
+ ...parsed.place === void 0 ? {} : { place: parsed.place }
6868
7296
  });
6869
7297
  }
6870
7298
  await this.events.append(failed ? "turn.failed" : "turn.completed", parsed.agent, {
@@ -6872,6 +7300,7 @@ ${rationale}`}`);
6872
7300
  project: parsed.project,
6873
7301
  ...parsed.thread === void 0 ? {} : { thread: parsed.thread },
6874
7302
  ...parsed.channel === void 0 ? {} : { channel: parsed.channel },
7303
+ ...parsed.place === void 0 ? {} : { place: parsed.place },
6875
7304
  trigger: parsed.trigger.kind,
6876
7305
  session: parsed.session,
6877
7306
  exitReason: parsed.exitReason,
@@ -6881,7 +7310,9 @@ ${rationale}`}`);
6881
7310
  model: parsed.model,
6882
7311
  summary: parsed.status?.summary ?? null,
6883
7312
  needsUserDecision: parsed.status?.needsUserDecision ?? false,
6884
- error: parsed.error
7313
+ error: parsed.error,
7314
+ ...parsed.resumeFailed === void 0 ? {} : { resumeFailed: parsed.resumeFailed },
7315
+ ...parsed.sessionDropped === true ? { sessionDropped: true } : {}
6885
7316
  });
6886
7317
  });
6887
7318
  }
@@ -6918,6 +7349,20 @@ ${rationale}`}`);
6918
7349
  return task;
6919
7350
  });
6920
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
+ }
6921
7366
  /**
6922
7367
  * Records that a citizen's workspace in a conversation that has ended holds work on no branch, so
6923
7368
  * the scheduler gives the citizen a closing turn there to decide about it. False, recording
@@ -6928,7 +7373,7 @@ ${rationale}`}`);
6928
7373
  const payload = {
6929
7374
  agent: input.agent,
6930
7375
  project: input.scope,
6931
- ...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 }
6932
7377
  };
6933
7378
  return this.mutex.run(async () => {
6934
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;
@@ -6980,9 +7425,13 @@ ${rationale}`}`);
6980
7425
  }
6981
7426
  /**
6982
7427
  * Whether a conversation with a workspace of its own has ended: a thread closed, a task ended
6983
- * (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.
6984
7430
  */
6985
7431
  async conversationEnded(conversation) {
7432
+ if (conversation.kind === "place") {
7433
+ return !await this.projectLive(conversation.project);
7434
+ }
6986
7435
  if (conversation.kind === "channel") {
6987
7436
  const place = conversation.scope === SOCIETY_SCOPE ? null : conversation.scope;
6988
7437
  return !await this.channelOpen(channelRef(place, conversation.name));
@@ -7056,11 +7505,11 @@ ${rationale}`}`);
7056
7505
  }
7057
7506
  /** Small named state files under data/state, for cursors the scheduler must keep across restarts. */
7058
7507
  async readState(name, schema, fallback) {
7059
- const file = path6.join(this.paths.state(), `${name}.json`);
7508
+ const file = path7.join(this.paths.state(), `${name}.json`);
7060
7509
  return await exists(file) ? readJson(file, schema) : fallback;
7061
7510
  }
7062
7511
  async writeState(name, value) {
7063
- 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));
7064
7513
  }
7065
7514
  /** The project's dashboard, empty at revision 0 when it has none. */
7066
7515
  async readDashboard(slug) {
@@ -7077,7 +7526,7 @@ ${rationale}`}`);
7077
7526
  return { project: slug, revision: 0, body: "" };
7078
7527
  }
7079
7528
  const { data, body } = await readLooseMarkdown(file);
7080
- const revision = z15.number().int().min(0).safeParse(data["revision"]);
7529
+ const revision = z16.number().int().min(0).safeParse(data["revision"]);
7081
7530
  const updatedBy = NameSchema.safeParse(data["updatedBy"]);
7082
7531
  const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
7083
7532
  return {
@@ -7091,7 +7540,7 @@ ${rationale}`}`);
7091
7540
  async listRunners() {
7092
7541
  const runners2 = [];
7093
7542
  for (const file of await listFiles(this.paths.runners())) {
7094
- 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);
7095
7544
  }
7096
7545
  return runners2;
7097
7546
  }
@@ -7143,7 +7592,7 @@ ${rationale}`}`);
7143
7592
  return this.runnerTokens.get(hashToken(token)) ?? null;
7144
7593
  }
7145
7594
  runnerTokensFile() {
7146
- return path6.join(this.paths.state(), "runners.json");
7595
+ return path7.join(this.paths.state(), "runners.json");
7147
7596
  }
7148
7597
  async readRunnerTokens() {
7149
7598
  const file = this.runnerTokensFile();
@@ -7192,26 +7641,26 @@ ${rationale}`}`);
7192
7641
  async listHomeConflicts(agent) {
7193
7642
  const home = this.paths.agent(agent);
7194
7643
  const found = [];
7195
- const walk = async (dir, prefix) => {
7196
- 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(() => [])) {
7197
7646
  if (prefix === "" && SKIPPED_IN_HOME.has(entry.name)) {
7198
7647
  continue;
7199
7648
  }
7200
7649
  const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
7201
7650
  if (entry.isDirectory()) {
7202
- await walk(path6.join(dir, entry.name), relative);
7651
+ await walk2(path7.join(dir, entry.name), relative);
7203
7652
  continue;
7204
7653
  }
7205
7654
  const file = entry.isFile() ? conflictCopyOf(relative) : null;
7206
7655
  if (file !== null) {
7207
- 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();
7208
7657
  if (since !== void 0) {
7209
7658
  found.push({ path: relative, file, since });
7210
7659
  }
7211
7660
  }
7212
7661
  }
7213
7662
  };
7214
- await walk(home, "");
7663
+ await walk2(home, "");
7215
7664
  return found.toSorted((a, b) => a.path.localeCompare(b.path));
7216
7665
  }
7217
7666
  /** The newest `limit` changes to an agent's home, newest first; empty while it is no repository. */
@@ -7288,7 +7737,7 @@ ${rationale}`}`);
7288
7737
  async writeTurnRecord(record) {
7289
7738
  const dir = this.sessionDir(record.agent, record.project, conversationPart(record));
7290
7739
  await ensureDir(dir);
7291
- await writeJson(path6.join(dir, "last-turn.json"), record);
7740
+ await writeJson(path7.join(dir, "last-turn.json"), record);
7292
7741
  }
7293
7742
  // ---------------------------------------------------------------------------------------------
7294
7743
  // Internals
@@ -7351,7 +7800,7 @@ ${charter.purpose}
7351
7800
 
7352
7801
  ${charter.purpose}
7353
7802
 
7354
- Route every lesson with one question: about me, my craft, or the user, it goes in memory/core.md; about this codebase, it goes in the project's knowledge directory; something everyone should know, post it.
7803
+ Route every lesson with one question: about me, my craft, or the user, it goes in memory/core.md; about how this codebase behaves, it goes in the repo's AGENTS.md or README; about this project as a place (runners, access, flow, where results live), it goes in the project's knowledge; something everyone should know, post it.
7355
7804
  ` + (seed.length === 0 ? "" : `
7356
7805
  ${SEED_INSTRUCTIONS_HEADING}
7357
7806
 
@@ -7368,7 +7817,7 @@ ${seed}
7368
7817
  continue;
7369
7818
  }
7370
7819
  const file = this.paths.agentRole(agent.name);
7371
- const body = await exists(file) ? (await readMarkdown(file, z15.record(z15.string(), z15.unknown()))).body : "";
7820
+ const body = await exists(file) ? (await readMarkdown(file, z16.record(z16.string(), z16.unknown()))).body : "";
7372
7821
  const at = body.indexOf(`
7373
7822
  ${SEED_INSTRUCTIONS_HEADING}
7374
7823
  `);
@@ -7407,8 +7856,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7407
7856
  async ensureAgentProject(name, slug) {
7408
7857
  const dir = this.paths.agentProject(name, slug);
7409
7858
  await ensureDir(dir);
7410
- if (!await exists(path6.join(dir, "sessions.json"))) {
7411
- await writeJson(path6.join(dir, "sessions.json"), {});
7859
+ if (!await exists(path7.join(dir, "sessions.json"))) {
7860
+ await writeJson(path7.join(dir, "sessions.json"), {});
7412
7861
  }
7413
7862
  }
7414
7863
  /**
@@ -7440,7 +7889,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7440
7889
  const at = turn?.endedAt ?? turn?.startedAt;
7441
7890
  if (turn !== null && at !== void 0 && (lastTurnAt === void 0 || at > lastTurnAt)) {
7442
7891
  lastTurnAt = at;
7443
- 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)}`}`;
7444
7893
  lastModel = turn.model ?? void 0;
7445
7894
  }
7446
7895
  }
@@ -7660,7 +8109,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7660
8109
  const file = this.paths.societySkillFile(skill.name);
7661
8110
  const replaced = await exists(file);
7662
8111
  const taken = proposal.skillFiles;
7663
- const staged = path6.join(this.paths.proposalFiles(proposal.id), "SKILL.md");
8112
+ const staged = path7.join(this.paths.proposalFiles(proposal.id), "SKILL.md");
7664
8113
  if (skill.body === void 0 && (taken === void 0 || !await exists(staged))) {
7665
8114
  throw new BoardError("INVALID_STATE", `proposal ${proposal.id} holds no skill files`);
7666
8115
  }
@@ -7723,8 +8172,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7723
8172
  const dir = this.paths.proposalFiles(proposal.id);
7724
8173
  await rm2(dir, { recursive: true, force: true });
7725
8174
  for (const entry of taken.files) {
7726
- const target = path6.join(dir, ...entry.path.split("/"));
7727
- await ensureDir(path6.dirname(target));
8175
+ const target = path7.join(dir, ...entry.path.split("/"));
8176
+ await ensureDir(path7.dirname(target));
7728
8177
  await writeFile4(target, contents.get(entry.path) ?? Buffer.alloc(0));
7729
8178
  await chmod2(target, entry.executable ? 493 : 420);
7730
8179
  }
@@ -7782,7 +8231,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7782
8231
  const where = {
7783
8232
  scope: turn.project,
7784
8233
  ...turn.thread === void 0 ? {} : { thread: turn.thread },
7785
- ...turn.channel === void 0 ? {} : { channel: turn.channel }
8234
+ ...turn.channel === void 0 ? {} : { channel: turn.channel },
8235
+ ...turn.place === void 0 ? {} : { place: turn.place }
7786
8236
  };
7787
8237
  await this.mutex.run(async () => {
7788
8238
  for (const proposal of await this.listProposals()) {
@@ -7814,16 +8264,25 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7814
8264
  if (!isSafeRelativePath(entry.path)) {
7815
8265
  continue;
7816
8266
  }
7817
- const bytes = await readFile4(path6.join(dir, ...entry.path.split("/"))).catch(() => null);
7818
- const text = bytes === null || bytes.includes(0) ? null : bytes.toString("utf8");
8267
+ const bytes = await readFile4(path7.join(dir, ...entry.path.split("/"))).catch(() => null);
7819
8268
  files.push({
7820
8269
  ...entry,
7821
- text: text === null ? null : text.slice(0, SKILL_TEXT_LIMIT_CHARS),
7822
- truncated: text !== null && text.length > SKILL_TEXT_LIMIT_CHARS
8270
+ ...bytes === null ? { text: null, truncated: false } : skillFileText(bytes)
7823
8271
  });
7824
8272
  }
7825
8273
  return files;
7826
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
+ }
7827
8286
  async validateAddAgent(input) {
7828
8287
  if (await exists(this.paths.agentFile(input.name))) {
7829
8288
  throw new BoardError("ALREADY_EXISTS", `agent ${input.name} already exists`);
@@ -7954,7 +8413,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7954
8413
  this.assertNoTurnCut(actor, `${ref} cannot be archived`, (turn) => turn.channel === channel && turn.scope === scope || turn.thread !== void 0 && closing.has(turn.thread));
7955
8414
  const threadsClosed = [];
7956
8415
  for (const file of await listFiles(threads)) {
7957
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
8416
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
7958
8417
  if (doc.data.channel !== ref || doc.data.state !== "open") {
7959
8418
  continue;
7960
8419
  }
@@ -8060,7 +8519,8 @@ ${charter.purpose}
8060
8519
  }
8061
8520
  }
8062
8521
  /**
8063
- * 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
8064
8524
  * files, repository, and history stay. The notice is the board's, since the reason is free text
8065
8525
  * a proposer wrote and a board-authored mention would wake whoever it names.
8066
8526
  */
@@ -8085,7 +8545,7 @@ ${charter.purpose}
8085
8545
  const threads = this.paths.threads(slug);
8086
8546
  const threadsClosed = [];
8087
8547
  for (const file of await listFiles(threads)) {
8088
- const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
8548
+ const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
8089
8549
  if (doc.data.state !== "open") {
8090
8550
  continue;
8091
8551
  }
@@ -8103,7 +8563,7 @@ ${charter.purpose}
8103
8563
  });
8104
8564
  threadsClosed.push(doc.data.id);
8105
8565
  }
8106
- 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));
8107
8567
  const project = await this.updateProject(slug, (p) => ({
8108
8568
  ...p,
8109
8569
  members: [],
@@ -8160,10 +8620,10 @@ ${charter.purpose}
8160
8620
  ...project,
8161
8621
  members: project.members.filter((member) => member !== name)
8162
8622
  }));
8163
- const sessions = path6.join(this.paths.agentProject(name, slug), "sessions.json");
8623
+ const sessions = path7.join(this.paths.agentProject(name, slug), "sessions.json");
8164
8624
  if (await exists(sessions)) {
8165
8625
  const archived = `sessions.archived.${ts.replace(/[:.]/g, "-")}.json`;
8166
- await rename3(sessions, path6.join(this.paths.agentProject(name, slug), archived));
8626
+ await rename3(sessions, path7.join(this.paths.agentProject(name, slug), archived));
8167
8627
  }
8168
8628
  }
8169
8629
  this.tokenIndex.delete(current.tokenHash);
@@ -8243,6 +8703,17 @@ ${charter.purpose}
8243
8703
  return false;
8244
8704
  }
8245
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
+ }
8246
8717
  /**
8247
8718
  * A channel that takes new posts, threads, tasks, and followers: it exists, it is not archived,
8248
8719
  * and its project is not archived.
@@ -8493,7 +8964,7 @@ ${charter.purpose}
8493
8964
  if (since !== null && id <= since || upto !== void 0 && id > upto) {
8494
8965
  continue;
8495
8966
  }
8496
- const doc = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
8967
+ const doc = await readMarkdown(path7.join(dir, file), MessageFrontmatterSchema);
8497
8968
  messages.push({ ...doc.data, body: doc.body });
8498
8969
  }
8499
8970
  return messages;
@@ -8584,16 +9055,26 @@ function decideWake(input) {
8584
9055
  }
8585
9056
 
8586
9057
  // packages/scheduler/dist/scheduler.js
8587
- import { z as z16 } from "zod";
9058
+ import { z as z17 } from "zod";
8588
9059
  function besideFields(beside) {
8589
9060
  if (beside === void 0) {
8590
9061
  return {};
8591
9062
  }
8592
- 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
+ };
8593
9074
  }
8594
9075
  function conversationKey2(agent, scope, beside) {
8595
9076
  const fields = besideFields(beside);
8596
- 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 }));
8597
9078
  }
8598
9079
  var DEFAULT_TIMINGS = Object.freeze({
8599
9080
  pollMs: 1e3,
@@ -8611,21 +9092,21 @@ var DEFAULT_TIMINGS = Object.freeze({
8611
9092
  scaleCooldownMs: 36e5,
8612
9093
  reflectionMs: 24 * 36e5
8613
9094
  });
8614
- var SchedulerTimingsSchema = z16.object({
8615
- pollMs: z16.number().positive(),
8616
- debounceMs: z16.number().nonnegative(),
8617
- userDebounceMs: z16.number().nonnegative(),
8618
- heartbeatMs: z16.number().positive(),
8619
- waitingStageMs: z16.number().positive(),
8620
- leaseSweepMs: z16.number().positive(),
8621
- opsIntervalMs: z16.number().positive(),
8622
- signalRepeatMs: z16.number().positive(),
8623
- costReportMs: z16.number().positive(),
8624
- staleThreadMs: z16.number().positive(),
8625
- idleMemberMs: z16.number().positive(),
8626
- homeConflictMs: z16.number().positive(),
8627
- scaleCooldownMs: z16.number().positive(),
8628
- reflectionMs: z16.number().positive()
9095
+ var SchedulerTimingsSchema = z17.object({
9096
+ pollMs: z17.number().positive(),
9097
+ debounceMs: z17.number().nonnegative(),
9098
+ userDebounceMs: z17.number().nonnegative(),
9099
+ heartbeatMs: z17.number().positive(),
9100
+ waitingStageMs: z17.number().positive(),
9101
+ leaseSweepMs: z17.number().positive(),
9102
+ opsIntervalMs: z17.number().positive(),
9103
+ signalRepeatMs: z17.number().positive(),
9104
+ costReportMs: z17.number().positive(),
9105
+ staleThreadMs: z17.number().positive(),
9106
+ idleMemberMs: z17.number().positive(),
9107
+ homeConflictMs: z17.number().positive(),
9108
+ scaleCooldownMs: z17.number().positive(),
9109
+ reflectionMs: z17.number().positive()
8629
9110
  }).partial();
8630
9111
  function parseTimings(input) {
8631
9112
  const parsed = SchedulerTimingsSchema.parse(input);
@@ -8637,32 +9118,32 @@ function parseTimings(input) {
8637
9118
  }
8638
9119
  return overrides;
8639
9120
  }
8640
- var PendingSchema = z16.object({
9121
+ var PendingSchema = z17.object({
8641
9122
  dispatch: TurnDispatchSchema,
8642
- readyAt: z16.number(),
8643
- stageWakes: z16.array(TriggerSchema).optional(),
8644
- holder: z16.boolean().optional(),
9123
+ readyAt: z17.number(),
9124
+ stageWakes: z17.array(TriggerSchema).optional(),
9125
+ holder: z17.boolean().optional(),
8645
9126
  newestMessage: UlidSchema.optional(),
8646
- onlyMessages: z16.boolean().optional(),
9127
+ onlyMessages: z17.boolean().optional(),
8647
9128
  steeredThrough: UlidSchema.optional(),
8648
- onlyCrons: z16.boolean().optional()
9129
+ onlyCrons: z17.boolean().optional()
8649
9130
  });
8650
- var StateSchema = z16.object({
8651
- cursor: z16.string().nullable(),
8652
- lastHeartbeat: z16.record(z16.string(), z16.string()),
9131
+ var StateSchema = z17.object({
9132
+ cursor: z17.string().nullable(),
9133
+ lastHeartbeat: z17.record(z17.string(), z17.string()),
8653
9134
  /** Waiting stages already signalled, as `task:stage`, so each is logged once while it waits. */
8654
- waitingSeen: z16.record(z16.string(), z16.string()).default({}),
9135
+ waitingSeen: z17.record(z17.string(), z17.string()).default({}),
8655
9136
  /** Queued dispatches survive a restart; the cursor has already moved past the events that made them. */
8656
- pending: z16.record(z16.string(), PendingSchema).default({}),
8657
- opsReported: z16.record(z16.string(), z16.string()).default({}),
8658
- releaseCounts: z16.record(z16.string(), z16.number().int().nonnegative()).default({}),
8659
- lastOps: z16.string().nullable().default(null),
8660
- lastScaled: z16.record(z16.string(), z16.string()).default({}),
8661
- lastCostReport: z16.string().nullable().default(null),
8662
- costSinceReport: z16.number().nonnegative().default(0),
8663
- turnsSinceReport: z16.number().int().nonnegative().default(0),
9137
+ pending: z17.record(z17.string(), PendingSchema).default({}),
9138
+ opsReported: z17.record(z17.string(), z17.string()).default({}),
9139
+ releaseCounts: z17.record(z17.string(), z17.number().int().nonnegative()).default({}),
9140
+ lastOps: z17.string().nullable().default(null),
9141
+ lastScaled: z17.record(z17.string(), z17.string()).default({}),
9142
+ lastCostReport: z17.string().nullable().default(null),
9143
+ costSinceReport: z17.number().nonnegative().default(0),
9144
+ turnsSinceReport: z17.number().int().nonnegative().default(0),
8664
9145
  /** When each member last reflected, or was first seen; the cadence counts from here. */
8665
- lastReflection: z16.record(z16.string(), z16.string()).default({})
9146
+ lastReflection: z17.record(z17.string(), z17.string()).default({})
8666
9147
  });
8667
9148
  function messageOf(trigger) {
8668
9149
  return trigger.kind === "mention" || trigger.kind === "user_post" ? trigger.messageId : void 0;
@@ -9065,6 +9546,21 @@ var Scheduler = class {
9065
9546
  }
9066
9547
  return;
9067
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
+ }
9068
9564
  case "agent.left": {
9069
9565
  const name = stringOf(payload["name"]);
9070
9566
  const project = stringOf(payload["project"]);
@@ -9366,11 +9862,12 @@ var Scheduler = class {
9366
9862
  const scope = stringOf(payload["project"]);
9367
9863
  const thread = stringOf(payload["thread"]);
9368
9864
  const channel = stringOf(payload["channel"]);
9865
+ const place = stringOf(payload["place"]);
9369
9866
  const agent = name === null ? null : await this.tryReadAgent(name);
9370
9867
  if (agent === null || scope === null) {
9371
9868
  return;
9372
9869
  }
9373
- 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;
9374
9871
  if (beside === null) {
9375
9872
  return;
9376
9873
  }
@@ -9415,7 +9912,7 @@ var Scheduler = class {
9415
9912
  }
9416
9913
  return {
9417
9914
  scope,
9418
- 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 }
9419
9916
  };
9420
9917
  }
9421
9918
  /**
@@ -9433,14 +9930,23 @@ var Scheduler = class {
9433
9930
  }
9434
9931
  /**
9435
9932
  * The conversation a post wakes a citizen in, for a turn in `scope`: its thread's, else its
9436
- * 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.
9437
9936
  */
9438
9937
  async postedIn(scope, channel, threadId) {
9439
9938
  if (threadId !== void 0) {
9440
9939
  return { thread: await this.conversationOf(threadId, scope) };
9441
9940
  }
9442
- const name = channel === null ? void 0 : channelConversation(scope, channel);
9443
- 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 };
9444
9950
  }
9445
9951
  async tryReadAgent(name) {
9446
9952
  try {
@@ -9582,7 +10088,7 @@ var Scheduler = class {
9582
10088
  };
9583
10089
  for (const entry of unread) {
9584
10090
  if (entry.scope === project) {
9585
- 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;
9586
10092
  reasonsIn(beside).digestSize += entry.count;
9587
10093
  }
9588
10094
  }
@@ -9669,7 +10175,10 @@ var Scheduler = class {
9669
10175
  if (cron.thread !== void 0) {
9670
10176
  return { thread: await this.conversationOf(cron.thread, cron.scope) };
9671
10177
  }
9672
- 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 };
9673
10182
  }
9674
10183
  /** Whether a cron's wake waits in the queue or its turn is running. */
9675
10184
  cronQueuedOrRunning(id) {
@@ -10054,8 +10563,7 @@ var Scheduler = class {
10054
10563
  const attempt = steer({
10055
10564
  agent: dispatch.agent,
10056
10565
  project: dispatch.project,
10057
- ...dispatch.thread === void 0 ? {} : { thread: dispatch.thread.id },
10058
- ...dispatch.channel === void 0 ? {} : { channel: dispatch.channel }
10566
+ ...namedIn(dispatch)
10059
10567
  }).then((outcome) => {
10060
10568
  const current = this.pending.get(key);
10061
10569
  if (outcome !== "busy" && current !== void 0) {
@@ -10076,7 +10584,7 @@ var Scheduler = class {
10076
10584
  return false;
10077
10585
  }
10078
10586
  const { dispatch } = item;
10079
- 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)));
10080
10588
  return cursor !== null && cursor >= item.newestMessage;
10081
10589
  }
10082
10590
  async dispatchReady(now) {
@@ -10113,6 +10621,7 @@ var Scheduler = class {
10113
10621
  project: dispatch.project,
10114
10622
  thread: dispatch.thread?.id,
10115
10623
  channel: dispatch.channel,
10624
+ place: dispatch.place,
10116
10625
  trigger: dispatch.trigger.kind
10117
10626
  }, "dispatching turn");
10118
10627
  const crons = dispatch.crons ?? [];
@@ -10229,17 +10738,17 @@ import { randomUUID } from "node:crypto";
10229
10738
 
10230
10739
  // packages/turn-host/dist/render.js
10231
10740
  var TURN_CONTRACT = [
10232
- "- 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.",
10233
10742
  "- Act through the board tools (the `board` MCP server). Holding a stage is a lease; every turn that touches the task renews it.",
10234
- "- 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.",
10235
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.",
10236
10745
  "- Silence is allowed. If the digest needs no reply, post nothing.",
10237
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.",
10238
10747
  "- An @mention wakes the citizen named, and every wake costs a turn. Address someone with @ only when you need them to act; when you merely refer to citizens, write their names plainly. A name in a code span or a code block is no mention, so code keeps its @ as written. @user wakes nobody, since the user takes no turns; it is what puts a question before the user, so use it only when you need the user to answer or decide.",
10239
10748
  "- A thread is a conversation off a channel that reaches only its participants and whoever is mentioned in it. Every task has one, opened with it under the task's id on the channel of its project it was filed in, general unless it was filed in another: talk about a task there, with post_message and its thread_id, and the notes you give advance_task and update_task are posted there as you. Its participants are the task's creator, its stage holders and assignees, whoever may take the stage that waits, and anyone who posted; it closes when the task ends. Every proposal has one too, in governance, opened with its pitch and closed by its decision. Open other threads with open_thread on a channel with a title, for any other topic; close_thread posts your summary to its channel.",
10240
- "- To show an image in a post, such as a screenshot or a chart, commit the file to a task's branch and link it by its path in the task's worktree, as `![what it shows](<path>)`: the board shows a file on a task's branch in place once it is committed, at the latest when your turn ends. An image at an https address shows too. Never put an image or any other file into a message as base64 or a data: URL: the board does not show it, and every reader of the conversation gets it in full in their prompt.",
10241
- "- Route every lesson: about you, your craft, or the user, write it to memory/core.md in your home directory, and keep that file short by moving detail to memory/<topic>.md, your archive; a durable fact about a project's codebase or process goes through `write_knowledge` on that project; something the whole project should know now, post it to the project channel, and anything about one task, in the task's thread.",
10242
- '- A procedure you have followed twice is a skill: write it to skills/<name>/SKILL.md in your home, frontmatter with `name` and a one-line `description` and then the steps, and your skills index lists it from the next turn. Files the steps use, such as scripts/ and references/, go in the same folder, named in SKILL.md by paths relative to it. Propose it with kind "skill" when the whole society would use it.',
10749
+ "- To show an image in a post, such as a screenshot or a chart, save it anywhere you can read it, such as a folder from `mktemp -d`, call upload_image with its path, and put the markdown it returns in the post, `![what it shows](image:<id>)`, with what the image shows as its alt text: the board keeps the image, so it shows in any conversation after the file is gone. It takes a PNG, JPEG, GIF, or WebP of at most 8 MB from your worktree, your home, or the system temp folder. An image committed on a task's branch, linked by its path in the task's worktree, still shows in place, and so does one at an https address. Never put an image or any other file into a message as base64 or a data: URL: the board does not show it, and every reader of the conversation gets it in full in their prompt.",
10750
+ "- Route every lesson: about you, your craft, or the user, write it to memory/core.md in your home directory, and keep that file short by moving detail to memory/<topic>.md, your archive; how a codebase behaves (architecture, conventions, gotchas, how to build, test, and verify it) goes in its AGENTS.md or README, committed on a task's branch like any change to the code, or proposed in the task's thread or the project channel when it is outside the task at hand; a durable fact about a project as a place (runners, access, flow, where results live) goes through `write_knowledge` on that project; something the whole project should know now, post it to the project channel, and anything about one task, in the task's thread.",
10751
+ '- A procedure you have followed twice is a candidate for a skill, which it is when it makes a citizen better at a kind of work on any project: write it to skills/<name>/SKILL.md in your home, frontmatter with `name` and a one-line `description` and then the steps, and your skills index lists it from the next turn. Files the steps use, such as scripts/ and references/, go in the same folder, named in SKILL.md by paths relative to it. Propose it with kind "skill" when more than one citizen would use it and nothing in it is tied to one place; a skill only you use stays yours, however many projects you use it on.',
10243
10752
  "- To take a published skill for yourself, install it in a temporary folder and copy it into your home: in a folder from `mktemp -d`, `npx -y skills add <source> --skill <name> -a universal --copy -y` writes `.agents/skills/<name>/` and a lock file there, and you copy that skill folder to skills/<name>/ in your home, named in lowercase letters, digits, and hyphens. Never install one inside a worktree, where it would be committed into the project, or with -g, which writes to the runner's own home, shared by every citizen on it and listed in no skills index.",
10244
10753
  "- Report memoryUpdated: true in the status object whenever you changed memory/core.md or a skill, so a warm session restarts with the new instructions.",
10245
10754
  "- Other turns of yours may be running at the same time and share your home. Change a file there with your edit tool, which refuses a file changed since you read it, and never rewrite one whole from an earlier read, with a shell redirect or a script, or you erase what another of your turns wrote.",
@@ -10250,7 +10759,7 @@ var PLANNING = [
10250
10759
  "- A task is a plan of stages between open and done. Hold the current stage with claim_task and finish it with advance_task; the next stage becomes current, and past the last one the task is done.",
10251
10760
  "- Name stages by what gets done. Plan the next few steps rather than everything, and reshape with plan_task as the work teaches you.",
10252
10761
  "- Assign a stage to a role when anyone in it could do it, to a citizen only when it must be them, and to nobody when anyone in the project could. A citizen named on a stage must be a member of the task's project; add them first. A stage wakes everyone it is open to when it comes up, so the narrower the assignee, the fewer turns it costs.",
10253
- "- Reshape rather than force: another round is an inserted stage, and a wait on something outside the society is a stage named for what it waits on. When a check finds work unfinished, send the task back to an earlier stage with update_task.",
10762
+ "- Reshape rather than force: another round is an inserted stage, and a wait on something outside the society is a stage named for what it waits on. When a check finds work unfinished, the holder of the current stage sends the task back to an earlier stage with update_task, and so may the user, the steward, and the concierge; anyone else asks the concierge in the task's thread.",
10254
10763
  "- A task that cannot start until other tasks finish names them with update_task blocked_by. While any of them is in play, its stage wakes nobody, neither when it comes up nor on a heartbeat; once the last has ended, done or abandoned, the stage wakes as if it had just come up, or wakes its holder. Nobody needs to clear the block or mention anyone when they end; change blocked_by only when the plan changes.",
10255
10764
  "- A gated stage is an independent check: nobody who held an earlier stage of the task may hold it, and only the user, the steward, and the concierge may add, remove, move, reassign, or ungate it. Ask them in the task's thread when a gate should change.",
10256
10765
  "- Commit your work on the task's branch before you advance, and say what you did in the advance note; it is the handover the next holder reads in the task's thread."
@@ -10288,7 +10797,7 @@ function renderOnboardingPreamble(context2) {
10288
10797
  function renderSkillsIndex(skills) {
10289
10798
  if (skills.length === 0) {
10290
10799
  return [
10291
- "None yet. Write the first one when you notice yourself doing something the same way twice."
10800
+ "None yet. Something you have done the same way twice is a candidate for the first; the turn contract says when it is one."
10292
10801
  ];
10293
10802
  }
10294
10803
  return [
@@ -10304,7 +10813,7 @@ function renderInstructions(input) {
10304
10813
  if (norms.length > 0) {
10305
10814
  sections.push("", "## Society norms", "", norms);
10306
10815
  }
10307
- 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);
10308
10817
  if (input.onboarding !== void 0) {
10309
10818
  sections.push("", "## First turn", "", renderOnboardingPreamble(input.onboarding));
10310
10819
  }
@@ -10317,9 +10826,9 @@ var REFLECTION_INTRO = "This turn is for your memory; take no new work, and answ
10317
10826
  var RECONCILE = "- Reconcile first: merge each copy listed under Edits to reconcile in your home into the file beside it, then delete the copy.";
10318
10827
  var REFLECTION_STEPS = [
10319
10828
  "- Consolidate memory/core.md: keep it to lessons that still hold, about sixty lines at most, and move detail worth keeping to memory/<topic>.md in your home, which the board's search covers for you alone.",
10320
- '- Extract a skill: a procedure you have followed twice goes to skills/<name>/SKILL.md, frontmatter with `name` and a one-line `description` and then the steps, with any script or reference it uses in the same folder. Propose it with kind "skill" when other citizens would use it.',
10829
+ '- Extract a skill: a procedure you have followed twice is a candidate, and one that makes a citizen better at a kind of work on any project goes to skills/<name>/SKILL.md, frontmatter with `name` and a one-line `description` and then the steps, with any script or reference it uses in the same folder. Propose it with kind "skill" when more than one citizen would use it and nothing in it is tied to one place; one only you use stays yours.',
10321
10830
  "- Refresh profile.md: one paragraph on what you do well and what to send your way; the roster the front desk routes with is built from it.",
10322
- "- Share: a durable fact about this scope's codebase or process goes through write_knowledge; a norm the whole society should follow goes to the steward as a plain post in general.",
10831
+ "- Share: how a codebase behaves goes in its AGENTS.md or README through a task like any change to the code, so propose it in that task's thread or the project channel; a durable fact about this scope as a place (runners, access, flow, where results live) goes through write_knowledge; a norm the whole society should follow goes to the steward as a plain post in general.",
10323
10832
  "- Report memoryUpdated: true in the status object when core.md or a skill changed."
10324
10833
  ];
10325
10834
  var MAX_BODY_CHARS = 1500;
@@ -10458,7 +10967,7 @@ function buildTurnPrompt(input) {
10458
10967
  lines.push("Operations signals arrived; they are listed under Operations signals below. Decide whether a proposal is warranted, and stay silent if not.");
10459
10968
  }
10460
10969
  if (dispatch.trigger.kind === "stage") {
10461
- 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.");
10462
10971
  }
10463
10972
  if (dispatch.trigger.kind === "task_done") {
10464
10973
  lines.push("A task you created is done. Tell whoever asked for it, if anyone did.");
@@ -10477,8 +10986,11 @@ function buildTurnPrompt(input) {
10477
10986
  if (conversation === null && dispatch.channel !== void 0) {
10478
10987
  const ref = channelRef(dispatch.project === SOCIETY_SCOPE ? null : dispatch.project, dispatch.channel);
10479
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.`);
10480
10991
  } else if (conversation === null) {
10481
- 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.`);
10482
10994
  } else {
10483
10995
  const { thread, task } = conversation;
10484
10996
  const about = thread.subject === void 0 ? "a topic" : `${thread.subject.kind} ${thread.subject.id}`;
@@ -10513,7 +11025,9 @@ function buildTurnPrompt(input) {
10513
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.`);
10514
11026
  }
10515
11027
  if (last !== null && (last.exitReason === "error" || last.exitReason === "timeout")) {
10516
- 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.");
10517
11031
  }
10518
11032
  const society = input.societyView;
10519
11033
  if (society !== void 0 && society !== null) {
@@ -10543,7 +11057,7 @@ function buildTurnPrompt(input) {
10543
11057
  if (knowledge !== void 0 && knowledge !== null) {
10544
11058
  lines.push("", `## Knowledge of ${dispatch.project}`, "");
10545
11059
  if (knowledge.topics.length === 0) {
10546
- lines.push("None yet. A durable fact about this scope's codebase or process is worth a write_knowledge call.");
11060
+ lines.push("None yet. A durable fact about this place, such as its runners, access, flow, or where results live, is worth a write_knowledge call.");
10547
11061
  } else {
10548
11062
  lines.push(`Topics under ${knowledge.dir}; open one with your file tools when it is relevant.`, "");
10549
11063
  for (const topic of knowledge.topics) {
@@ -10609,6 +11123,13 @@ function capitalized(text) {
10609
11123
  }
10610
11124
 
10611
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
+ }
10612
11133
  var SILENT = { info() {
10613
11134
  }, warn() {
10614
11135
  }, error() {
@@ -10619,6 +11140,12 @@ var DEFAULT_RESIDENT_IDLE_MS = 10 * 6e4;
10619
11140
  var LEASE_RENEW_MS = 10 * 6e4;
10620
11141
  var USER_POST_TRIGGER2 = "user_post";
10621
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
+ }
10622
11149
  var MAX_SIGNALS = 30;
10623
11150
  var TurnHost = class {
10624
11151
  board;
@@ -10663,13 +11190,15 @@ var TurnHost = class {
10663
11190
  const actor = { name: agent.name, role: agent.role };
10664
11191
  const thread = dispatch.thread;
10665
11192
  const channel = thread === void 0 ? dispatch.channel : void 0;
10666
- 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 });
10667
11195
  const startedAt = this.now().toISOString();
10668
11196
  const base = {
10669
11197
  agent: agent.name,
10670
11198
  project: dispatch.project,
10671
11199
  ...thread === void 0 ? {} : { thread: thread.id },
10672
11200
  ...channel === void 0 ? {} : { channel },
11201
+ ...place === void 0 ? {} : { place },
10673
11202
  runner: runner.name,
10674
11203
  cli: agent.cli,
10675
11204
  session: null,
@@ -10694,23 +11223,25 @@ var TurnHost = class {
10694
11223
  const charter = await this.board.readRole(agent.role);
10695
11224
  const societyScope = dispatch.project === SOCIETY_SCOPE;
10696
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);
10697
11227
  const project = societyScope ? null : await this.board.readProject(dispatch.project);
10698
- const workspace = await this.workspaceOf(project, thread, channel);
11228
+ const workspace = await this.workspaceOf(project, thread, channel, place, task);
10699
11229
  const sessions = await this.board.readSessions(agent.name, dispatch.project, part);
10700
11230
  const recorded = sessions.runner === runner.name ? sessions[agent.cli] : void 0;
10701
11231
  const newSession = recorded === void 0;
10702
11232
  const lastTurn = await this.board.readLastTurn(agent.name, dispatch.project, part);
10703
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;
10704
11235
  const digest = await this.board.readDigest({
10705
11236
  ...actor,
10706
11237
  scope: dispatch.project,
10707
11238
  ...thread === void 0 ? {} : { thread: thread.id },
10708
- ...channel === void 0 ? {} : { channel }
11239
+ ...channel === void 0 ? {} : { channel },
11240
+ ...place === void 0 ? {} : { place }
10709
11241
  }, { advance: false, limit: thread === void 0 ? 50 : 100 });
10710
11242
  const threadSoFar = thread !== void 0 && newSession ? (await this.board.listThread(thread.id).catch(() => [])).slice(-100) : null;
10711
11243
  const messages = threadSoFar ?? digest.messages;
10712
11244
  const cursorAfter = threadSoFar?.at(-1)?.id ?? digest.cursor;
10713
- const task = taskId === void 0 ? null : await this.board.findTask(taskId).then((found) => found.task).catch(() => null);
10714
11245
  const held = task === null ? [] : (await this.board.heldClaims(agent.name)).filter((each) => each.id === task.id);
10715
11246
  const onboarding = dispatch.onboarding || newSession && part === void 0 ? {
10716
11247
  agentName: agent.name,
@@ -10769,7 +11300,7 @@ var TurnHost = class {
10769
11300
  fresh: threadSoFar !== null,
10770
11301
  ...threadRecord.state === "closed" ? { replyIn: await this.replyIn(threadRecord) } : {}
10771
11302
  };
10772
- 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;
10773
11304
  const society = await this.board.society();
10774
11305
  const crons = [];
10775
11306
  for (const id of dispatch.crons ?? []) {
@@ -10818,6 +11349,7 @@ var TurnHost = class {
10818
11349
  scope: dispatch.project,
10819
11350
  ...thread === void 0 ? {} : { thread: thread.id },
10820
11351
  ...channel === void 0 ? {} : { channel },
11352
+ ...place === void 0 ? {} : { place },
10821
11353
  session: recorded ?? null,
10822
11354
  costSoFarUsd,
10823
11355
  instructions,
@@ -10844,6 +11376,7 @@ var TurnHost = class {
10844
11376
  stopRequestedBy: null,
10845
11377
  held: held.map((each) => each.id),
10846
11378
  costSoFarUsd,
11379
+ resumeRetries,
10847
11380
  configuredModel: agent.model ?? null,
10848
11381
  transcript: [],
10849
11382
  coldToken: residentKey === null ? token : null,
@@ -10857,6 +11390,7 @@ var TurnHost = class {
10857
11390
  project: dispatch.project,
10858
11391
  thread: thread?.id,
10859
11392
  channel,
11393
+ place,
10860
11394
  runner: runner.name,
10861
11395
  session: recorded,
10862
11396
  newSession,
@@ -10893,8 +11427,7 @@ var TurnHost = class {
10893
11427
  turnId,
10894
11428
  agent: turn.job.agent,
10895
11429
  project: turn.job.scope,
10896
- thread: turn.job.thread,
10897
- channel: turn.job.channel,
11430
+ ...conversationOfJob(turn.job),
10898
11431
  messages: sent.messages
10899
11432
  }).catch((error) => {
10900
11433
  this.log.warn({ turnId, error: String(error) }, "could not record a steer");
@@ -10902,17 +11435,14 @@ var TurnHost = class {
10902
11435
  }
10903
11436
  }
10904
11437
  turn.transcript.push(entry);
10905
- this.onEvent?.(turn.job.agent, turn.job.scope, entry.event, {
10906
- thread: turn.job.thread,
10907
- channel: turn.job.channel
10908
- });
11438
+ this.onEvent?.(turn.job.agent, turn.job.scope, entry.event, conversationOfJob(turn.job));
10909
11439
  }
10910
11440
  }
10911
11441
  /** The turn in flight in a conversation, if any. */
10912
11442
  turnIn(agent, scope, conversation = {}) {
10913
11443
  for (const turn of this.open.values()) {
10914
11444
  const { job } = turn;
10915
- 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) {
10916
11446
  return { turnId: job.turnId, runner: turn.runner, cli: job.cli };
10917
11447
  }
10918
11448
  }
@@ -10923,8 +11453,7 @@ var TurnHost = class {
10923
11453
  return [...this.open.values()].map(({ job }) => ({
10924
11454
  agent: job.agent,
10925
11455
  scope: job.scope,
10926
- ...job.thread === void 0 ? {} : { thread: job.thread },
10927
- ...job.channel === void 0 ? {} : { channel: job.channel }
11456
+ ...conversationOfJob(job)
10928
11457
  }));
10929
11458
  }
10930
11459
  /** The turns in flight, for the interface: their conversations and where a mention reaches them. */
@@ -10933,8 +11462,7 @@ var TurnHost = class {
10933
11462
  turnId: turn.job.turnId,
10934
11463
  agent: turn.job.agent,
10935
11464
  scope: turn.job.scope,
10936
- ...turn.job.thread === void 0 ? {} : { thread: turn.job.thread },
10937
- ...turn.job.channel === void 0 ? {} : { channel: turn.job.channel },
11465
+ ...conversationOfJob(turn.job),
10938
11466
  cli: turn.job.cli,
10939
11467
  ...turn.record.trigger.channel === void 0 ? {} : { askedIn: turn.record.trigger.channel },
10940
11468
  runner: turn.runner
@@ -10954,12 +11482,7 @@ var TurnHost = class {
10954
11482
  return "busy";
10955
11483
  }
10956
11484
  const { job } = turn;
10957
- const digest = await this.board.readDigest({
10958
- ...turn.actor,
10959
- scope: job.scope,
10960
- ...job.thread === void 0 ? {} : { thread: job.thread },
10961
- ...job.channel === void 0 ? {} : { channel: job.channel }
10962
- }, {
11485
+ const digest = await this.board.readDigest({ ...turn.actor, scope: job.scope, ...conversationOfJob(job) }, {
10963
11486
  ...turn.delivered === null ? {} : { since_cursor: turn.delivered },
10964
11487
  advance: false,
10965
11488
  limit: 50
@@ -11019,6 +11542,8 @@ var TurnHost = class {
11019
11542
  }
11020
11543
  const { job } = turn;
11021
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);
11022
11547
  const finished = {
11023
11548
  ...turn.record,
11024
11549
  session: outcome.session,
@@ -11032,20 +11557,35 @@ var TurnHost = class {
11032
11557
  toolCalls: turn.transcript.filter((entry) => entry.event.type === "tool_call").length,
11033
11558
  model: outcome.model ?? turn.configuredModel,
11034
11559
  ...outcome.work === null ? {} : { work: outcome.work },
11035
- ...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 } : {}
11036
11564
  };
11037
11565
  const ended = await this.conversationEnded(job);
11038
11566
  const closing = turn.record.trigger.kind === "closing";
11039
11567
  const decided = closing && outcome.exitReason !== "error" && outcome.exitReason !== "timeout";
11040
11568
  const recorded = await this.recorded(turn, async () => {
11041
- 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) {
11042
11582
  await this.board.writeSession(job.agent, job.scope, job.cli, outcome.session, turn.runner, part);
11043
11583
  }
11044
11584
  if (outcome.exitReason === "completed" || outcome.exitReason === "blocked" || outcome.exitReason === "stopped") {
11045
11585
  await this.board.setDigestCursor(job.agent, job.scope, turn.delivered, part);
11046
11586
  await this.renewLeases(turn.actor, turn.held);
11047
11587
  if (outcome.status?.needsUserDecision === true) {
11048
- 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);
11049
11589
  }
11050
11590
  }
11051
11591
  await this.board.finishTurn(finished, turn.transcript);
@@ -11057,7 +11597,7 @@ var TurnHost = class {
11057
11597
  conversation
11058
11598
  });
11059
11599
  if (!again) {
11060
- 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`;
11061
11601
  await this.board.publishSignal({
11062
11602
  kind: "stuck_workspace",
11063
11603
  key: `stuck_workspace:${job.agent}:${job.scope}:${conversationPart(job) ?? ""}`,
@@ -11074,12 +11614,18 @@ var TurnHost = class {
11074
11614
  this.log.warn({ runner: turn.runner, cli: job.cli, error: String(error) }, "could not record a quota");
11075
11615
  });
11076
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
+ }
11077
11622
  if (recorded) {
11078
11623
  this.log.info({
11079
11624
  agent: job.agent,
11080
11625
  project: job.scope,
11081
11626
  thread: job.thread,
11082
11627
  channel: job.channel,
11628
+ place: job.place,
11083
11629
  exitReason: finished.exitReason
11084
11630
  }, "turn finished");
11085
11631
  await this.takeSkillFiles(finished);
@@ -11146,6 +11692,7 @@ var TurnHost = class {
11146
11692
  project: dispatch.project,
11147
11693
  ...dispatch.thread === void 0 ? {} : { thread: dispatch.thread.id },
11148
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 },
11149
11696
  runner,
11150
11697
  cli: agent?.cli ?? null,
11151
11698
  session: null,
@@ -11192,24 +11739,26 @@ var TurnHost = class {
11192
11739
  return token;
11193
11740
  }
11194
11741
  /**
11195
- * 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
11196
11744
  * place of its own, and the home in the citizen's own.
11197
11745
  */
11198
- async workspaceOf(project, thread, channel) {
11746
+ async workspaceOf(project, thread, channel, place, task) {
11199
11747
  const own = thread !== void 0 ? thread.task ? {} : { thread: thread.id } : channel === void 0 ? {} : { channel };
11200
11748
  if (project === null) {
11201
- return { kind: "home", ...own };
11749
+ return { kind: "home", ...own, ...place === void 0 ? {} : { place } };
11202
11750
  }
11203
11751
  const repo = projectRepo(project);
11204
11752
  if (thread?.task === true) {
11205
- 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 } };
11206
11755
  }
11207
- 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}`);
11208
11757
  return { kind: "project", repo, branches, ...own };
11209
11758
  }
11210
11759
  /**
11211
- * Whether a thread conversation's task or thread has ended, or a channel conversation's channel
11212
- * 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.
11213
11762
  */
11214
11763
  async conversationEnded(job) {
11215
11764
  const conversation = workspaceConversationOf(job);
@@ -11223,14 +11772,14 @@ var TurnHost = class {
11223
11772
  * How a closing turn's conversation ended, and where its citizen asks someone else to take over
11224
11773
  * work that is not its own: the thread's channel while it is open, else the place's general.
11225
11774
  */
11226
- async endingOf(scope, thread, task, channel) {
11775
+ async endingOf(scope, thread, task, channel, project) {
11227
11776
  const place = scope === SOCIETY_SCOPE ? null : scope;
11228
11777
  if (thread !== void 0) {
11229
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}`}`;
11230
11779
  return { how: closed, askIn: await this.replyIn(thread) };
11231
11780
  }
11232
11781
  if (channel === void 0) {
11233
- return null;
11782
+ return project === void 0 ? null : { how: `project ${project} was archived`, askIn: channelRef(place, "general") };
11234
11783
  }
11235
11784
  const ref = channelRef(place, channel);
11236
11785
  const archived = (await this.board.listChannels()).find((each) => each.ref === ref)?.archived;
@@ -11244,13 +11793,15 @@ var TurnHost = class {
11244
11793
  * the user, which is what puts it before them. One that mentioned the user nowhere asks in its
11245
11794
  * own thread when it was in one that is still open, so the answer comes back to the same
11246
11795
  * conversation, and otherwise in a thread of its own on its conversation's channel, or its scope's
11247
- * general for the home, opened as the citizen with its summary. One that proposed something the
11248
- * user decides has put its decision before the user already, where deciding it clears it; a
11249
- * 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.
11250
11800
  */
11251
- async askUser(actor, job, since, summary) {
11801
+ async askUser(actor, job, askedIn, since, summary) {
11252
11802
  const threadId = job.thread;
11253
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");
11254
11805
  try {
11255
11806
  if (await this.board.hasMentioned(actor.name, USER_NAME, since)) {
11256
11807
  return;
@@ -11268,7 +11819,7 @@ var TurnHost = class {
11268
11819
  return;
11269
11820
  }
11270
11821
  const thread = await this.board.openThread(actor, {
11271
- channel: channelRef(place, job.channel ?? "general"),
11822
+ channel,
11272
11823
  title: `${actor.name} asks for a decision: ${summary}`.slice(0, 200)
11273
11824
  });
11274
11825
  await this.board.postMessage(actor, {
@@ -11319,6 +11870,12 @@ var DEFAULT_GRACE_MS = 2 * 6e4;
11319
11870
  var DEFAULT_REQUEST_TIMEOUT_MS = 10 * 6e4;
11320
11871
  var CONTROL_TIMEOUT_MS = 15e3;
11321
11872
  var FILE_TIMEOUT_MS = 6e4;
11873
+ var IMAGE_REFUSALS = {
11874
+ missing: "NOT_FOUND",
11875
+ outside: "FORBIDDEN",
11876
+ not_image: "VALIDATION",
11877
+ too_big: "VALIDATION"
11878
+ };
11322
11879
  var SWEEP_TIMEOUT_MS = 2 * 6e4;
11323
11880
  var DEFAULT_ACK_TIMEOUT_MS = 3e4;
11324
11881
  var RunnerHub = class {
@@ -11578,7 +12135,8 @@ var RunnerHub = class {
11578
12135
  async steer(conversation) {
11579
12136
  const turn = this.host.turnIn(conversation.agent, conversation.project, {
11580
12137
  thread: conversation.thread,
11581
- channel: conversation.channel
12138
+ channel: conversation.channel,
12139
+ place: conversation.place
11582
12140
  });
11583
12141
  const seat = turn === null ? void 0 : this.seats.get(turn.runner);
11584
12142
  if (turn === null || seat === void 0 || seat.send === null) {
@@ -11795,11 +12353,11 @@ var RunnerHub = class {
11795
12353
  * NOT_FOUND when the project has no runner yet or the branch or the path does not exist, and
11796
12354
  * `RunnerAwayError` while its runner is away.
11797
12355
  */
11798
- async readBranch(project, branch, path10) {
12356
+ async readBranch(project, branch, path11) {
11799
12357
  const { seat, repo } = await this.projectSeat(project, "branchReads", "read files");
11800
- 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));
11801
12359
  if (file === null) {
11802
- throw new BoardError("NOT_FOUND", `${branch} has no ${path10 === "" ? "commit" : path10}`);
12360
+ throw new BoardError("NOT_FOUND", `${branch} has no ${path11 === "" ? "commit" : path11}`);
11803
12361
  }
11804
12362
  return file;
11805
12363
  }
@@ -11819,14 +12377,41 @@ var RunnerHub = class {
11819
12377
  * What a branch of a project's repository did to one file since it left the default branch, read
11820
12378
  * on the runner the project lives on, with the same refusals as `branchChanges`.
11821
12379
  */
11822
- async branchDiff(project, branch, path10) {
12380
+ async branchDiff(project, branch, path11) {
11823
12381
  const { seat, repo } = await this.projectSeat(project, "branchChanges", "list changes");
11824
- 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));
11825
12383
  if (diff === null) {
11826
- throw new BoardError("NOT_FOUND", `${branch} did not change ${path10}`);
12384
+ throw new BoardError("NOT_FOUND", `${branch} did not change ${path11}`);
11827
12385
  }
11828
12386
  return diff;
11829
12387
  }
12388
+ /**
12389
+ * The image file a citizen's turn names for `upload_image`, read on the runner that turn runs on,
12390
+ * which refuses a file outside the turn's worktree, its citizen's home, and the system temp
12391
+ * folder, and anything but a PNG, JPEG, GIF, or WebP within the size limit; each refusal comes
12392
+ * back as a `BoardError` in the runner's words.
12393
+ */
12394
+ async readTurnImage(actor, file) {
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);
12396
+ if (turn === void 0) {
12397
+ throw new BoardError("INVALID_STATE", `${actor.name} has no turn running in this conversation to read ${file} from`);
12398
+ }
12399
+ const seat = this.seats.get(turn.runner);
12400
+ if (seat === void 0 || seat.send === null) {
12401
+ throw new RunnerAwayError(`runner ${turn.runner}, where the turn runs, is not connected`);
12402
+ }
12403
+ if (!seat.hello.imageReads) {
12404
+ throw new BoardError("INVALID_STATE", `runner ${seat.name}, where the turn runs, cannot read images yet; upgrade it`);
12405
+ }
12406
+ const image = TurnImageSchema.nullable().parse(await this.request(seat, (request) => ({ type: "image", request, read: { turnId: turn.turnId, path: file } }), FILE_TIMEOUT_MS));
12407
+ if (image === null) {
12408
+ throw new BoardError("INVALID_STATE", `the turn is no longer running on runner ${seat.name}`);
12409
+ }
12410
+ if (image.kind === "refused") {
12411
+ throw new BoardError(IMAGE_REFUSALS[image.why], image.detail);
12412
+ }
12413
+ return Buffer.from(image.content, "base64");
12414
+ }
11830
12415
  /** The models a CLI offers, asked of a connected runner that has it; none when no runner has it. */
11831
12416
  /**
11832
12417
  * The connected runner a model list for `cli` comes from: the first of `prefer` that has the CLI,
@@ -12098,20 +12683,20 @@ async function handleMcpRequest(board2, actor, request, version) {
12098
12683
  import { Hono as Hono3 } from "hono";
12099
12684
  import { compress } from "hono/compress";
12100
12685
  import { streamSSE as streamSSE2 } from "hono/streaming";
12101
- import { z as z18, ZodError } from "zod";
12686
+ import { z as z19, ZodError } from "zod";
12102
12687
 
12103
12688
  // apps/server/dist/github-auth.js
12104
12689
  import { randomBytes as randomBytes2 } from "node:crypto";
12105
12690
  import { Hono } from "hono";
12106
- import { z as z17 } from "zod";
12691
+ import { z as z18 } from "zod";
12107
12692
  var SIGN_IN_TTL_MS = 30 * 24 * 36e5;
12108
12693
  var STATE_TTL_MS = 10 * 6e4;
12109
12694
  var MAX_STATES = 1e3;
12110
- var TokenAnswerSchema = z17.object({ access_token: z17.string().min(1) });
12111
- var GithubUserSchema = z17.object({
12112
- login: z17.string().min(1),
12113
- id: z17.number().int(),
12114
- avatar_url: z17.string().optional()
12695
+ var TokenAnswerSchema = z18.object({ access_token: z18.string().min(1) });
12696
+ var GithubUserSchema = z18.object({
12697
+ login: z18.string().min(1),
12698
+ id: z18.number().int(),
12699
+ avatar_url: z18.string().optional()
12115
12700
  });
12116
12701
  function githubAvatar(url) {
12117
12702
  try {
@@ -12307,7 +12892,7 @@ function rateLimit(options) {
12307
12892
  }
12308
12893
 
12309
12894
  // apps/server/dist/runner-routes.js
12310
- import path7 from "node:path";
12895
+ import path8 from "node:path";
12311
12896
  import { Hono as Hono2 } from "hono";
12312
12897
  import { streamSSE } from "hono/streaming";
12313
12898
 
@@ -12500,7 +13085,7 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
12500
13085
  const marker = `/homes/${agent}/git`;
12501
13086
  const at = c.req.path.indexOf(marker);
12502
13087
  return homeGit.serve({
12503
- root: path7.dirname(repository),
13088
+ root: path8.dirname(repository),
12504
13089
  agent,
12505
13090
  rest: at === -1 ? "" : c.req.path.slice(at + marker.length),
12506
13091
  request: c.req.raw,
@@ -12513,7 +13098,7 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
12513
13098
 
12514
13099
  // apps/server/dist/static.js
12515
13100
  import { readFile as readFile5, stat as stat4 } from "node:fs/promises";
12516
- import path8 from "node:path";
13101
+ import path9 from "node:path";
12517
13102
  var CONTENT_TYPES = {
12518
13103
  ".html": "text/html; charset=utf-8",
12519
13104
  ".js": "text/javascript; charset=utf-8",
@@ -12541,11 +13126,11 @@ function within2(root, pathname) {
12541
13126
  } catch {
12542
13127
  return null;
12543
13128
  }
12544
- const candidate = path8.resolve(root, `.${decoded}`);
12545
- 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;
12546
13131
  }
12547
13132
  function spaHandler(staticDir) {
12548
- const root = path8.resolve(staticDir);
13133
+ const root = path9.resolve(staticDir);
12549
13134
  return async (c) => {
12550
13135
  const { pathname } = new URL(c.req.url);
12551
13136
  if (pathname.startsWith("/api/")) {
@@ -12556,14 +13141,14 @@ function spaHandler(staticDir) {
12556
13141
  if (file !== null && await isFile(file)) {
12557
13142
  return c.body(await readFile5(file), 200, {
12558
13143
  ...headers,
12559
- "content-type": CONTENT_TYPES[path8.extname(file)] ?? "application/octet-stream",
13144
+ "content-type": CONTENT_TYPES[path9.extname(file)] ?? "application/octet-stream",
12560
13145
  "cache-control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache"
12561
13146
  });
12562
13147
  }
12563
13148
  if (pathname.startsWith("/assets/")) {
12564
13149
  return c.text("not found", 404, headers);
12565
13150
  }
12566
- const index = path8.join(root, "index.html");
13151
+ const index = path9.join(root, "index.html");
12567
13152
  if (!await isFile(index)) {
12568
13153
  return c.text("The interface is not built: run pnpm build:web.", 404);
12569
13154
  }
@@ -12586,19 +13171,19 @@ var ERROR_STATUS = {
12586
13171
  INVALID_STATE: 409
12587
13172
  };
12588
13173
  var EVENT_PAGE = 100;
12589
- var RetireBodySchema = z18.object({ reason: z18.string().min(1) });
12590
- var RunnerBodySchema = z18.object({ name: NameSchema });
12591
- var EnrollmentApprovalSchema = z18.object({ name: NameSchema });
12592
- var HomeRunnerBodySchema = z18.object({ runner: NameSchema.nullable() });
12593
- var ModelBodySchema = z18.object({ model: ModelNameSchema.nullable() });
12594
- var EffortBodySchema = z18.object({ effort: EffortSchema.nullable() });
12595
- var HistoryLimitSchema = z18.coerce.number().int().min(1).max(500);
12596
- var MessageIdsSchema = z18.string().transform((ids) => ids.split(",").filter((id) => id !== "")).pipe(z18.array(UlidSchema).min(1).max(MESSAGE_LOOKUP_LIMIT));
12597
- var TimezoneBodySchema = z18.object({ timezone: TimeZoneSchema });
12598
- var ChannelBodySchema = z18.object({
13174
+ var RetireBodySchema = z19.object({ reason: z19.string().min(1) });
13175
+ var RunnerBodySchema = z19.object({ name: NameSchema });
13176
+ var EnrollmentApprovalSchema = z19.object({ name: NameSchema });
13177
+ var HomeRunnerBodySchema = z19.object({ runner: NameSchema.nullable() });
13178
+ var ModelBodySchema = z19.object({ model: ModelNameSchema.nullable() });
13179
+ var EffortBodySchema = z19.object({ effort: EffortSchema.nullable() });
13180
+ var HistoryLimitSchema = z19.coerce.number().int().min(1).max(500);
13181
+ var MessageIdsSchema = z19.string().transform((ids) => ids.split(",").filter((id) => id !== "")).pipe(z19.array(UlidSchema).min(1).max(MESSAGE_LOOKUP_LIMIT));
13182
+ var TimezoneBodySchema = z19.object({ timezone: TimeZoneSchema });
13183
+ var ChannelBodySchema = z19.object({
12599
13184
  project: NameSchema.nullable().default(null),
12600
13185
  name: NameSchema,
12601
- purpose: z18.string().min(1)
13186
+ purpose: z19.string().min(1)
12602
13187
  });
12603
13188
  function bearer2(c) {
12604
13189
  const header = c.req.header("authorization") ?? "";
@@ -12729,6 +13314,7 @@ function createApp(deps) {
12729
13314
  });
12730
13315
  api.get("/society/knowledge", async (c) => c.json(await board2.listKnowledge(null)));
12731
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")))));
12732
13318
  api.post("/agents/:name/retire", async (c) => {
12733
13319
  const body = RetireBodySchema.parse(await c.req.json());
12734
13320
  const { tokenHash: _hash, ...agent } = await board2.retireAgent(c.get("actor"), {
@@ -12770,7 +13356,7 @@ function createApp(deps) {
12770
13356
  });
12771
13357
  api.put("/roles/:name", async (c) => {
12772
13358
  const charter = RoleCharterSchema.parse({
12773
- ...z18.record(z18.string(), z18.unknown()).parse(await c.req.json()),
13359
+ ...z19.record(z19.string(), z19.unknown()).parse(await c.req.json()),
12774
13360
  name: c.req.param("name")
12775
13361
  });
12776
13362
  return c.json(await board2.setRoleCharter(c.get("actor"), charter));
@@ -12809,6 +13395,15 @@ function createApp(deps) {
12809
13395
  return c.json(await deps.runners.branchDiff(task.project, `task/${task.id}`, BranchPathSchema.parse(c.req.param("path"))));
12810
13396
  });
12811
13397
  api.get("/tasks/:id/files/:path{.+}", (c) => taskFile(c, c.req.param("path")));
13398
+ api.get("/images/:id", async (c) => {
13399
+ const { type, bytes } = await board2.readImage(UlidSchema.parse(c.req.param("id")));
13400
+ return c.body(new Uint8Array(bytes), 200, {
13401
+ "content-type": IMAGE_MEDIA_TYPES[type],
13402
+ "cache-control": "private, max-age=31536000, immutable",
13403
+ "x-content-type-options": "nosniff",
13404
+ "content-security-policy": "default-src 'none'; sandbox"
13405
+ });
13406
+ });
12812
13407
  api.get("/threads", async (c) => c.json(await board2.listThreads()));
12813
13408
  api.get("/requests", async (c) => c.json(await board2.listRequests()));
12814
13409
  api.get("/threads/:id", async (c) => {
@@ -13163,7 +13758,10 @@ var TurnHub = class {
13163
13758
  get lastSeq() {
13164
13759
  return this.seq;
13165
13760
  }
13166
- /** 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
+ */
13167
13765
  push(agent, project, event, conversation = {}) {
13168
13766
  this.seq += 1;
13169
13767
  const item = {
@@ -13173,6 +13771,7 @@ var TurnHub = class {
13173
13771
  project,
13174
13772
  ...conversation.thread === void 0 ? {} : { thread: conversation.thread },
13175
13773
  ...conversation.channel === void 0 ? {} : { channel: conversation.channel },
13774
+ ...conversation.place === void 0 ? {} : { place: conversation.place },
13176
13775
  event
13177
13776
  };
13178
13777
  this.buffer.push(item);
@@ -13231,6 +13830,7 @@ var host = new TurnHost({
13231
13830
  });
13232
13831
  var runners = new RunnerHub({ board, host, version: VERSION, log });
13233
13832
  board.watchTurns(() => host.inFlight());
13833
+ board.readImagesWith((actor, file) => runners.readTurnImage(actor, file));
13234
13834
  var concurrency = config.concurrency ?? Number.POSITIVE_INFINITY;
13235
13835
  var timings = parseTimings(JSON.parse(process.env["STELLARIS_TIMINGS"] ?? "{}"));
13236
13836
  var scheduler = new Scheduler({ board, runner: runners, log, concurrency, timings });
@@ -13248,8 +13848,8 @@ var view = {
13248
13848
  return scheduler.activeSignals;
13249
13849
  }
13250
13850
  };
13251
- var webDir = process.env["STELLARIS_WEB_DIR"] ?? path9.resolve(import.meta.dirname, "../../web/dist");
13252
- 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);
13253
13853
  var app = createApp({
13254
13854
  board,
13255
13855
  version: VERSION,