@kubitnodes/stellaris 0.3.12 → 0.3.14

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 (52) hide show
  1. package/dist/cli/index.js +1149 -535
  2. package/dist/server/index.js +1426 -593
  3. package/package.json +2 -1
  4. package/web/dist/assets/{BackLink-BlJvN2RA.js → BackLink-giAdtos8.js} +1 -1
  5. package/web/dist/assets/{ChannelForms-CmFxBXNK.js → ChannelForms-b6eKogvP.js} +1 -1
  6. package/web/dist/assets/ChannelView-DXoJ5w3z.js +1 -0
  7. package/web/dist/assets/CitizenView-yOnQtNz4.js +2 -0
  8. package/web/dist/assets/{CitizensView-B8lVGSBD.js → CitizensView-Dbo47F82.js} +1 -1
  9. package/web/dist/assets/{CliIcon-CT5ljQDC.js → CliIcon-DxDvRRcX.js} +1 -1
  10. package/web/dist/assets/CronsView-DYx7dQcr.js +1 -0
  11. package/web/dist/assets/KnowledgeView-BXLj85OH.js +1 -0
  12. package/web/dist/assets/{Markdown-B5zEdYuO.js → Markdown-Dw04_ImA.js} +4 -4
  13. package/web/dist/assets/{MessageItem-DuxSZwn0.js → MessageItem-DnfMDxDB.js} +1 -1
  14. package/web/dist/assets/{MetricsView-D8D-y-sz.js → MetricsView-_h2TWeo8.js} +1 -1
  15. package/web/dist/assets/{NeedsYouView-Buwq8-_l.js → NeedsYouView-BI1-_bYI.js} +1 -1
  16. package/web/dist/assets/{Overview-CrQ8ybpq.js → Overview-BdZiHglw.js} +1 -1
  17. package/web/dist/assets/{Pane-CFoSKOo5.js → Pane-CODhA5zE.js} +1 -1
  18. package/web/dist/assets/ProjectView-CScK4F7F.js +1 -0
  19. package/web/dist/assets/ProposalView-BQ1rflqo.js +1 -0
  20. package/web/dist/assets/{ProposalsView-DjyZpZe0.js → ProposalsView-DSi2u6qO.js} +1 -1
  21. package/web/dist/assets/{RunnersView-2h5NWUuD.js → RunnersView-9E2qUSO5.js} +1 -1
  22. package/web/dist/assets/{SocietyView-BuaSb_3d.js → SocietyView-DEeUo4wb.js} +1 -1
  23. package/web/dist/assets/SubjectThread-DhGB5RPc.js +1 -0
  24. package/web/dist/assets/{TaskFileView-CeEzbSCL.js → TaskFileView-DQC_ZUdY.js} +2 -2
  25. package/web/dist/assets/TaskView-yVpUG83f.js +3 -0
  26. package/web/dist/assets/{TasksView-DCVU5oZn.js → TasksView-CJbJt5dM.js} +1 -1
  27. package/web/dist/assets/ThreadCard-DVAx3gGc.js +1 -0
  28. package/web/dist/assets/{ThreadForms-CTBaAJ57.js → ThreadForms-RnrV7V5X.js} +1 -1
  29. package/web/dist/assets/{ThreadView-zg8KqF3t.js → ThreadView-CMwiVZFq.js} +1 -1
  30. package/web/dist/assets/{citizen-CImM6WB6.js → citizen-aLkZ4owL.js} +1 -1
  31. package/web/dist/assets/{compose-BG_-Tp87.js → compose-D7DOz6kC.js} +1 -1
  32. package/web/dist/assets/format-BORdp4w-.js +55 -0
  33. package/web/dist/assets/{governance-cDTg2_iq.js → governance-WrDQawLN.js} +1 -1
  34. package/web/dist/assets/index-7uInG5D9.js +23 -0
  35. package/web/dist/assets/index-D02Wgo_C.css +2 -0
  36. package/web/dist/assets/{link-CFdt17Sp.js → link-1v5lugnf.js} +1 -1
  37. package/web/dist/assets/{tasks-C10zwkED.js → tasks-C-9dAYQV.js} +1 -1
  38. package/web/dist/assets/useMatch-CyeFfbcR.js +1 -0
  39. package/web/dist/assets/{useStickyScroll-BjqdEt_f.js → useStickyScroll-CE7dyz0Q.js} +1 -1
  40. package/web/dist/index.html +9 -9
  41. package/web/dist/assets/ChannelView-4s--IwKC.js +0 -1
  42. package/web/dist/assets/CitizenView-B5-LVVCG.js +0 -2
  43. package/web/dist/assets/KnowledgeView-DbI8DIZb.js +0 -1
  44. package/web/dist/assets/ProjectView-BBQg-ZxC.js +0 -1
  45. package/web/dist/assets/ProposalView-BjPHVxj4.js +0 -1
  46. package/web/dist/assets/SubjectThread-C1mHvHMI.js +0 -1
  47. package/web/dist/assets/TaskView-yL9hCM7G.js +0 -3
  48. package/web/dist/assets/ThreadCard-CXXf9qfA.js +0 -1
  49. package/web/dist/assets/format-BWr3zKWg.js +0 -55
  50. package/web/dist/assets/index-Cxqhrg2v.js +0 -23
  51. package/web/dist/assets/index-VVnGdAXc.css +0 -2
  52. package/web/dist/assets/useMatch-BpnmzT6s.js +0 -1
package/dist/cli/index.js CHANGED
@@ -9,7 +9,7 @@ import { Command } from "commander";
9
9
  import { mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rename as rename3, stat as stat2 } from "node:fs/promises";
10
10
  import path6 from "node:path";
11
11
  import { decodeTime, monotonicFactory } from "ulid";
12
- import { z as z12 } from "zod";
12
+ import { z as z13 } from "zod";
13
13
 
14
14
  // packages/shared/dist/ids.js
15
15
  import { z } from "zod";
@@ -60,7 +60,10 @@ var VERB_NAMES = [
60
60
  "remove_knowledge",
61
61
  "create_channel",
62
62
  "archive_channel",
63
- "update_dashboard"
63
+ "update_dashboard",
64
+ "create_cron",
65
+ "update_cron",
66
+ "remove_cron"
64
67
  ];
65
68
  var VerbNameSchema = z2.enum(VERB_NAMES);
66
69
  var CharterTriggerSchema = z2.enum(["user_post", "ops_event", "heartbeat"]);
@@ -107,7 +110,10 @@ var MEMBER_VERBS = [
107
110
  "remove_knowledge",
108
111
  "plan_task",
109
112
  "advance_task",
110
- "update_dashboard"
113
+ "update_dashboard",
114
+ "create_cron",
115
+ "update_cron",
116
+ "remove_cron"
111
117
  ];
112
118
  var GOVERNANCE_VERBS = ["approve", "reject"];
113
119
  var FRONT_DESK_VERBS = ["create_project"];
@@ -160,7 +166,126 @@ var SEED_ROLES = [
160
166
  ];
161
167
 
162
168
  // packages/shared/dist/board.js
169
+ import { z as z4 } from "zod";
170
+
171
+ // packages/shared/dist/crons.js
172
+ import { Cron as Pattern } from "croner";
163
173
  import { z as z3 } from "zod";
174
+ var CRON_FLOOR_MS = 15 * 6e4;
175
+ var FIRE_SLACK_MS = 6e4;
176
+ var FLOOR_LOOKAHEAD = 200;
177
+ var DEFAULT_TIME_ZONE = "UTC";
178
+ function canonicalTimeZone(zone) {
179
+ try {
180
+ return new Intl.DateTimeFormat("en-US", { timeZone: zone }).resolvedOptions().timeZone;
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+ var TimeZoneSchema = z3.string().trim().min(1).max(64).transform((zone, ctx) => {
186
+ const canonical = canonicalTimeZone(zone);
187
+ if (canonical === null) {
188
+ ctx.addIssue({
189
+ code: "custom",
190
+ message: `${zone} is not a time zone; name one as IANA does, such as Europe/Berlin, America/New_York, or UTC`
191
+ });
192
+ return z3.NEVER;
193
+ }
194
+ return canonical;
195
+ });
196
+ function pattern(expression, timeZone) {
197
+ return new Pattern(expression, { mode: "5-part", timezone: timeZone, paused: true });
198
+ }
199
+ function cronExpressionError(expression) {
200
+ try {
201
+ pattern(expression, DEFAULT_TIME_ZONE);
202
+ return null;
203
+ } catch (error) {
204
+ return error instanceof Error ? error.message : String(error);
205
+ }
206
+ }
207
+ var CronExpressionSchema = z3.string().trim().min(1).max(120).superRefine((expression, ctx) => {
208
+ const error = cronExpressionError(expression);
209
+ if (error !== null) {
210
+ ctx.addIssue({
211
+ code: "custom",
212
+ message: `not a five-field cron expression (minute, hour, day of month, month, day of week): ${error}`
213
+ });
214
+ }
215
+ });
216
+ var CronScheduleSchema = z3.union([
217
+ z3.object({ cron: CronExpressionSchema, timezone: TimeZoneSchema.optional() }),
218
+ z3.object({ at: IsoDateTimeSchema })
219
+ ]);
220
+ var CronRunSchema = z3.object({
221
+ turnId: UlidSchema,
222
+ at: IsoDateTimeSchema,
223
+ outcome: z3.enum(["completed", "failed", "stopped"])
224
+ });
225
+ var CronFrontmatterSchema = z3.object({
226
+ id: UlidSchema,
227
+ title: z3.string().trim().min(1).max(120),
228
+ agent: NameSchema,
229
+ scope: NameSchema,
230
+ thread: UlidSchema.optional(),
231
+ channel: NameSchema.optional(),
232
+ schedule: CronScheduleSchema,
233
+ createdBy: NameSchema,
234
+ createdAt: IsoDateTimeSchema,
235
+ /** When the current schedule took effect: set, changed, or resumed. It fires only after this. */
236
+ since: IsoDateTimeSchema,
237
+ paused: z3.object({ at: IsoDateTimeSchema, by: NameSchema }).optional(),
238
+ lastFiredAt: IsoDateTimeSchema.optional(),
239
+ /** When a time came while its previous fire's turn was still queued or running, so it passed. */
240
+ lastSkippedAt: IsoDateTimeSchema.optional(),
241
+ /** How the turn of its latest fire ended. */
242
+ lastRun: CronRunSchema.optional(),
243
+ /** Its turns that failed in a row, up to the latest. */
244
+ failures: z3.number().int().nonnegative().default(0),
245
+ ended: z3.object({ at: IsoDateTimeSchema, by: NameSchema, reason: z3.string().min(1) }).optional()
246
+ });
247
+ function isOneTime(schedule) {
248
+ return "at" in schedule;
249
+ }
250
+ function scheduleZone(schedule, societyZone) {
251
+ return isOneTime(schedule) ? societyZone : schedule.timezone ?? societyZone;
252
+ }
253
+ function fireTimes(schedule, societyZone, after, count) {
254
+ if (isOneTime(schedule)) {
255
+ const at = new Date(schedule.at);
256
+ return at > after && count > 0 ? [at] : [];
257
+ }
258
+ return pattern(schedule.cron, scheduleZone(schedule, societyZone)).nextRuns(count, after);
259
+ }
260
+ function shortestGapMs(schedule, societyZone, after) {
261
+ const times = fireTimes(schedule, societyZone, after, FLOOR_LOOKAHEAD);
262
+ let shortest = null;
263
+ for (let index = 1; index < times.length; index += 1) {
264
+ const gap = (times[index]?.getTime() ?? 0) - (times[index - 1]?.getTime() ?? 0);
265
+ shortest = shortest === null ? gap : Math.min(shortest, gap);
266
+ }
267
+ return shortest;
268
+ }
269
+ function nextFire(cron2, societyZone, floorMs = CRON_FLOOR_MS) {
270
+ if (cron2.ended !== void 0 || cron2.paused !== void 0) {
271
+ return null;
272
+ }
273
+ const fired = cron2.lastFiredAt === void 0 ? null : Date.parse(cron2.lastFiredAt);
274
+ const since = Date.parse(cron2.since);
275
+ if (isOneTime(cron2.schedule)) {
276
+ return fired !== null && fired >= since ? null : new Date(cron2.schedule.at);
277
+ }
278
+ const from = Math.max(since, fired === null ? since : fired + floorMs - FIRE_SLACK_MS, cron2.lastSkippedAt === void 0 ? since : Date.parse(cron2.lastSkippedAt));
279
+ return fireTimes(cron2.schedule, societyZone, new Date(from), 1)[0] ?? null;
280
+ }
281
+ function describeSchedule(schedule, societyZone) {
282
+ if (isOneTime(schedule)) {
283
+ return `once at ${schedule.at}`;
284
+ }
285
+ return schedule.timezone === void 0 ? `\`${schedule.cron}\` in the society's time zone, ${societyZone}` : `\`${schedule.cron}\` in ${schedule.timezone}`;
286
+ }
287
+
288
+ // packages/shared/dist/board.js
164
289
  var ASK_CHANNEL = "asks";
165
290
  var SOCIETY_CHANNELS = ["general", "governance", ASK_CHANNEL];
166
291
  var PROJECT_DEFAULT_CHANNELS = ["general"];
@@ -172,30 +297,32 @@ var SOCIETY_SCOPE = "society";
172
297
  function wakeScope(member, project2) {
173
298
  return project2 !== null && member.memberships.includes(project2) ? project2 : SOCIETY_SCOPE;
174
299
  }
175
- var CliKindSchema = z3.enum(["claude", "codex"]);
176
- var ArchivedChannelSchema = z3.object({
300
+ var CliKindSchema = z4.enum(["claude", "codex"]);
301
+ var ArchivedChannelSchema = z4.object({
177
302
  name: NameSchema,
178
303
  at: IsoDateTimeSchema,
179
304
  by: NameSchema,
180
- reason: z3.string().min(1)
305
+ reason: z4.string().min(1)
181
306
  });
182
- var SocietySchema = z3.object({
183
- name: z3.string().min(1),
184
- version: z3.literal(1),
307
+ var SocietySchema = z4.object({
308
+ name: z4.string().min(1),
309
+ version: z4.literal(1),
185
310
  createdAt: IsoDateTimeSchema,
186
311
  /** The channels that take posts; an archived one moves to `archivedChannels`. */
187
- channels: z3.array(NameSchema),
188
- archivedChannels: z3.array(ArchivedChannelSchema).default([])
312
+ channels: z4.array(NameSchema),
313
+ archivedChannels: z4.array(ArchivedChannelSchema).default([]),
314
+ /** The time zone crons are read in when they name none, and every prompt tells the time in; the user's to set. */
315
+ timezone: TimeZoneSchema.default(DEFAULT_TIME_ZONE)
189
316
  });
190
- var CompletionEffectSchema = z3.enum(["none", "merge", "ghpr"]);
317
+ var CompletionEffectSchema = z4.enum(["none", "merge", "ghpr"]);
191
318
  var GITHUB_REMOTE = [
192
319
  /^https:\/\/github\.com\/([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?$/,
193
320
  /^git@github\.com:([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?$/,
194
321
  /^ssh:\/\/git@github\.com\/([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?$/
195
322
  ];
196
323
  function githubRepository(remote) {
197
- for (const pattern of GITHUB_REMOTE) {
198
- const match = remote === null ? null : pattern.exec(remote);
324
+ for (const pattern2 of GITHUB_REMOTE) {
325
+ const match = remote === null ? null : pattern2.exec(remote);
199
326
  if (match?.[1] !== void 0 && match[2] !== void 0) {
200
327
  return { owner: match[1], name: match[2] };
201
328
  }
@@ -203,7 +330,7 @@ function githubRepository(remote) {
203
330
  return null;
204
331
  }
205
332
  var PULL_REQUEST_URL = /^https:\/\/github\.com\/([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+)\/pull\/([1-9]\d*)\/?$/;
206
- var PullRequestUrlSchema = z3.string().trim().regex(PULL_REQUEST_URL, "a pull request's address, https://github.com/<owner>/<repo>/pull/<number>");
333
+ var PullRequestUrlSchema = z4.string().trim().regex(PULL_REQUEST_URL, "a pull request's address, https://github.com/<owner>/<repo>/pull/<number>");
207
334
  function parsePullRequestUrl(url) {
208
335
  const match = PULL_REQUEST_URL.exec(url.trim());
209
336
  return match?.[1] === void 0 || match[2] === void 0 || match[3] === void 0 ? null : { owner: match[1], name: match[2], number: Number(match[3]) };
@@ -211,60 +338,65 @@ function parsePullRequestUrl(url) {
211
338
  function sameRepository(a, b) {
212
339
  return a.owner.toLowerCase() === b.owner.toLowerCase() && a.name.toLowerCase() === b.name.toLowerCase();
213
340
  }
214
- var MergeSubjectSchema = z3.string().trim().min(1).max(300).regex(/^[^\n]+$/, "a merge commit's subject is one line");
215
- var PullRequestLinkSchema = z3.object({
341
+ var MergeSubjectSchema = z4.string().trim().min(1).max(300).regex(/^[^\n]+$/, "a merge commit's subject is one line");
342
+ var PullRequestLinkSchema = z4.object({
216
343
  url: PullRequestUrlSchema,
217
- number: z3.number().int().positive(),
344
+ number: z4.number().int().positive(),
218
345
  subject: MergeSubjectSchema.optional(),
219
- body: z3.string().max(2e4).optional(),
346
+ body: z4.string().max(2e4).optional(),
220
347
  linkedBy: NameSchema,
221
- linkedAt: IsoDateTimeSchema
348
+ linkedAt: IsoDateTimeSchema,
349
+ /**
350
+ * Once its `ghpr` task was abandoned, how the board's closing of it on GitHub went: `ok` false
351
+ * leaves it open, for the reason `detail` gives.
352
+ */
353
+ closed: z4.object({ at: IsoDateTimeSchema, ok: z4.boolean(), detail: z4.string() }).optional()
222
354
  });
223
- var DefaultBranchSchema = z3.string().trim().min(1).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/, "a branch name is letters, digits, and . _ / -").refine((branch) => !branch.includes("..") && !branch.includes("//") && !branch.endsWith("/") && !branch.endsWith(".") && !branch.endsWith(".lock"), "not a branch name git takes").refine((branch) => !branch.startsWith("task/") && !branch.startsWith("agent/"), "task/ and agent/ hold the board's own branches");
224
- var StageIdSchema = z3.string().regex(/^s[1-9][0-9]*$/, "expected a stage id such as s1");
355
+ var DefaultBranchSchema = z4.string().trim().min(1).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/, "a branch name is letters, digits, and . _ / -").refine((branch) => !branch.includes("..") && !branch.includes("//") && !branch.endsWith("/") && !branch.endsWith(".") && !branch.endsWith(".lock"), "not a branch name git takes").refine((branch) => !branch.startsWith("task/") && !branch.startsWith("agent/"), "task/ and agent/ hold the board's own branches");
356
+ var StageIdSchema = z4.string().regex(/^s[1-9][0-9]*$/, "expected a stage id such as s1");
225
357
  var stageFields = {
226
- name: z3.string().trim().min(1).max(120),
358
+ name: z4.string().trim().min(1).max(120),
227
359
  role: NameSchema.optional(),
228
360
  agent: NameSchema.optional(),
229
- gate: z3.boolean().default(false)
361
+ gate: z4.boolean().default(false)
230
362
  };
231
363
  function oneAssignee(stage) {
232
364
  return stage.role === void 0 || stage.agent === void 0;
233
365
  }
234
366
  var ONE_ASSIGNEE = { message: "a stage names a role or an agent, not both" };
235
- var PlanStageSchema = z3.object(stageFields).refine(oneAssignee, ONE_ASSIGNEE);
236
- var PlanEditStageSchema = z3.object({ id: StageIdSchema.optional(), ...stageFields }).refine(oneAssignee, ONE_ASSIGNEE);
237
- var PlanSchema = z3.array(PlanStageSchema);
238
- var PlanEditSchema = z3.array(PlanEditStageSchema);
239
- var ProjectSchema = z3.object({
367
+ var PlanStageSchema = z4.object(stageFields).refine(oneAssignee, ONE_ASSIGNEE);
368
+ var PlanEditStageSchema = z4.object({ id: StageIdSchema.optional(), ...stageFields }).refine(oneAssignee, ONE_ASSIGNEE);
369
+ var PlanSchema = z4.array(PlanStageSchema);
370
+ var PlanEditSchema = z4.array(PlanEditStageSchema);
371
+ var ProjectSchema = z4.object({
240
372
  slug: NameSchema,
241
- name: z3.string().min(1),
242
- repo: z3.string().nullable(),
243
- defaultBranch: z3.string().min(1),
373
+ name: z4.string().min(1),
374
+ repo: z4.string().nullable(),
375
+ defaultBranch: z4.string().min(1),
244
376
  /** The channels that take posts; an archived one moves to `archivedChannels`. */
245
- channels: z3.array(NameSchema),
246
- archivedChannels: z3.array(ArchivedChannelSchema).default([]),
247
- members: z3.array(NameSchema),
248
- approvers: z3.array(NameSchema),
249
- requiredCapabilities: z3.array(z3.string()),
377
+ channels: z4.array(NameSchema),
378
+ archivedChannels: z4.array(ArchivedChannelSchema).default([]),
379
+ members: z4.array(NameSchema),
380
+ approvers: z4.array(NameSchema),
381
+ requiredCapabilities: z4.array(z4.string()),
250
382
  createdAt: IsoDateTimeSchema,
251
383
  onDone: CompletionEffectSchema.default("none"),
252
384
  /** The runner the project lives on, which holds its repository; set on the project's first turn. */
253
385
  runner: NameSchema.optional(),
254
386
  /** Set once the project is archived: nobody belongs to it, and nothing more is posted, filed, or joined there. */
255
- archived: z3.object({ at: IsoDateTimeSchema, by: NameSchema, reason: z3.string().min(1) }).optional()
387
+ archived: z4.object({ at: IsoDateTimeSchema, by: NameSchema, reason: z4.string().min(1) }).optional()
256
388
  });
257
- var TaskStepSchema = z3.object({
258
- action: z3.enum(["advanced", "returned", "abandoned", "landed", "reopened"]),
389
+ var TaskStepSchema = z4.object({
390
+ action: z4.enum(["advanced", "returned", "abandoned", "landed", "reopened"]),
259
391
  stage: StageIdSchema,
260
392
  to: StageIdSchema.nullable()
261
393
  });
262
- var ProposalStepSchema = z3.object({ action: z3.enum(["approved", "rejected"]) });
263
- var MessageStepSchema = z3.union([TaskStepSchema, ProposalStepSchema]);
394
+ var ProposalStepSchema = z4.object({ action: z4.enum(["approved", "rejected"]) });
395
+ var MessageStepSchema = z4.union([TaskStepSchema, ProposalStepSchema]);
264
396
  function isTaskStep(step) {
265
397
  return "stage" in step;
266
398
  }
267
- var MessageFrontmatterSchema = z3.object({
399
+ var MessageFrontmatterSchema = z4.object({
268
400
  id: UlidSchema,
269
401
  author: NameSchema,
270
402
  channel: ChannelRefSchema,
@@ -276,18 +408,18 @@ var MessageFrontmatterSchema = z3.object({
276
408
  /** On a post a task verb made in the task's thread, or a decision in a proposal's: the step it recorded. */
277
409
  step: MessageStepSchema.optional(),
278
410
  ts: IsoDateTimeSchema,
279
- mentions: z3.array(NameSchema)
411
+ mentions: z4.array(NameSchema)
280
412
  });
281
- var TaskStatusSchema = z3.enum(["open", "claimed", "done", "abandoned"]);
282
- var ThreadStateSchema = z3.enum(["open", "closed"]);
283
- var ThreadSubjectSchema = z3.object({
284
- kind: z3.enum(["task", "proposal"]),
413
+ var TaskStatusSchema = z4.enum(["open", "claimed", "done", "abandoned"]);
414
+ var ThreadStateSchema = z4.enum(["open", "closed"]);
415
+ var ThreadSubjectSchema = z4.object({
416
+ kind: z4.enum(["task", "proposal"]),
285
417
  id: UlidSchema
286
418
  });
287
- var ThreadFrontmatterSchema = z3.object({
419
+ var ThreadFrontmatterSchema = z4.object({
288
420
  id: UlidSchema,
289
421
  channel: ChannelRefSchema,
290
- title: z3.string().min(1).max(200),
422
+ title: z4.string().min(1).max(200),
291
423
  subject: ThreadSubjectSchema.optional(),
292
424
  state: ThreadStateSchema,
293
425
  openedBy: NameSchema,
@@ -295,28 +427,28 @@ var ThreadFrontmatterSchema = z3.object({
295
427
  closedBy: NameSchema.optional(),
296
428
  closedAt: IsoDateTimeSchema.optional()
297
429
  });
298
- var StageSchema = z3.object({
430
+ var StageSchema = z4.object({
299
431
  id: StageIdSchema,
300
- name: z3.string().min(1),
432
+ name: z4.string().min(1),
301
433
  role: NameSchema.optional(),
302
434
  agent: NameSchema.optional(),
303
- gate: z3.boolean().default(false),
435
+ gate: z4.boolean().default(false),
304
436
  /** Everyone who has held the stage, oldest first. */
305
- holders: z3.array(NameSchema).default([]),
437
+ holders: z4.array(NameSchema).default([]),
306
438
  completedBy: NameSchema.optional(),
307
439
  completedAt: IsoDateTimeSchema.optional()
308
440
  });
309
- var TaskReturnSchema = z3.object({
441
+ var TaskReturnSchema = z4.object({
310
442
  from: StageIdSchema,
311
443
  by: NameSchema,
312
444
  at: IsoDateTimeSchema
313
445
  });
314
- var TaskFrontmatterSchema = z3.object({
446
+ var TaskFrontmatterSchema = z4.object({
315
447
  id: UlidSchema,
316
448
  project: NameSchema,
317
449
  /** The channel of the project the task is filed in, where its thread hangs. */
318
450
  channel: NameSchema.default("general"),
319
- title: z3.string().min(1),
451
+ title: z4.string().min(1),
320
452
  status: TaskStatusSchema,
321
453
  createdBy: NameSchema,
322
454
  createdAt: IsoDateTimeSchema,
@@ -325,19 +457,19 @@ var TaskFrontmatterSchema = z3.object({
325
457
  claimedBy: NameSchema.optional(),
326
458
  leaseExpiresAt: IsoDateTimeSchema.optional(),
327
459
  parentId: UlidSchema.optional(),
328
- blockedBy: z3.array(UlidSchema),
329
- requiredCapabilities: z3.array(z3.string()),
330
- stages: z3.array(StageSchema).min(1),
460
+ blockedBy: z4.array(UlidSchema),
461
+ requiredCapabilities: z4.array(z4.string()),
462
+ stages: z4.array(StageSchema).min(1),
331
463
  stage: StageIdSchema,
332
464
  /** When the current stage last became current or lost its holder: the waiting clock. */
333
465
  stageSince: IsoDateTimeSchema,
334
466
  /** The highest stage number issued, so a removed stage's id is never reused. */
335
- stageSeq: z3.number().int().positive(),
467
+ stageSeq: z4.number().int().positive(),
336
468
  onDone: CompletionEffectSchema.default("none"),
337
469
  /** The pull request a `ghpr` task lands through. */
338
470
  pullRequest: PullRequestLinkSchema.optional(),
339
471
  /** The last stage is complete and the completion effect is running. */
340
- completing: z3.boolean().default(false),
472
+ completing: z4.boolean().default(false),
341
473
  /** The latest send-back while its rework lasts: cleared once the task reaches that stage again. */
342
474
  returned: TaskReturnSchema.optional()
343
475
  });
@@ -361,79 +493,79 @@ function mayHoldStage(member, task2) {
361
493
  }
362
494
  return !(stage.gate && task2.stages.slice(0, index).some((s) => s.holders.includes(member.name)));
363
495
  }
364
- var ModelNameSchema = z3.string().trim().min(1).max(120).regex(/^[\w.:/[\]-]+$/, "a model name is one word of letters, digits, and . _ : / - [ ]");
365
- var EffortSchema = z3.string().trim().min(1).max(32).regex(/^[a-z][a-z0-9_-]*$/, "an effort is one lowercase word, such as high");
366
- var EffortOptionSchema = z3.object({
496
+ var ModelNameSchema = z4.string().trim().min(1).max(120).regex(/^[\w.:/[\]-]+$/, "a model name is one word of letters, digits, and . _ : / - [ ]");
497
+ var EffortSchema = z4.string().trim().min(1).max(32).regex(/^[a-z][a-z0-9_-]*$/, "an effort is one lowercase word, such as high");
498
+ var EffortOptionSchema = z4.object({
367
499
  id: EffortSchema,
368
- description: z3.string().default("")
500
+ description: z4.string().default("")
369
501
  });
370
- var ModelOptionSchema = z3.object({
502
+ var ModelOptionSchema = z4.object({
371
503
  id: ModelNameSchema,
372
- name: z3.string(),
373
- description: z3.string().default(""),
374
- isDefault: z3.boolean().default(false),
375
- efforts: z3.array(EffortOptionSchema).default([]),
504
+ name: z4.string(),
505
+ description: z4.string().default(""),
506
+ isDefault: z4.boolean().default(false),
507
+ efforts: z4.array(EffortOptionSchema).default([]),
376
508
  defaultEffort: EffortSchema.optional()
377
509
  });
378
- var RunnerModelsSchema = z3.object({
510
+ var RunnerModelsSchema = z4.object({
379
511
  runner: NameSchema,
380
512
  cli: CliKindSchema,
381
- models: z3.array(ModelOptionSchema)
513
+ models: z4.array(ModelOptionSchema)
382
514
  });
383
- var AgentStatusSchema = z3.enum(["active", "retired"]);
384
- var AgentSchema = z3.object({
515
+ var AgentStatusSchema = z4.enum(["active", "retired"]);
516
+ var AgentSchema = z4.object({
385
517
  name: NameSchema,
386
518
  role: NameSchema,
387
519
  cli: CliKindSchema.nullable(),
388
- model: z3.string().optional(),
520
+ model: z4.string().optional(),
389
521
  /** The reasoning effort set for the agent's turns; the model's own default applies otherwise. */
390
- effort: z3.string().optional(),
522
+ effort: z4.string().optional(),
391
523
  /** The runner preferred for the agent's turns where nothing else decides; any runner with its CLI otherwise. */
392
524
  homeRunner: NameSchema.optional(),
393
- memberships: z3.array(NameSchema),
394
- subscriptions: z3.array(ChannelRefSchema),
525
+ memberships: z4.array(NameSchema),
526
+ subscriptions: z4.array(ChannelRefSchema),
395
527
  status: AgentStatusSchema,
396
528
  createdAt: IsoDateTimeSchema,
397
- tokenHash: z3.string().min(1),
529
+ tokenHash: z4.string().min(1),
398
530
  retiredAt: IsoDateTimeSchema.optional(),
399
- retiredReason: z3.string().optional()
531
+ retiredReason: z4.string().optional()
400
532
  });
401
- var MemberSchema = z3.object({
533
+ var MemberSchema = z4.object({
402
534
  name: NameSchema,
403
535
  role: NameSchema,
404
536
  cli: CliKindSchema.nullable(),
405
537
  homeRunner: NameSchema.optional(),
406
538
  status: AgentStatusSchema,
407
- resident: z3.boolean().default(false),
539
+ resident: z4.boolean().default(false),
408
540
  /** The model configured on the record, if any; the CLI's own default applies otherwise. */
409
- model: z3.string().optional(),
541
+ model: z4.string().optional(),
410
542
  /** The model the CLI reported on the citizen's last turn. */
411
- lastModel: z3.string().optional(),
543
+ lastModel: z4.string().optional(),
412
544
  /** The reasoning effort configured on the record, if any; the model's own default applies otherwise. */
413
- effort: z3.string().optional(),
545
+ effort: z4.string().optional(),
414
546
  /** The citizen's own skills, by name. */
415
- skills: z3.array(NameSchema).default([]),
416
- memberships: z3.array(NameSchema),
417
- subscriptions: z3.array(ChannelRefSchema),
418
- claimsHeld: z3.number().int().nonnegative().default(0),
419
- tasksDone: z3.number().int().nonnegative().default(0),
547
+ skills: z4.array(NameSchema).default([]),
548
+ memberships: z4.array(NameSchema),
549
+ subscriptions: z4.array(ChannelRefSchema),
550
+ claimsHeld: z4.number().int().nonnegative().default(0),
551
+ tasksDone: z4.number().int().nonnegative().default(0),
420
552
  lastTurnAt: IsoDateTimeSchema.optional(),
421
- lastTurnOutcome: z3.string().optional(),
553
+ lastTurnOutcome: z4.string().optional(),
422
554
  createdAt: IsoDateTimeSchema,
423
555
  retiredAt: IsoDateTimeSchema.optional()
424
556
  });
425
- var RunnerOsSchema = z3.enum(["linux", "windows", "darwin"]);
426
- var RunnerSchema = z3.object({
557
+ var RunnerOsSchema = z4.enum(["linux", "windows", "darwin"]);
558
+ var RunnerSchema = z4.object({
427
559
  name: NameSchema,
428
560
  os: RunnerOsSchema,
429
- clis: z3.array(CliKindSchema),
430
- capabilities: z3.array(z3.string()),
561
+ clis: z4.array(CliKindSchema),
562
+ capabilities: z4.array(z4.string()),
431
563
  /** The release the runner reported when it last registered; absent until it has. */
432
- version: z3.string().optional(),
433
- status: z3.enum(["connected", "disconnected"]),
564
+ version: z4.string().optional(),
565
+ status: z4.enum(["connected", "disconnected"]),
434
566
  lastSeen: IsoDateTimeSchema.optional()
435
567
  });
436
- var ProposalKindSchema = z3.enum([
568
+ var ProposalKindSchema = z4.enum([
437
569
  "role",
438
570
  "member",
439
571
  "channel",
@@ -442,43 +574,43 @@ var ProposalKindSchema = z3.enum([
442
574
  "skill",
443
575
  "archive"
444
576
  ]);
445
- var ProposalStatusSchema = z3.enum([
577
+ var ProposalStatusSchema = z4.enum([
446
578
  "proposed",
447
579
  "approved",
448
580
  "rejected",
449
581
  "provisioned",
450
582
  "retired"
451
583
  ]);
452
- var MemberProposalSchema = z3.object({
584
+ var MemberProposalSchema = z4.object({
453
585
  name: NameSchema,
454
586
  role: NameSchema,
455
587
  cli: CliKindSchema,
456
- model: z3.string().optional(),
588
+ model: z4.string().optional(),
457
589
  homeRunner: NameSchema.optional(),
458
- memberships: z3.array(NameSchema).default([]),
459
- subscriptions: z3.array(ChannelRefSchema).default([]),
460
- seedInstructions: z3.string().optional()
590
+ memberships: z4.array(NameSchema).default([]),
591
+ subscriptions: z4.array(ChannelRefSchema).default([]),
592
+ seedInstructions: z4.string().optional()
461
593
  });
462
- var ChannelProposalSchema = z3.object({
594
+ var ChannelProposalSchema = z4.object({
463
595
  project: NameSchema.nullable().default(null),
464
596
  name: NameSchema,
465
- purpose: z3.string().min(1)
597
+ purpose: z4.string().min(1)
466
598
  });
467
- var ReallocationProposalSchema = z3.object({
468
- description: z3.string().min(1)
599
+ var ReallocationProposalSchema = z4.object({
600
+ description: z4.string().min(1)
469
601
  });
470
- var RetirementProposalSchema = z3.object({
602
+ var RetirementProposalSchema = z4.object({
471
603
  agent: NameSchema,
472
- reason: z3.string().min(1)
604
+ reason: z4.string().min(1)
473
605
  });
474
- var ArchiveProposalSchema = z3.object({
606
+ var ArchiveProposalSchema = z4.object({
475
607
  project: NameSchema,
476
- reason: z3.string().min(1)
608
+ reason: z4.string().min(1)
477
609
  });
478
- var SkillProposalSchema = z3.object({
610
+ var SkillProposalSchema = z4.object({
479
611
  name: NameSchema,
480
- summary: z3.string().min(1).max(200),
481
- body: z3.string().min(1)
612
+ summary: z4.string().min(1).max(200),
613
+ body: z4.string().min(1)
482
614
  });
483
615
  var ProposalCharterSchemas = {
484
616
  role: RoleCharterSchema,
@@ -550,17 +682,17 @@ function describeCharter(kind, charter) {
550
682
  }
551
683
  return `${kind} ${JSON.stringify(charter)}`;
552
684
  }
553
- var SkillSchema = z3.object({
685
+ var SkillSchema = z4.object({
554
686
  name: NameSchema,
555
- summary: z3.string().default(""),
556
- scope: z3.enum(["own", "society"]),
557
- path: z3.string().min(1)
687
+ summary: z4.string().default(""),
688
+ scope: z4.enum(["own", "society"]),
689
+ path: z4.string().min(1)
558
690
  });
559
- var HomeConflictSchema = z3.object({
691
+ var HomeConflictSchema = z4.object({
560
692
  /** The copy, relative to the home. */
561
- path: z3.string().min(1),
693
+ path: z4.string().min(1),
562
694
  /** The file beside it, whose version the board kept. */
563
- file: z3.string().min(1),
695
+ file: z4.string().min(1),
564
696
  /** When the copy reached the board. */
565
697
  since: IsoDateTimeSchema
566
698
  });
@@ -568,200 +700,242 @@ var CONFLICT_COPY = /^(.+)\.conflict-[^/.]+$/;
568
700
  function conflictCopyOf(copy) {
569
701
  return CONFLICT_COPY.exec(copy)?.[1] ?? null;
570
702
  }
571
- var CommitIdSchema = z3.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/);
572
- var HomeFileChangeSchema = z3.object({
573
- path: z3.string().min(1),
574
- status: z3.enum(["added", "modified", "deleted"]),
703
+ var CommitIdSchema = z4.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/);
704
+ var HomeFileChangeSchema = z4.object({
705
+ path: z4.string().min(1),
706
+ status: z4.enum(["added", "modified", "deleted"]),
575
707
  /** Lines added and removed, or null for a binary file. */
576
- added: z3.number().int().nonnegative().nullable(),
577
- removed: z3.number().int().nonnegative().nullable()
708
+ added: z4.number().int().nonnegative().nullable(),
709
+ removed: z4.number().int().nonnegative().nullable()
578
710
  });
579
- var HomeChangeSchema = z3.object({
711
+ var HomeChangeSchema = z4.object({
580
712
  commit: CommitIdSchema,
581
713
  at: IsoDateTimeSchema,
582
- kind: z3.enum(["turn", "board", "merge"]),
583
- author: z3.string(),
714
+ kind: z4.enum(["turn", "board", "merge"]),
715
+ author: z4.string(),
584
716
  /** The turn that made it, from the runner's commit message. */
585
717
  turnId: UlidSchema.optional(),
586
- subject: z3.string(),
587
- files: z3.array(HomeFileChangeSchema)
718
+ subject: z4.string(),
719
+ files: z4.array(HomeFileChangeSchema)
588
720
  });
589
- var HomeHistorySchema = z3.object({
590
- changes: z3.array(HomeChangeSchema),
721
+ var HomeHistorySchema = z4.object({
722
+ changes: z4.array(HomeChangeSchema),
591
723
  /** Whether older commits remain past the ones read. */
592
- more: z3.boolean()
724
+ more: z4.boolean()
593
725
  });
594
726
  var HomeFileDiffSchema = HomeFileChangeSchema.pick({ path: true, status: true }).extend({
595
- patch: z3.string().nullable(),
727
+ patch: z4.string().nullable(),
596
728
  /** The patch was cut at its size limit. */
597
- truncated: z3.boolean()
729
+ truncated: z4.boolean()
598
730
  });
599
- var KnowledgeSchema = z3.object({
731
+ var KnowledgeSchema = z4.object({
600
732
  topic: NameSchema,
601
733
  project: NameSchema.nullable(),
602
734
  updatedBy: NameSchema,
603
735
  updatedAt: IsoDateTimeSchema
604
736
  });
605
- var RemovedKnowledgeSchema = z3.object({
737
+ var RemovedKnowledgeSchema = z4.object({
606
738
  topic: NameSchema,
607
739
  project: NameSchema.nullable(),
608
740
  removedBy: NameSchema,
609
741
  removedAt: IsoDateTimeSchema
610
742
  });
611
- var DashboardSchema = z3.object({
743
+ var DashboardSchema = z4.object({
612
744
  project: NameSchema,
613
- revision: z3.number().int().min(0),
745
+ revision: z4.number().int().min(0),
614
746
  /** Absent until someone updates the dashboard the project began with. */
615
747
  updatedBy: NameSchema.optional(),
616
748
  updatedAt: IsoDateTimeSchema.optional(),
617
- body: z3.string()
749
+ body: z4.string()
618
750
  });
619
- var ProposalFrontmatterSchema = z3.object({
751
+ var ProposalOriginSchema = z4.object({
752
+ scope: NameSchema,
753
+ thread: UlidSchema.optional(),
754
+ channel: NameSchema.optional()
755
+ });
756
+ var ProposalFrontmatterSchema = z4.object({
620
757
  id: UlidSchema,
621
758
  kind: ProposalKindSchema,
622
759
  proposedBy: NameSchema,
623
760
  status: ProposalStatusSchema,
624
761
  createdAt: IsoDateTimeSchema,
762
+ /** Absent for a proposal filed outside a turn, or before proposals recorded it. */
763
+ filedFrom: ProposalOriginSchema.optional(),
625
764
  decidedBy: NameSchema.optional(),
626
765
  decidedAt: IsoDateTimeSchema.optional(),
627
- reason: z3.string().optional(),
628
- charter: z3.record(z3.string(), z3.unknown()),
766
+ reason: z4.string().optional(),
767
+ charter: z4.record(z4.string(), z4.unknown()),
629
768
  /** What approval created, for kinds the board provisions: the agent, channel, role, or retirement. */
630
769
  provisionedAt: IsoDateTimeSchema.optional(),
631
- provision: z3.record(z3.string(), z3.unknown()).optional()
770
+ provision: z4.record(z4.string(), z4.unknown()).optional()
632
771
  });
633
- var DecisionSchema = z3.object({
772
+ var DecisionSchema = z4.object({
634
773
  id: UlidSchema,
635
774
  proposalId: UlidSchema,
636
775
  decidedBy: NameSchema,
637
- outcome: z3.enum(["approved", "rejected"]),
638
- reason: z3.string().optional(),
776
+ outcome: z4.enum(["approved", "rejected"]),
777
+ reason: z4.string().optional(),
639
778
  ts: IsoDateTimeSchema
640
779
  });
641
780
 
642
781
  // packages/shared/dist/verbs.js
643
- import { z as z4 } from "zod";
782
+ import { z as z5 } from "zod";
783
+ var MomentSchema = z5.iso.datetime({ offset: true });
784
+ var CronTitleSchema = z5.string().trim().min(1).max(120);
785
+ var CronNoteSchema = z5.string().trim().min(1).max(4e3);
644
786
  var VerbInputs = {
645
- post_message: z4.object({
787
+ post_message: z5.object({
646
788
  /** Required outside a thread; in one it may be left out, and must otherwise be the thread's. */
647
789
  channel: ChannelRefSchema.optional(),
648
- body: z4.string().min(1),
790
+ body: z5.string().min(1),
649
791
  thread_id: UlidSchema.optional()
650
792
  }),
651
- read_inbox: z4.object({
793
+ read_inbox: z5.object({
652
794
  since_cursor: UlidSchema.nullable().optional(),
653
- limit: z4.number().int().positive().max(200).default(50),
654
- advance: z4.boolean().default(true)
795
+ limit: z5.number().int().positive().max(200).default(50),
796
+ advance: z5.boolean().default(true)
655
797
  }),
656
- search: z4.object({
657
- query: z4.string().min(1),
798
+ search: z5.object({
799
+ query: z5.string().min(1),
658
800
  project: NameSchema.optional(),
659
801
  channel: ChannelRefSchema.optional(),
660
- limit: z4.number().int().positive().max(100).default(20)
802
+ limit: z5.number().int().positive().max(100).default(20)
661
803
  }),
662
804
  /** On a task or a proposal, whose id the thread takes, or on a channel with a title. */
663
- open_thread: z4.object({
805
+ open_thread: z5.object({
664
806
  task_id: UlidSchema.optional(),
665
807
  proposal_id: UlidSchema.optional(),
666
808
  channel: ChannelRefSchema.optional(),
667
- title: z4.string().min(1).max(200).optional()
809
+ title: z5.string().min(1).max(200).optional()
668
810
  }),
669
- close_thread: z4.object({ thread_id: UlidSchema, summary: z4.string().min(1) }),
670
- create_task: z4.object({
811
+ close_thread: z5.object({ thread_id: UlidSchema, summary: z5.string().min(1) }),
812
+ create_task: z5.object({
671
813
  project: NameSchema,
672
- title: z4.string().min(1).max(200),
673
- body: z4.string().default(""),
814
+ title: z5.string().min(1).max(200),
815
+ body: z5.string().default(""),
674
816
  parent_id: UlidSchema.optional(),
675
- required_capabilities: z4.array(z4.string()).default([]),
817
+ required_capabilities: z5.array(z5.string()).default([]),
676
818
  /** The plan; without it the task gets one stage, work, that anyone in the project may take. */
677
- stages: z4.array(PlanStageSchema).min(1).optional(),
819
+ stages: z5.array(PlanStageSchema).min(1).optional(),
678
820
  /** A channel of the project the task belongs to, such as a release's; its thread opens there. */
679
821
  channel: NameSchema.default("general")
680
822
  }),
681
- claim_task: z4.object({ task_id: UlidSchema }),
682
- release_task: z4.object({ task_id: UlidSchema }),
683
- update_task: z4.object({
823
+ claim_task: z5.object({ task_id: UlidSchema }),
824
+ release_task: z5.object({ task_id: UlidSchema }),
825
+ update_task: z5.object({
684
826
  task_id: UlidSchema,
685
827
  /** Move the task back to this earlier stage. */
686
828
  stage: StageIdSchema.optional(),
687
- status: z4.enum(["abandoned"]).optional(),
829
+ status: z5.enum(["abandoned"]).optional(),
688
830
  /** Posted to the task's thread as the caller, marked with the step when the task moved. */
689
- note: z4.string().min(1).optional(),
690
- blocked_by: z4.array(UlidSchema).optional(),
831
+ note: z5.string().min(1).optional(),
832
+ blocked_by: z5.array(UlidSchema).optional(),
691
833
  /**
692
834
  * The pull request a `ghpr` task lands through, on its project's repository, with the merge
693
835
  * commit's subject and body; null unlinks it.
694
836
  */
695
- pull_request: z4.object({
837
+ pull_request: z5.object({
696
838
  url: PullRequestUrlSchema,
697
839
  merge_subject: MergeSubjectSchema.optional(),
698
- merge_body: z4.string().max(2e4).optional()
840
+ merge_body: z5.string().max(2e4).optional()
699
841
  }).nullable().optional()
700
842
  }),
701
- get_task: z4.object({ task_id: UlidSchema }),
702
- subscribe: z4.object({ channel: ChannelRefSchema }),
703
- unsubscribe: z4.object({ channel: ChannelRefSchema }),
704
- propose: z4.object({
843
+ get_task: z5.object({ task_id: UlidSchema }),
844
+ subscribe: z5.object({ channel: ChannelRefSchema }),
845
+ unsubscribe: z5.object({ channel: ChannelRefSchema }),
846
+ propose: z5.object({
705
847
  kind: ProposalKindSchema,
706
- charter: z4.record(z4.string(), z4.unknown()),
707
- rationale: z4.string().default("")
848
+ charter: z5.record(z5.string(), z5.unknown()),
849
+ rationale: z5.string().default("")
708
850
  }),
709
- approve: z4.object({ proposal_id: UlidSchema, reason: z4.string().optional() }),
710
- reject: z4.object({ proposal_id: UlidSchema, reason: z4.string().min(1) }),
711
- create_project: z4.object({
851
+ approve: z5.object({ proposal_id: UlidSchema, reason: z5.string().optional() }),
852
+ reject: z5.object({ proposal_id: UlidSchema, reason: z5.string().min(1) }),
853
+ create_project: z5.object({
712
854
  slug: NameSchema,
713
- name: z4.string().min(1).optional(),
714
- repo: z4.string().min(1).nullable().default(null),
855
+ name: z5.string().min(1).optional(),
856
+ repo: z5.string().min(1).nullable().default(null),
715
857
  default_branch: DefaultBranchSchema.default("main"),
716
858
  on_done: CompletionEffectSchema.optional()
717
859
  }),
718
- join_project: z4.object({ project: NameSchema, agent: NameSchema.optional() }),
719
- leave_project: z4.object({ project: NameSchema, agent: NameSchema.optional() }),
720
- write_knowledge: z4.object({
860
+ join_project: z5.object({ project: NameSchema, agent: NameSchema.optional() }),
861
+ leave_project: z5.object({ project: NameSchema, agent: NameSchema.optional() }),
862
+ write_knowledge: z5.object({
721
863
  /** A project you belong to, or null for society knowledge, which the steward and the user curate. */
722
864
  project: NameSchema.nullable().default(null),
723
865
  topic: NameSchema,
724
- body: z4.string().min(1)
866
+ body: z5.string().min(1)
725
867
  }),
726
- remove_knowledge: z4.object({
868
+ remove_knowledge: z5.object({
727
869
  /** As for write_knowledge: a project you belong to, or null for society knowledge. */
728
870
  project: NameSchema.nullable().default(null),
729
871
  topic: NameSchema
730
872
  }),
731
- plan_task: z4.object({
873
+ plan_task: z5.object({
732
874
  task_id: UlidSchema,
733
- stages: z4.array(PlanEditStageSchema),
875
+ stages: z5.array(PlanEditStageSchema),
734
876
  on_done: CompletionEffectSchema.optional()
735
877
  }),
736
878
  /** The note is posted to the task's thread as the caller, marked with the step. */
737
- advance_task: z4.object({ task_id: UlidSchema, note: z4.string().min(1).optional() }),
738
- configure_project: z4.object({
879
+ advance_task: z5.object({ task_id: UlidSchema, note: z5.string().min(1).optional() }),
880
+ configure_project: z5.object({
739
881
  project: NameSchema,
740
882
  on_done: CompletionEffectSchema.optional(),
741
883
  /** A new display name; the slug, which everything refers to the project by, never changes. */
742
- name: z4.string().trim().min(1).max(120).optional(),
884
+ name: z5.string().trim().min(1).max(120).optional(),
743
885
  /** The branch tasks start from and land on from now; tasks in play keep their branches. */
744
886
  default_branch: DefaultBranchSchema.optional()
745
887
  }),
746
- archive_project: z4.object({ project: NameSchema, reason: z4.string().min(1) }),
747
- create_channel: z4.object({
888
+ archive_project: z5.object({ project: NameSchema, reason: z5.string().min(1) }),
889
+ create_channel: z5.object({
748
890
  /** A project, or null for a channel of the society. */
749
891
  project: NameSchema.nullable().default(null),
750
892
  name: NameSchema,
751
- purpose: z4.string().min(1)
893
+ purpose: z5.string().min(1)
752
894
  }),
753
- archive_channel: z4.object({ channel: ChannelRefSchema, reason: z4.string().min(1) }),
754
- update_dashboard: z4.object({
895
+ archive_channel: z5.object({ channel: ChannelRefSchema, reason: z5.string().min(1) }),
896
+ update_dashboard: z5.object({
755
897
  project: NameSchema,
756
898
  /** The whole dashboard as markdown; the board keeps its frontmatter. */
757
- body: z4.string().min(1),
899
+ body: z5.string().min(1),
758
900
  /** The `revision` in the frontmatter of the dashboard you read, 0 when it names none. */
759
- revision: z4.number().int().min(0)
760
- })
901
+ revision: z5.number().int().min(0)
902
+ }),
903
+ create_cron: z5.object({
904
+ title: CronTitleSchema,
905
+ /** What to do when it fires; quoted in the prompt of every turn it starts. */
906
+ note: CronNoteSchema,
907
+ /** Minute, hour, day of month, month, day of week, such as `0 9 * * 1-5`; or `at` instead. */
908
+ cron: CronExpressionSchema.optional(),
909
+ /** The IANA time zone `cron` is read in; the society's when left out. */
910
+ timezone: TimeZoneSchema.optional(),
911
+ /** One time to fire, instead of `cron`; the cron ends once it has fired. */
912
+ at: MomentSchema.optional(),
913
+ /** The citizen it wakes: yourself when left out. */
914
+ agent: NameSchema.optional(),
915
+ /** Where it fires: a project the citizen belongs to, or `society`; this turn's scope when left out. */
916
+ project: NameSchema.optional(),
917
+ /** An open thread there, whose conversation it fires in. */
918
+ thread_id: UlidSchema.optional(),
919
+ /** An open channel of that place other than general, whose conversation it fires in. */
920
+ channel: NameSchema.optional()
921
+ }),
922
+ update_cron: z5.object({
923
+ cron_id: UlidSchema,
924
+ /** Pause it, or resume it, which starts its schedule afresh from now. */
925
+ paused: z5.boolean().optional(),
926
+ title: CronTitleSchema.optional(),
927
+ note: CronNoteSchema.optional(),
928
+ /** A new expression, or `at` for one time; either starts the schedule afresh. */
929
+ cron: CronExpressionSchema.optional(),
930
+ /** The zone `cron` is read in; null for the society's. */
931
+ timezone: TimeZoneSchema.nullable().optional(),
932
+ at: MomentSchema.optional()
933
+ }),
934
+ remove_cron: z5.object({ cron_id: UlidSchema, reason: z5.string().trim().min(1) })
761
935
  };
762
936
 
763
937
  // packages/shared/dist/events.js
764
- import { z as z5 } from "zod";
938
+ import { z as z6 } from "zod";
765
939
  var BOARD_EVENT_TYPES = [
766
940
  "society.initialized",
767
941
  "project.added",
@@ -814,30 +988,38 @@ var BOARD_EVENT_TYPES = [
814
988
  "turn.failed",
815
989
  "merge.completed",
816
990
  "merge.failed",
817
- "workspace.leftovers"
991
+ "pull_request.closed",
992
+ "pull_request.close_failed",
993
+ "workspace.leftovers",
994
+ "society.configured",
995
+ "cron.created",
996
+ "cron.updated",
997
+ "cron.fired",
998
+ "cron.skipped",
999
+ "cron.ended"
818
1000
  ];
819
- var BoardEventTypeSchema = z5.enum(BOARD_EVENT_TYPES);
820
- var BoardEventSchema = z5.object({
1001
+ var BoardEventTypeSchema = z6.enum(BOARD_EVENT_TYPES);
1002
+ var BoardEventSchema = z6.object({
821
1003
  id: UlidSchema,
822
1004
  ts: IsoDateTimeSchema,
823
1005
  type: BoardEventTypeSchema,
824
1006
  actor: NameSchema,
825
- payload: z5.record(z5.string(), z5.unknown())
826
- });
827
- var UsageSchema = z5.object({
828
- inputTokens: z5.number().int().nonnegative(),
829
- outputTokens: z5.number().int().nonnegative(),
830
- cacheReadTokens: z5.number().int().nonnegative().default(0),
831
- cacheWriteTokens: z5.number().int().nonnegative().default(0)
832
- });
833
- var TurnStatusSchema = z5.object({
834
- summary: z5.string().min(1),
835
- claimsHeld: z5.array(UlidSchema).default([]),
836
- blockedOn: z5.array(z5.string()).default([]),
837
- needsUserDecision: z5.boolean().default(false),
838
- memoryUpdated: z5.boolean().default(false)
839
- });
840
- var TurnExitReasonSchema = z5.enum([
1007
+ payload: z6.record(z6.string(), z6.unknown())
1008
+ });
1009
+ var UsageSchema = z6.object({
1010
+ inputTokens: z6.number().int().nonnegative(),
1011
+ outputTokens: z6.number().int().nonnegative(),
1012
+ cacheReadTokens: z6.number().int().nonnegative().default(0),
1013
+ cacheWriteTokens: z6.number().int().nonnegative().default(0)
1014
+ });
1015
+ var TurnStatusSchema = z6.object({
1016
+ summary: z6.string().min(1),
1017
+ claimsHeld: z6.array(UlidSchema).default([]),
1018
+ blockedOn: z6.array(z6.string()).default([]),
1019
+ needsUserDecision: z6.boolean().default(false),
1020
+ memoryUpdated: z6.boolean().default(false)
1021
+ });
1022
+ var TurnExitReasonSchema = z6.enum([
841
1023
  "completed",
842
1024
  "timeout",
843
1025
  "error",
@@ -845,50 +1027,50 @@ var TurnExitReasonSchema = z5.enum([
845
1027
  "blocked",
846
1028
  "stopped"
847
1029
  ]);
848
- var AgentEventSchema = z5.discriminatedUnion("type", [
849
- z5.object({
850
- type: z5.literal("turn_started"),
1030
+ var AgentEventSchema = z6.discriminatedUnion("type", [
1031
+ z6.object({
1032
+ type: z6.literal("turn_started"),
851
1033
  agent: NameSchema,
852
- session: z5.string(),
1034
+ session: z6.string(),
853
1035
  runner: NameSchema,
854
1036
  /** The model the CLI reports running, when it reports one. */
855
- model: z5.string().optional()
1037
+ model: z6.string().optional()
856
1038
  }),
857
1039
  /**
858
1040
  * The CLI took a steer into the running turn. Adapters name the steer; the server adds the
859
1041
  * messages it delivered and the text the CLI was given.
860
1042
  */
861
- z5.object({
862
- type: z5.literal("steered"),
863
- steer: z5.string().min(1),
864
- messages: z5.array(UlidSchema).optional(),
865
- text: z5.string().optional()
1043
+ z6.object({
1044
+ type: z6.literal("steered"),
1045
+ steer: z6.string().min(1),
1046
+ messages: z6.array(UlidSchema).optional(),
1047
+ text: z6.string().optional()
866
1048
  }),
867
- z5.object({ type: z5.literal("text"), delta: z5.string() }),
868
- z5.object({ type: z5.literal("tool_call"), name: z5.string(), input: z5.unknown() }),
869
- z5.object({
870
- type: z5.literal("tool_result"),
871
- name: z5.string(),
872
- ok: z5.boolean(),
1049
+ z6.object({ type: z6.literal("text"), delta: z6.string() }),
1050
+ z6.object({ type: z6.literal("tool_call"), name: z6.string(), input: z6.unknown() }),
1051
+ z6.object({
1052
+ type: z6.literal("tool_result"),
1053
+ name: z6.string(),
1054
+ ok: z6.boolean(),
873
1055
  /** What the tool returned, cut to `OUTPUT_LIMIT` characters by `capOutput`. */
874
- output: z5.string().optional()
1056
+ output: z6.string().optional()
875
1057
  }),
876
- z5.object({ type: z5.literal("approval_requested"), kind: z5.string(), detail: z5.unknown() }),
877
- z5.object({
878
- type: z5.literal("turn_completed"),
1058
+ z6.object({ type: z6.literal("approval_requested"), kind: z6.string(), detail: z6.unknown() }),
1059
+ z6.object({
1060
+ type: z6.literal("turn_completed"),
879
1061
  usage: UsageSchema,
880
- costUsd: z5.number().nonnegative(),
1062
+ costUsd: z6.number().nonnegative(),
881
1063
  status: TurnStatusSchema.nullable(),
882
1064
  exitReason: TurnExitReasonSchema
883
1065
  }),
884
- z5.object({ type: z5.literal("error"), message: z5.string() })
1066
+ z6.object({ type: z6.literal("error"), message: z6.string() })
885
1067
  ]);
886
- var TranscriptEntrySchema = z5.object({
1068
+ var TranscriptEntrySchema = z6.object({
887
1069
  ts: IsoDateTimeSchema,
888
1070
  event: AgentEventSchema
889
1071
  });
890
- var LiveTurnEventSchema = z5.object({
891
- seq: z5.number().int().positive(),
1072
+ var LiveTurnEventSchema = z6.object({
1073
+ seq: z6.number().int().positive(),
892
1074
  ts: IsoDateTimeSchema,
893
1075
  agent: NameSchema,
894
1076
  project: NameSchema,
@@ -900,97 +1082,97 @@ var LiveTurnEventSchema = z5.object({
900
1082
  });
901
1083
 
902
1084
  // packages/shared/dist/turns.js
903
- import { z as z7 } from "zod";
1085
+ import { z as z8 } from "zod";
904
1086
 
905
1087
  // packages/shared/dist/runner.js
906
- import { z as z6 } from "zod";
1088
+ import { z as z7 } from "zod";
907
1089
  var HOME_SCRATCH = "scratch";
908
1090
  var HOME_FILE_LIMIT_BYTES = 8 * 1024 * 1024;
909
- var RunnerHelloSchema = z6.object({
910
- protocol: z6.number().int().positive(),
911
- version: z6.string().min(1),
1091
+ var RunnerHelloSchema = z7.object({
1092
+ protocol: z7.number().int().positive(),
1093
+ version: z7.string().min(1),
912
1094
  os: RunnerOsSchema,
913
- clis: z6.array(CliKindSchema),
1095
+ clis: z7.array(CliKindSchema),
914
1096
  /** The CLIs whose adapter can keep a session warm between turns. */
915
- residentClis: z6.array(CliKindSchema).default([]),
1097
+ residentClis: z7.array(CliKindSchema).default([]),
916
1098
  /** The CLIs whose adapter can take input into a running turn; the server steers only these. */
917
- steerableClis: z6.array(CliKindSchema).default([]),
1099
+ steerableClis: z7.array(CliKindSchema).default([]),
918
1100
  /** The CLIs whose adapter can stop a running turn; the server stops only these. */
919
- stoppableClis: z6.array(CliKindSchema).default([]),
1101
+ stoppableClis: z7.array(CliKindSchema).default([]),
920
1102
  /** Whether the runner answers file reads on its projects' branches; one from before never does. */
921
- branchReads: z6.boolean().default(false),
1103
+ branchReads: z7.boolean().default(false),
922
1104
  /** Whether the runner lists what a branch changed; one from before never does. */
923
- branchChanges: z6.boolean().default(false),
924
- capabilities: z6.array(z6.string()).default([]),
1105
+ branchChanges: z7.boolean().default(false),
1106
+ capabilities: z7.array(z7.string()).default([]),
925
1107
  /** Turns the runner runs at once, or null for no limit. */
926
- slots: z6.number().int().positive().nullable(),
1108
+ slots: z7.number().int().positive().nullable(),
927
1109
  /** Turns the runner is still running, when it registers again; the server fails the rest of its turns. */
928
- turns: z6.array(UlidSchema).default([])
1110
+ turns: z7.array(UlidSchema).default([])
929
1111
  });
930
- var RunnerWelcomeSchema = z6.object({ name: NameSchema, version: z6.string() });
931
- var RunnerEnrollRequestSchema = z6.object({
932
- protocol: z6.number().int().positive(),
933
- version: z6.string().min(1).max(64),
934
- hostname: z6.string().min(1).max(253),
1112
+ var RunnerWelcomeSchema = z7.object({ name: NameSchema, version: z7.string() });
1113
+ var RunnerEnrollRequestSchema = z7.object({
1114
+ protocol: z7.number().int().positive(),
1115
+ version: z7.string().min(1).max(64),
1116
+ hostname: z7.string().min(1).max(253),
935
1117
  os: RunnerOsSchema,
936
- clis: z6.array(CliKindSchema).max(8),
937
- capabilities: z6.array(z6.string().min(1).max(64)).max(32).default([])
1118
+ clis: z7.array(CliKindSchema).max(8),
1119
+ capabilities: z7.array(z7.string().min(1).max(64)).max(32).default([])
938
1120
  });
939
- var RunnerEnrollmentSchema = z6.object({
940
- deviceCode: z6.string().min(1),
941
- userCode: z6.string().min(1),
1121
+ var RunnerEnrollmentSchema = z7.object({
1122
+ deviceCode: z7.string().min(1),
1123
+ userCode: z7.string().min(1),
942
1124
  expiresAt: IsoDateTimeSchema,
943
- intervalMs: z6.number().int().positive()
1125
+ intervalMs: z7.number().int().positive()
944
1126
  });
945
- var RunnerEnrollPollSchema = z6.object({ deviceCode: z6.string().min(1) });
946
- var RunnerEnrollStatusSchema = z6.discriminatedUnion("status", [
947
- z6.object({ status: z6.literal("pending") }),
948
- z6.object({ status: z6.literal("approved"), name: NameSchema, token: z6.string().min(1) }),
949
- z6.object({ status: z6.literal("denied") })
1127
+ var RunnerEnrollPollSchema = z7.object({ deviceCode: z7.string().min(1) });
1128
+ var RunnerEnrollStatusSchema = z7.discriminatedUnion("status", [
1129
+ z7.object({ status: z7.literal("pending") }),
1130
+ z7.object({ status: z7.literal("approved"), name: NameSchema, token: z7.string().min(1) }),
1131
+ z7.object({ status: z7.literal("denied") })
950
1132
  ]);
951
- var PendingEnrollmentSchema = z6.object({
952
- userCode: z6.string().min(1),
953
- hostname: z6.string(),
1133
+ var PendingEnrollmentSchema = z7.object({
1134
+ userCode: z7.string().min(1),
1135
+ hostname: z7.string(),
954
1136
  os: RunnerOsSchema,
955
- clis: z6.array(CliKindSchema),
956
- capabilities: z6.array(z6.string()),
957
- version: z6.string(),
1137
+ clis: z7.array(CliKindSchema),
1138
+ capabilities: z7.array(z7.string()),
1139
+ version: z7.string(),
958
1140
  requestedAt: IsoDateTimeSchema,
959
1141
  expiresAt: IsoDateTimeSchema
960
1142
  });
961
- var ProjectRepoSchema = z6.object({
1143
+ var ProjectRepoSchema = z7.object({
962
1144
  slug: NameSchema,
963
- origin: z6.string().nullable(),
964
- defaultBranch: z6.string().min(1),
1145
+ origin: z7.string().nullable(),
1146
+ defaultBranch: z7.string().min(1),
965
1147
  /**
966
1148
  * The board lands finished tasks on the default branch, itself or through their pull requests,
967
1149
  * so the runner lets nothing else move it.
968
1150
  */
969
- boardLands: z6.boolean()
1151
+ boardLands: z7.boolean()
970
1152
  });
971
- var TurnWorkspaceSchema = z6.discriminatedUnion("kind", [
972
- z6.object({
973
- kind: z6.literal("home"),
1153
+ var TurnWorkspaceSchema = z7.discriminatedUnion("kind", [
1154
+ z7.object({
1155
+ kind: z7.literal("home"),
974
1156
  thread: UlidSchema.optional(),
975
1157
  channel: NameSchema.optional()
976
1158
  }),
977
- z6.object({
978
- kind: z6.literal("project"),
1159
+ z7.object({
1160
+ kind: z7.literal("project"),
979
1161
  repo: ProjectRepoSchema,
980
- branches: z6.array(z6.string().min(1)),
1162
+ branches: z7.array(z7.string().min(1)),
981
1163
  thread: UlidSchema.optional(),
982
1164
  channel: NameSchema.optional()
983
1165
  }),
984
- z6.object({ kind: z6.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
1166
+ z7.object({ kind: z7.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
985
1167
  ]);
986
- var TurnJobSchema = z6.object({
1168
+ var TurnJobSchema = z7.object({
987
1169
  turnId: UlidSchema,
988
1170
  agent: NameSchema,
989
1171
  role: NameSchema,
990
1172
  cli: CliKindSchema,
991
- model: z6.string().optional(),
1173
+ model: z7.string().optional(),
992
1174
  /** The reasoning effort set on the agent; absent for the model's own default. */
993
- effort: z6.string().optional(),
1175
+ effort: z7.string().optional(),
994
1176
  /** A project slug, or the society scope. */
995
1177
  scope: NameSchema,
996
1178
  /** The thread whose conversation the turn is in; absent for the home conversation. */
@@ -1001,160 +1183,167 @@ var TurnJobSchema = z6.object({
1001
1183
  * The session to resume, or null to start one: the CLI's adapter picks a fresh session's id, and
1002
1184
  * the outcome reports it.
1003
1185
  */
1004
- session: z6.string().min(1).nullable(),
1186
+ session: z7.string().min(1).nullable(),
1005
1187
  /** The running total the session reported after its last turn, which a resumed session starts from. */
1006
- costSoFarUsd: z6.number().nonnegative(),
1188
+ costSoFarUsd: z7.number().nonnegative(),
1007
1189
  /** The rendered charter, norms, memory core, and skills index, with `PATH_TOKENS` for places. */
1008
- instructions: z6.string(),
1009
- prompt: z6.string(),
1010
- mcp: z6.object({ url: z6.string().min(1), token: z6.string().min(1) }),
1011
- limits: z6.object({
1012
- timeoutMs: z6.number().int().positive().nullable(),
1013
- maxTurns: z6.number().int().positive().nullable()
1190
+ instructions: z7.string(),
1191
+ prompt: z7.string(),
1192
+ mcp: z7.object({ url: z7.string().min(1), token: z7.string().min(1) }),
1193
+ limits: z7.object({
1194
+ timeoutMs: z7.number().int().positive().nullable(),
1195
+ maxTurns: z7.number().int().positive().nullable()
1014
1196
  }),
1015
1197
  /** Present when the runner keeps the conversation's session warm between turns, under `key`. */
1016
- resident: z6.object({ key: z6.string().min(1), idleMs: z6.number().int().positive() }).optional(),
1198
+ resident: z7.object({ key: z7.string().min(1), idleMs: z7.number().int().positive() }).optional(),
1017
1199
  workspace: TurnWorkspaceSchema
1018
1200
  });
1019
- var TurnWorkSchema = z6.object({ branch: z6.string().min(1), head: z6.string().min(1) });
1020
- var TurnEventsSchema = z6.object({ entries: z6.array(TranscriptEntrySchema) });
1021
- var TurnOutcomeSchema = z6.object({
1201
+ var TurnWorkSchema = z7.object({ branch: z7.string().min(1), head: z7.string().min(1) });
1202
+ var TurnEventsSchema = z7.object({ entries: z7.array(TranscriptEntrySchema) });
1203
+ var TurnOutcomeSchema = z7.object({
1022
1204
  exitReason: TurnExitReasonSchema,
1023
1205
  status: TurnStatusSchema.nullable(),
1024
- error: z6.string().nullable(),
1206
+ error: z7.string().nullable(),
1025
1207
  usage: UsageSchema,
1026
- costUsd: z6.number().nonnegative(),
1208
+ costUsd: z7.number().nonnegative(),
1027
1209
  /** The session's running total after this turn, for CLIs that report one. */
1028
- sessionCostUsd: z6.number().nonnegative().optional(),
1210
+ sessionCostUsd: z7.number().nonnegative().optional(),
1029
1211
  /** The session the CLI used, which differs from the job's for CLIs that assign their own ids. */
1030
- session: z6.string().min(1),
1212
+ session: z7.string().min(1),
1031
1213
  /** The model the CLI reported, when it reported one. */
1032
- model: z6.string().nullable(),
1214
+ model: z7.string().nullable(),
1033
1215
  work: TurnWorkSchema.nullable(),
1034
1216
  /**
1035
1217
  * Whether the turn's own workspace, a thread's, a channel's, or a task's, holds work on no
1036
1218
  * branch after the hand-back: uncommitted changes, or commits no branch contains.
1037
1219
  */
1038
- leftovers: z6.boolean().default(false)
1220
+ leftovers: z7.boolean().default(false)
1039
1221
  });
1040
- var TurnAckSchema = z6.object({ dropWorkspace: z6.boolean() });
1041
- var LandRequestSchema = z6.object({
1222
+ var TurnAckSchema = z7.object({ dropWorkspace: z7.boolean() });
1223
+ var LandRequestSchema = z7.object({
1042
1224
  repo: ProjectRepoSchema,
1043
- branch: z6.string().min(1),
1225
+ branch: z7.string().min(1),
1044
1226
  /**
1045
1227
  * For a `ghpr` task, the pull request to merge in place of the branch, which must hold the
1046
1228
  * branch's files, and the merge commit's subject and body, the pull request's title and an
1047
1229
  * empty body when left out.
1048
1230
  */
1049
- pullRequest: z6.object({
1050
- url: z6.string().min(1),
1051
- subject: z6.string().min(1).optional(),
1052
- body: z6.string().optional()
1231
+ pullRequest: z7.object({
1232
+ url: z7.string().min(1),
1233
+ subject: z7.string().min(1).optional(),
1234
+ body: z7.string().optional()
1053
1235
  }).optional()
1054
1236
  });
1055
- var MergeOutcomeSchema = z6.object({ ok: z6.boolean(), detail: z6.string() });
1056
- var ModelListSchema = z6.array(ModelOptionSchema);
1237
+ var CloseRequestSchema = z7.object({
1238
+ repo: ProjectRepoSchema,
1239
+ url: z7.string().min(1),
1240
+ comment: z7.string().min(1)
1241
+ });
1242
+ var MergeOutcomeSchema = z7.object({ ok: z7.boolean(), detail: z7.string() });
1243
+ var ModelListSchema = z7.array(ModelOptionSchema);
1057
1244
  var BRANCH_FILE_LIMIT_BYTES = 8 * 1024 * 1024;
1058
- var BranchPathSchema = z6.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" });
1059
- var BranchNameSchema = z6.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/);
1060
- var BranchReadSchema = z6.object({
1245
+ var BranchPathSchema = z7.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" });
1246
+ var BranchNameSchema = z7.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/);
1247
+ var BranchReadSchema = z7.object({
1061
1248
  repo: ProjectRepoSchema,
1062
1249
  branch: BranchNameSchema,
1063
1250
  path: BranchPathSchema
1064
1251
  });
1065
- var BranchCommitSchema = z6.object({
1066
- id: z6.string().min(1),
1252
+ var BranchCommitSchema = z7.object({
1253
+ id: z7.string().min(1),
1067
1254
  at: IsoDateTimeSchema,
1068
- author: z6.string()
1255
+ author: z7.string()
1069
1256
  });
1070
- var BranchFileSchema = z6.discriminatedUnion("kind", [
1071
- z6.object({
1072
- kind: z6.literal("file"),
1257
+ var BranchFileSchema = z7.discriminatedUnion("kind", [
1258
+ z7.object({
1259
+ kind: z7.literal("file"),
1073
1260
  path: BranchPathSchema,
1074
1261
  commit: BranchCommitSchema,
1075
- size: z6.number().int().nonnegative(),
1076
- content: z6.string().nullable()
1262
+ size: z7.number().int().nonnegative(),
1263
+ content: z7.string().nullable()
1077
1264
  }),
1078
- z6.object({
1079
- kind: z6.literal("dir"),
1265
+ z7.object({
1266
+ kind: z7.literal("dir"),
1080
1267
  path: BranchPathSchema,
1081
1268
  commit: BranchCommitSchema,
1082
- entries: z6.array(z6.object({
1083
- name: z6.string().min(1),
1084
- kind: z6.enum(["file", "dir"]),
1085
- size: z6.number().int().nonnegative().nullable()
1269
+ entries: z7.array(z7.object({
1270
+ name: z7.string().min(1),
1271
+ kind: z7.enum(["file", "dir"]),
1272
+ size: z7.number().int().nonnegative().nullable()
1086
1273
  }))
1087
1274
  })
1088
1275
  ]);
1089
- var BranchChangesReadSchema = z6.object({
1276
+ var BranchChangesReadSchema = z7.object({
1090
1277
  repo: ProjectRepoSchema,
1091
1278
  branch: BranchNameSchema
1092
1279
  });
1093
- var BranchFileChangeSchema = z6.object({
1094
- path: z6.string().min(1),
1095
- status: z6.enum(["added", "modified", "deleted"]),
1280
+ var BranchFileChangeSchema = z7.object({
1281
+ path: z7.string().min(1),
1282
+ status: z7.enum(["added", "modified", "deleted"]),
1096
1283
  /** Lines added and removed, or null for a binary file. */
1097
- added: z6.number().int().nonnegative().nullable(),
1098
- removed: z6.number().int().nonnegative().nullable(),
1284
+ added: z7.number().int().nonnegative().nullable(),
1285
+ removed: z7.number().int().nonnegative().nullable(),
1099
1286
  /** Absent when the file changed only through a merge of another branch. */
1100
- lastChange: z6.object({ author: z6.string(), at: IsoDateTimeSchema }).optional()
1287
+ lastChange: z7.object({ author: z7.string(), at: IsoDateTimeSchema }).optional()
1101
1288
  });
1102
- var BranchChangesSchema = z6.object({
1289
+ var BranchChangesSchema = z7.object({
1103
1290
  head: BranchCommitSchema,
1104
- files: z6.array(BranchFileChangeSchema),
1105
- total: z6.number().int().nonnegative()
1291
+ files: z7.array(BranchFileChangeSchema),
1292
+ total: z7.number().int().nonnegative()
1106
1293
  });
1107
- var WorkspaceConversationSchema = z6.discriminatedUnion("kind", [
1108
- z6.object({ kind: z6.literal("thread"), id: UlidSchema }),
1109
- z6.object({ kind: z6.literal("channel"), scope: NameSchema, name: NameSchema })
1294
+ var WorkspaceConversationSchema = z7.discriminatedUnion("kind", [
1295
+ z7.object({ kind: z7.literal("thread"), id: UlidSchema }),
1296
+ z7.object({ kind: z7.literal("channel"), scope: NameSchema, name: NameSchema })
1110
1297
  ]);
1111
- var HeldWorkspaceSchema = z6.object({
1298
+ var HeldWorkspaceSchema = z7.object({
1112
1299
  agent: NameSchema,
1113
1300
  scope: NameSchema,
1114
1301
  conversation: WorkspaceConversationSchema
1115
1302
  });
1116
- var HeldWorkspacesSchema = z6.object({ workspaces: z6.array(HeldWorkspaceSchema) });
1117
- var SweepResultSchema = z6.object({ kept: z6.array(HeldWorkspaceSchema) });
1118
- var TurnSteerSchema = z6.object({ id: z6.uuid(), text: z6.string().min(1) });
1119
- var RunnerMessageSchema = z6.discriminatedUnion("type", [
1120
- z6.object({ type: z6.literal("turn"), job: TurnJobSchema }),
1121
- z6.object({ type: z6.literal("land"), request: z6.string().min(1), land: LandRequestSchema }),
1122
- z6.object({ type: z6.literal("models"), request: z6.string().min(1), cli: CliKindSchema }),
1303
+ var HeldWorkspacesSchema = z7.object({ workspaces: z7.array(HeldWorkspaceSchema) });
1304
+ var SweepResultSchema = z7.object({ kept: z7.array(HeldWorkspaceSchema) });
1305
+ var TurnSteerSchema = z7.object({ id: z7.uuid(), text: z7.string().min(1) });
1306
+ var RunnerMessageSchema = z7.discriminatedUnion("type", [
1307
+ z7.object({ type: z7.literal("turn"), job: TurnJobSchema }),
1308
+ z7.object({ type: z7.literal("land"), request: z7.string().min(1), land: LandRequestSchema }),
1309
+ /** Answered with a `MergeOutcome`. */
1310
+ z7.object({ type: z7.literal("close"), request: z7.string().min(1), close: CloseRequestSchema }),
1311
+ z7.object({ type: z7.literal("models"), request: z7.string().min(1), cli: CliKindSchema }),
1123
1312
  /** Answered with a `BranchFile`, or null when the branch or the path does not exist. */
1124
- z6.object({ type: z6.literal("file"), request: z6.string().min(1), read: BranchReadSchema }),
1313
+ z7.object({ type: z7.literal("file"), request: z7.string().min(1), read: BranchReadSchema }),
1125
1314
  /** Answered with `BranchChanges`, or null when the repository or the branch does not exist. */
1126
- z6.object({
1127
- type: z6.literal("changes"),
1128
- request: z6.string().min(1),
1315
+ z7.object({
1316
+ type: z7.literal("changes"),
1317
+ request: z7.string().min(1),
1129
1318
  read: BranchChangesReadSchema
1130
1319
  }),
1131
1320
  /** Answered with whether the CLI accepted it; that it took it comes as a `steered` event. */
1132
- z6.object({
1133
- type: z6.literal("steer"),
1134
- request: z6.string().min(1),
1321
+ z7.object({
1322
+ type: z7.literal("steer"),
1323
+ request: z7.string().min(1),
1135
1324
  turnId: UlidSchema,
1136
1325
  steer: TurnSteerSchema
1137
1326
  }),
1138
1327
  /** Answered with whether the turn was there to stop; it ends `stopped`. */
1139
- z6.object({ type: z6.literal("stop"), request: z6.string().min(1), turnId: UlidSchema }),
1328
+ z7.object({ type: z7.literal("stop"), request: z7.string().min(1), turnId: UlidSchema }),
1140
1329
  /**
1141
1330
  * A conversation ended: the runner removes every citizen's workspace for it but those a turn
1142
1331
  * still uses, and answers with a `SweepResult` naming those it kept for holding work on no branch.
1143
1332
  */
1144
- z6.object({
1145
- type: z6.literal("sweep"),
1146
- request: z6.string().min(1),
1333
+ z7.object({
1334
+ type: z7.literal("sweep"),
1335
+ request: z7.string().min(1),
1147
1336
  conversation: WorkspaceConversationSchema
1148
1337
  })
1149
1338
  ]);
1150
- var RunnerAnswerSchema = z6.discriminatedUnion("ok", [
1151
- z6.object({ ok: z6.literal(true), value: z6.unknown() }),
1152
- z6.object({ ok: z6.literal(false), error: z6.string() })
1339
+ var RunnerAnswerSchema = z7.discriminatedUnion("ok", [
1340
+ z7.object({ ok: z7.literal(true), value: z7.unknown() }),
1341
+ z7.object({ ok: z7.literal(false), error: z7.string() })
1153
1342
  ]);
1154
- var WarmSessionsSchema = z6.object({ warm: z6.array(z6.string()) });
1155
- var FileManifestSchema = z6.object({ files: z6.record(z6.string(), z6.string()) });
1156
- var FileReadSchema = z6.object({ paths: z6.array(z6.string()) });
1157
- var FileContentsSchema = z6.object({ files: z6.record(z6.string(), z6.string()) });
1343
+ var WarmSessionsSchema = z7.object({ warm: z7.array(z7.string()) });
1344
+ var FileManifestSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
1345
+ var FileReadSchema = z7.object({ paths: z7.array(z7.string()) });
1346
+ var FileContentsSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
1158
1347
  function isSafeRelativePath(file) {
1159
1348
  if (file.length === 0 || file.startsWith("/") || file.includes("\\") || file.includes("\0")) {
1160
1349
  return false;
@@ -1163,7 +1352,7 @@ function isSafeRelativePath(file) {
1163
1352
  }
1164
1353
 
1165
1354
  // packages/shared/dist/turns.js
1166
- var TriggerKindSchema = z7.enum([
1355
+ var TriggerKindSchema = z8.enum([
1167
1356
  "mention",
1168
1357
  "stage",
1169
1358
  "task_done",
@@ -1174,27 +1363,32 @@ var TriggerKindSchema = z7.enum([
1174
1363
  "manual",
1175
1364
  "ops_event",
1176
1365
  "user_post",
1177
- "closing"
1366
+ "closing",
1367
+ "cron"
1178
1368
  ]);
1179
- var TriggerSchema = z7.object({
1369
+ var TriggerSchema = z8.object({
1180
1370
  kind: TriggerKindSchema,
1181
1371
  from: NameSchema.optional(),
1182
- fromUser: z7.boolean().default(false),
1183
- reason: z7.string().default(""),
1372
+ fromUser: z8.boolean().default(false),
1373
+ reason: z8.string().default(""),
1184
1374
  taskId: UlidSchema.optional(),
1185
1375
  messageId: UlidSchema.optional(),
1186
1376
  /** The channel the waking message was posted in. */
1187
- channel: ChannelRefSchema.optional()
1377
+ channel: ChannelRefSchema.optional(),
1378
+ /** The cron that came due, for a `cron` wake. */
1379
+ cronId: UlidSchema.optional()
1188
1380
  });
1189
- var TurnThreadSchema = z7.object({ id: UlidSchema, task: z7.boolean() });
1190
- var TurnDispatchSchema = z7.object({
1381
+ var TurnThreadSchema = z8.object({ id: UlidSchema, task: z8.boolean() });
1382
+ var TurnDispatchSchema = z8.object({
1191
1383
  agent: NameSchema,
1192
1384
  project: NameSchema,
1193
1385
  thread: TurnThreadSchema.optional(),
1194
1386
  channel: NameSchema.optional(),
1195
1387
  trigger: TriggerSchema,
1196
- priority: z7.number().int().min(0).max(2),
1197
- onboarding: z7.boolean().default(false)
1388
+ priority: z8.number().int().min(0).max(2),
1389
+ onboarding: z8.boolean().default(false),
1390
+ /** Every cron that came due for this turn, whatever wake it merged into; its prompt quotes each. */
1391
+ crons: z8.array(UlidSchema).optional()
1198
1392
  });
1199
1393
  function conversationPart(conversation) {
1200
1394
  return conversation.thread ?? (conversation.channel === void 0 ? void 0 : `#${conversation.channel}`);
@@ -1205,7 +1399,7 @@ function channelConversation(scope, channel) {
1205
1399
  const name = at < 0 ? channel : channel.slice(at + 1);
1206
1400
  return place === scope && name !== "general" ? name : void 0;
1207
1401
  }
1208
- var TurnRecordSchema = z7.object({
1402
+ var TurnRecordSchema = z8.object({
1209
1403
  /** Assigned by the board when the turn begins; the turn's transcript is filed under it. */
1210
1404
  id: UlidSchema.optional(),
1211
1405
  agent: NameSchema,
@@ -1216,29 +1410,31 @@ var TurnRecordSchema = z7.object({
1216
1410
  channel: NameSchema.optional(),
1217
1411
  runner: NameSchema,
1218
1412
  cli: CliKindSchema.nullable(),
1219
- session: z7.string().nullable(),
1413
+ session: z8.string().nullable(),
1220
1414
  trigger: TriggerSchema,
1221
1415
  startedAt: IsoDateTimeSchema,
1222
1416
  endedAt: IsoDateTimeSchema.nullable(),
1223
1417
  exitReason: TurnExitReasonSchema.nullable(),
1224
1418
  status: TurnStatusSchema.nullable(),
1225
- error: z7.string().nullable(),
1419
+ error: z8.string().nullable(),
1226
1420
  usage: UsageSchema.nullable(),
1227
- costUsd: z7.number().nonnegative().default(0),
1421
+ costUsd: z8.number().nonnegative().default(0),
1228
1422
  /**
1229
1423
  * The running total the CLI reported for the session after this turn, when it reports one. A
1230
1424
  * resumed session starts from it, so the next turn's own cost is measured from here.
1231
1425
  */
1232
- sessionCostUsd: z7.number().nonnegative().optional(),
1233
- toolCalls: z7.number().int().nonnegative().default(0),
1426
+ sessionCostUsd: z8.number().nonnegative().optional(),
1427
+ toolCalls: z8.number().int().nonnegative().default(0),
1234
1428
  /** The model the CLI reported for this turn, else the model configured for the agent, else null. */
1235
- model: z7.string().nullable().default(null),
1429
+ model: z8.string().nullable().default(null),
1236
1430
  /** The task branch the turn left work on, with its commit, as the runner reported it. */
1237
1431
  work: TurnWorkSchema.optional(),
1238
1432
  /** Who stopped the turn, for one that ended `stopped`. */
1239
- stoppedBy: NameSchema.optional()
1433
+ stoppedBy: NameSchema.optional(),
1434
+ /** The crons whose fire started the turn, which record how it ended. */
1435
+ crons: z8.array(UlidSchema).optional()
1240
1436
  });
1241
- var RunningTurnSchema = z7.object({
1437
+ var RunningTurnSchema = z8.object({
1242
1438
  turnId: UlidSchema,
1243
1439
  agent: NameSchema,
1244
1440
  scope: NameSchema,
@@ -1246,49 +1442,49 @@ var RunningTurnSchema = z7.object({
1246
1442
  channel: NameSchema.optional(),
1247
1443
  cli: CliKindSchema,
1248
1444
  askedIn: ChannelRefSchema.optional(),
1249
- steerable: z7.boolean(),
1250
- stoppable: z7.boolean()
1445
+ steerable: z8.boolean(),
1446
+ stoppable: z8.boolean()
1251
1447
  });
1252
- var SessionsFileSchema = z7.object({
1253
- claude: z7.string().optional(),
1254
- codex: z7.string().optional(),
1448
+ var SessionsFileSchema = z8.object({
1449
+ claude: z8.string().optional(),
1450
+ codex: z8.string().optional(),
1255
1451
  runner: NameSchema.optional()
1256
1452
  });
1257
- var WakeRequestSchema = z7.object({
1453
+ var WakeRequestSchema = z8.object({
1258
1454
  agent: NameSchema,
1259
1455
  project: NameSchema,
1260
- reason: z7.string().default("manual wake"),
1456
+ reason: z8.string().default("manual wake"),
1261
1457
  /** A manual wake, or a reflection turn requested ahead of the cadence. */
1262
- kind: z7.enum(["manual", "reflection"]).default("manual")
1458
+ kind: z8.enum(["manual", "reflection"]).default("manual")
1263
1459
  });
1264
- var TurnHistoryEntrySchema = z7.object({
1460
+ var TurnHistoryEntrySchema = z8.object({
1265
1461
  /** The id of the event that ended the turn. */
1266
1462
  id: UlidSchema,
1267
1463
  /** The turn's own id, when it has a transcript to open; turns before transcripts have none. */
1268
1464
  turnId: UlidSchema.optional(),
1269
1465
  ts: IsoDateTimeSchema,
1270
- outcome: z7.enum(["completed", "failed"]),
1466
+ outcome: z8.enum(["completed", "failed"]),
1271
1467
  /** A project slug, or the society scope. */
1272
1468
  project: NameSchema,
1273
1469
  /** The thread whose conversation the turn was in; absent for the home conversation. */
1274
1470
  thread: UlidSchema.optional(),
1275
1471
  /** The channel, never general, whose conversation the turn was in. */
1276
1472
  channel: NameSchema.optional(),
1277
- trigger: z7.string(),
1278
- exitReason: z7.string().nullable(),
1279
- costUsd: z7.number(),
1473
+ trigger: z8.string(),
1474
+ exitReason: z8.string().nullable(),
1475
+ costUsd: z8.number(),
1280
1476
  /** The tokens the turn used; absent for turns logged before turns recorded them. */
1281
1477
  usage: UsageSchema.optional(),
1282
- model: z7.string().nullable(),
1283
- summary: z7.string().nullable(),
1284
- error: z7.string().nullable(),
1478
+ model: z8.string().nullable(),
1479
+ summary: z8.string().nullable(),
1480
+ error: z8.string().nullable(),
1285
1481
  /** When the log recorded the turn's start; absent when the start is missing from the log. */
1286
1482
  startedAt: IsoDateTimeSchema.optional(),
1287
- toolCalls: z7.number().int().nonnegative().optional()
1483
+ toolCalls: z8.number().int().nonnegative().optional()
1288
1484
  });
1289
1485
 
1290
1486
  // packages/shared/dist/ops.js
1291
- import { z as z8 } from "zod";
1487
+ import { z as z9 } from "zod";
1292
1488
  var OPS_SIGNAL_KINDS = [
1293
1489
  "waiting_stage",
1294
1490
  "backlog",
@@ -1301,18 +1497,19 @@ var OPS_SIGNAL_KINDS = [
1301
1497
  "scaled",
1302
1498
  "runner",
1303
1499
  "home_conflict",
1304
- "stuck_workspace"
1500
+ "stuck_workspace",
1501
+ "cron_failing"
1305
1502
  ];
1306
- var OpsSignalKindSchema = z8.enum(OPS_SIGNAL_KINDS);
1307
- var OpsSignalSchema = z8.object({
1503
+ var OpsSignalKindSchema = z9.enum(OPS_SIGNAL_KINDS);
1504
+ var OpsSignalSchema = z9.object({
1308
1505
  kind: OpsSignalKindSchema,
1309
1506
  /** Identifies the condition, for example `backlog:demo:engineer`, so a persisting one is not logged again every pass. */
1310
- key: z8.string().min(1),
1507
+ key: z9.string().min(1),
1311
1508
  /** One line a reader can act on. */
1312
- summary: z8.string().min(1),
1509
+ summary: z9.string().min(1),
1313
1510
  /** The counter or timer value behind the signal. */
1314
- value: z8.number(),
1315
- threshold: z8.number().optional(),
1511
+ value: z9.number(),
1512
+ threshold: z9.number().optional(),
1316
1513
  project: NameSchema.optional(),
1317
1514
  agent: NameSchema.optional(),
1318
1515
  role: NameSchema.optional(),
@@ -1320,8 +1517,8 @@ var OpsSignalSchema = z8.object({
1320
1517
  });
1321
1518
 
1322
1519
  // packages/shared/dist/metrics.js
1323
- import { z as z9 } from "zod";
1324
- var MetricsWindowSchema = z9.enum(["24h", "7d", "all"]);
1520
+ import { z as z10 } from "zod";
1521
+ var MetricsWindowSchema = z10.enum(["24h", "7d", "all"]);
1325
1522
  var METRICS_WINDOW_MS = {
1326
1523
  "24h": 24 * 36e5,
1327
1524
  "7d": 7 * 24 * 36e5,
@@ -1332,9 +1529,9 @@ function isBoardAction(tool) {
1332
1529
  const verb = /^mcp__board__(.+)$/.exec(tool)?.[1];
1333
1530
  return verb !== void 0 && !READ_VERBS.some((read2) => read2 === verb);
1334
1531
  }
1335
- var CountSchema = z9.number().int().nonnegative();
1336
- var MsSchema = z9.number().nonnegative();
1337
- var MetricsSchema = z9.object({
1532
+ var CountSchema = z10.number().int().nonnegative();
1533
+ var MsSchema = z10.number().nonnegative();
1534
+ var MetricsSchema = z10.object({
1338
1535
  window: MetricsWindowSchema,
1339
1536
  since: IsoDateTimeSchema.nullable(),
1340
1537
  until: IsoDateTimeSchema,
@@ -1342,12 +1539,12 @@ var MetricsSchema = z9.object({
1342
1539
  * Finished turns that changed nothing on the board. Reflections are left out, since they write
1343
1540
  * memory, not the board; turns without a transcript are counted apart, their actions unknown.
1344
1541
  */
1345
- idle: z9.object({
1542
+ idle: z10.object({
1346
1543
  turns: CountSchema,
1347
1544
  idle: CountSchema,
1348
1545
  unknown: CountSchema,
1349
- byTrigger: z9.array(z9.object({ trigger: z9.string(), turns: CountSchema, idle: CountSchema })),
1350
- byAgent: z9.array(z9.object({
1546
+ byTrigger: z10.array(z10.object({ trigger: z10.string(), turns: CountSchema, idle: CountSchema })),
1547
+ byAgent: z10.array(z10.object({
1351
1548
  agent: NameSchema,
1352
1549
  role: NameSchema.nullable(),
1353
1550
  turns: CountSchema,
@@ -1355,33 +1552,33 @@ var MetricsSchema = z9.object({
1355
1552
  }))
1356
1553
  }),
1357
1554
  /** Work sent back to an earlier stage: over the tasks finished in the window, and by where. */
1358
- sentBack: z9.object({
1555
+ sentBack: z10.object({
1359
1556
  finished: CountSchema,
1360
1557
  sendBacks: CountSchema,
1361
1558
  tasksSentBack: CountSchema,
1362
- byStage: z9.array(z9.object({ project: NameSchema, stage: z9.string(), count: CountSchema })),
1363
- bySender: z9.array(z9.object({ agent: NameSchema, count: CountSchema }))
1559
+ byStage: z10.array(z10.object({ project: NameSchema, stage: z10.string(), count: CountSchema })),
1560
+ bySender: z10.array(z10.object({ agent: NameSchema, count: CountSchema }))
1364
1561
  }),
1365
1562
  /** Posts in the threads of the tasks finished in the window, the board's own notices left out. */
1366
- messages: z9.object({
1563
+ messages: z10.object({
1367
1564
  finished: CountSchema,
1368
1565
  messages: CountSchema,
1369
- byProject: z9.array(z9.object({ project: NameSchema, finished: CountSchema, messages: CountSchema })),
1370
- busiest: z9.array(z9.object({
1566
+ byProject: z10.array(z10.object({ project: NameSchema, finished: CountSchema, messages: CountSchema })),
1567
+ busiest: z10.array(z10.object({
1371
1568
  taskId: UlidSchema,
1372
- title: z9.string(),
1569
+ title: z10.string(),
1373
1570
  project: NameSchema,
1374
1571
  messages: CountSchema
1375
1572
  }))
1376
1573
  }),
1377
1574
  /** Time from a mention to the start of the turn it woke, in the conversation it was made in. */
1378
- latency: z9.object({
1575
+ latency: z10.object({
1379
1576
  mentions: CountSchema,
1380
1577
  /** Mentions no turn has followed yet. */
1381
1578
  unanswered: CountSchema,
1382
1579
  medianMs: MsSchema.nullable(),
1383
1580
  slowestMs: MsSchema.nullable(),
1384
- byAgent: z9.array(z9.object({
1581
+ byAgent: z10.array(z10.object({
1385
1582
  agent: NameSchema,
1386
1583
  mentions: CountSchema,
1387
1584
  medianMs: MsSchema,
@@ -1389,81 +1586,81 @@ var MetricsSchema = z9.object({
1389
1586
  }))
1390
1587
  }),
1391
1588
  /** The user's decisions: proposals decided, questions answered, and stages passed or sent back. */
1392
- decisions: z9.object({
1589
+ decisions: z10.object({
1393
1590
  total: CountSchema,
1394
- byDay: z9.array(z9.object({
1395
- day: z9.string(),
1591
+ byDay: z10.array(z10.object({
1592
+ day: z10.string(),
1396
1593
  proposals: CountSchema,
1397
1594
  answers: CountSchema,
1398
1595
  stages: CountSchema
1399
1596
  }))
1400
1597
  }),
1401
1598
  /** Tasks that needed a capability no connected runner offered, and whether they still do. */
1402
- blocked: z9.array(z9.object({
1599
+ blocked: z10.array(z10.object({
1403
1600
  taskId: UlidSchema,
1404
- title: z9.string().nullable(),
1601
+ title: z10.string().nullable(),
1405
1602
  project: NameSchema.nullable(),
1406
- summary: z9.string(),
1603
+ summary: z10.string(),
1407
1604
  firstAt: IsoDateTimeSchema,
1408
1605
  lastAt: IsoDateTimeSchema,
1409
- holds: z9.boolean()
1606
+ holds: z10.boolean()
1410
1607
  }))
1411
1608
  });
1412
1609
 
1413
1610
  // packages/shared/dist/config.js
1414
- import { z as z10 } from "zod";
1415
- var LogLevelSchema = z10.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
1416
- var GithubSignInSchema = z10.object({
1417
- clientId: z10.string().min(1),
1418
- clientSecret: z10.string().min(1),
1611
+ import { z as z11 } from "zod";
1612
+ var LogLevelSchema = z11.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
1613
+ var GithubSignInSchema = z11.object({
1614
+ clientId: z11.string().min(1),
1615
+ clientSecret: z11.string().min(1),
1419
1616
  /** Logins, lowercased; GitHub logins are case-insensitive. */
1420
- users: z10.array(z10.string().min(1)).min(1)
1617
+ users: z11.array(z11.string().min(1)).min(1)
1421
1618
  });
1422
- var ServerConfigSchema = z10.object({
1423
- dataDir: z10.string().min(1),
1424
- host: z10.string().min(1),
1425
- port: z10.number().int().positive().max(65535),
1619
+ var ServerConfigSchema = z11.object({
1620
+ dataDir: z11.string().min(1),
1621
+ host: z11.string().min(1),
1622
+ port: z11.number().int().positive().max(65535),
1426
1623
  /**
1427
1624
  * An address every runner's agents reach this server at, for example https://board.example, or
1428
1625
  * null for each runner to use the address it reaches the server at itself.
1429
1626
  */
1430
- publicUrl: z10.string().min(1).nullable(),
1627
+ publicUrl: z11.string().min(1).nullable(),
1431
1628
  logLevel: LogLevelSchema,
1432
1629
  /** Turns running at once across every runner, or null for no limit beyond each runner's own. */
1433
- concurrency: z10.number().int().positive().nullable(),
1630
+ concurrency: z11.number().int().positive().nullable(),
1434
1631
  /** How long one turn may run before it is stopped, or null for no limit. */
1435
- turnTimeoutMs: z10.number().int().positive().nullable(),
1632
+ turnTimeoutMs: z11.number().int().positive().nullable(),
1436
1633
  /** Rounds of tool calls one Claude turn may take, or null for no limit. */
1437
- toolRounds: z10.number().int().positive().nullable(),
1634
+ toolRounds: z11.number().int().positive().nullable(),
1438
1635
  /** How long a resident session stays warm after its last turn. */
1439
- residentIdleMs: z10.number().int().positive(),
1636
+ residentIdleMs: z11.number().int().positive(),
1440
1637
  /** Signing in to the board with GitHub, or null when only a token lets anyone in. */
1441
1638
  github: GithubSignInSchema.nullable(),
1442
1639
  /**
1443
1640
  * Reverse proxies in front of the server whose X-Forwarded-For entries name the client, for the
1444
1641
  * rate limits on what anyone may ask without a token: 0 when clients connect directly.
1445
1642
  */
1446
- trustedProxies: z10.number().int().min(0).max(16)
1643
+ trustedProxies: z11.number().int().min(0).max(16)
1447
1644
  });
1448
- var RunnerConfigSchema = z10.object({
1645
+ var RunnerConfigSchema = z11.object({
1449
1646
  /** The board server, for example http://127.0.0.1:4700. */
1450
- serverUrl: z10.string().url(),
1647
+ serverUrl: z11.string().url(),
1451
1648
  /**
1452
1649
  * The runner's token, from `runner add`, or null for the one it saved when it was enrolled, or
1453
1650
  * to enroll it.
1454
1651
  */
1455
- token: z10.string().min(1).nullable(),
1652
+ token: z11.string().min(1).nullable(),
1456
1653
  /** The runner's own data directory: the board mirror, agent homes, repositories, worktrees. */
1457
- dataDir: z10.string().min(1),
1654
+ dataDir: z11.string().min(1),
1458
1655
  logLevel: LogLevelSchema,
1459
1656
  /** Turns this machine runs at once, or null for no limit. */
1460
- slots: z10.number().int().positive().nullable(),
1461
- clis: z10.array(CliKindSchema).min(1),
1462
- capabilities: z10.array(z10.string())
1657
+ slots: z11.number().int().positive().nullable(),
1658
+ clis: z11.array(CliKindSchema).min(1),
1659
+ capabilities: z11.array(z11.string())
1463
1660
  });
1464
1661
 
1465
1662
  // packages/shared/dist/version.js
1466
- var STELLARIS_VERSION = "0.3.12";
1663
+ var STELLARIS_VERSION = "0.3.14";
1467
1664
 
1468
1665
  // packages/board-core/dist/errors.js
1469
1666
  var BoardError = class extends Error {
@@ -2008,32 +2205,32 @@ function filesOf(lines, status) {
2008
2205
  }
2009
2206
 
2010
2207
  // packages/board-core/dist/metrics.js
2011
- import { z as z11 } from "zod";
2012
- var TurnPayload = z11.object({
2013
- turnId: z11.string().optional(),
2014
- project: z11.string(),
2015
- thread: z11.string().optional(),
2016
- channel: z11.string().optional(),
2017
- trigger: z11.string()
2018
- });
2019
- var TaskPayload = z11.object({
2020
- taskId: z11.string(),
2021
- project: z11.string(),
2022
- from: z11.string().optional()
2023
- });
2024
- var MessagePayload = z11.object({
2025
- id: z11.string().optional(),
2026
- channel: z11.string(),
2027
- thread: z11.string().nullable().optional(),
2028
- mentions: z11.array(z11.string()).default([])
2029
- });
2030
- var SteeredPayload = z11.object({ messages: z11.array(z11.string()) });
2031
- var SignalPayload = z11.object({
2032
- kind: z11.string(),
2033
- key: z11.string(),
2034
- summary: z11.string(),
2035
- taskId: z11.string().optional(),
2036
- project: z11.string().optional()
2208
+ import { z as z12 } from "zod";
2209
+ var TurnPayload = z12.object({
2210
+ turnId: z12.string().optional(),
2211
+ project: z12.string(),
2212
+ thread: z12.string().optional(),
2213
+ channel: z12.string().optional(),
2214
+ trigger: z12.string()
2215
+ });
2216
+ var TaskPayload = z12.object({
2217
+ taskId: z12.string(),
2218
+ project: z12.string(),
2219
+ from: z12.string().optional()
2220
+ });
2221
+ var MessagePayload = z12.object({
2222
+ id: z12.string().optional(),
2223
+ channel: z12.string(),
2224
+ thread: z12.string().nullable().optional(),
2225
+ mentions: z12.array(z12.string()).default([])
2226
+ });
2227
+ var SteeredPayload = z12.object({ messages: z12.array(z12.string()) });
2228
+ var SignalPayload = z12.object({
2229
+ kind: z12.string(),
2230
+ key: z12.string(),
2231
+ summary: z12.string(),
2232
+ taskId: z12.string().optional(),
2233
+ project: z12.string().optional()
2037
2234
  });
2038
2235
  function median(values) {
2039
2236
  const sorted = values.toSorted((a, b) => a - b);
@@ -2402,6 +2599,13 @@ var BoardPaths = class {
2402
2599
  members() {
2403
2600
  return path5.join(this.society, "members");
2404
2601
  }
2602
+ /** Crons, one file each with its note as the body; ended ones stay. */
2603
+ crons() {
2604
+ return path5.join(this.society, "crons");
2605
+ }
2606
+ cron(id) {
2607
+ return path5.join(this.crons(), `${id}.md`);
2608
+ }
2405
2609
  member(name) {
2406
2610
  return path5.join(this.members(), `${name}.md`);
2407
2611
  }
@@ -2548,41 +2752,41 @@ function namesList(names) {
2548
2752
  return names.length <= 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}`;
2549
2753
  }
2550
2754
  var SKIPPED_IN_HOME = /* @__PURE__ */ new Set([".git", "turns", ".claude", ".codex"]);
2551
- var RunnerTokensSchema = z12.object({
2552
- runners: z12.record(z12.string(), z12.object({ tokenHash: z12.string().min(1) }))
2755
+ var RunnerTokensSchema = z13.object({
2756
+ runners: z13.record(z13.string(), z13.object({ tokenHash: z13.string().min(1) }))
2553
2757
  });
2554
- var SignInsSchema = z12.object({
2555
- signIns: z12.array(z12.object({
2556
- tokenHash: z12.string().min(1),
2557
- login: z12.string().min(1),
2758
+ var SignInsSchema = z13.object({
2759
+ signIns: z13.array(z13.object({
2760
+ tokenHash: z13.string().min(1),
2761
+ login: z13.string().min(1),
2558
2762
  /** The account's picture at the identity provider; sign-ins from before it have none. */
2559
- avatarUrl: z12.url().optional(),
2763
+ avatarUrl: z13.url().optional(),
2560
2764
  createdAt: IsoDateTimeSchema,
2561
2765
  expiresAt: IsoDateTimeSchema
2562
2766
  })).default([])
2563
2767
  });
2564
- var TurnHistoryPayloadSchema = z12.object({
2768
+ var TurnHistoryPayloadSchema = z13.object({
2565
2769
  project: NameSchema,
2566
2770
  thread: UlidSchema.optional(),
2567
2771
  channel: NameSchema.optional(),
2568
- trigger: z12.string().default("manual"),
2569
- exitReason: z12.string().nullable().default(null),
2570
- costUsd: z12.number().default(0),
2772
+ trigger: z13.string().default("manual"),
2773
+ exitReason: z13.string().nullable().default(null),
2774
+ costUsd: z13.number().default(0),
2571
2775
  usage: UsageSchema.optional(),
2572
- model: z12.string().nullable().default(null),
2573
- summary: z12.string().nullable().default(null),
2574
- error: z12.string().nullable().default(null),
2575
- toolCalls: z12.number().int().nonnegative().optional(),
2776
+ model: z13.string().nullable().default(null),
2777
+ summary: z13.string().nullable().default(null),
2778
+ error: z13.string().nullable().default(null),
2779
+ toolCalls: z13.number().int().nonnegative().optional(),
2576
2780
  turnId: UlidSchema.optional()
2577
2781
  });
2578
- var TurnStartPayloadSchema = z12.object({ project: NameSchema, turnId: UlidSchema.optional() });
2579
- var CursorsSchema = z12.object({
2580
- digest: z12.string().nullable(),
2581
- scopes: z12.record(z12.string(), z12.string()).default({}),
2582
- threadsFrom: z12.record(z12.string(), z12.string().nullable()).default({}),
2583
- channelsFrom: z12.record(z12.string(), z12.string().nullable()).default({}),
2782
+ var TurnStartPayloadSchema = z13.object({ project: NameSchema, turnId: UlidSchema.optional() });
2783
+ var CursorsSchema = z13.object({
2784
+ digest: z13.string().nullable(),
2785
+ scopes: z13.record(z13.string(), z13.string()).default({}),
2786
+ threadsFrom: z13.record(z13.string(), z13.string().nullable()).default({}),
2787
+ channelsFrom: z13.record(z13.string(), z13.string().nullable()).default({}),
2584
2788
  /** How far a work role has read news, which goes to whichever of its general conversations reads first. */
2585
- news: z12.string().optional()
2789
+ news: z13.string().optional()
2586
2790
  });
2587
2791
  var NO_CURSORS = { digest: null, scopes: {}, threadsFrom: {}, channelsFrom: {} };
2588
2792
  function conversationKey(scope, part) {
@@ -2621,7 +2825,7 @@ function byId(a, b) {
2621
2825
  function turnTime(record) {
2622
2826
  return record.endedAt ?? record.startedAt;
2623
2827
  }
2624
- var PausedSchema = z12.object({ paused: z12.boolean() });
2828
+ var PausedSchema = z13.object({ paused: z13.boolean() });
2625
2829
  var DEFAULT_LEASE_MS = 30 * 60 * 1e3;
2626
2830
  var MENTION_PATTERN = /(^|[^\w@])@([a-z0-9][a-z0-9-]{0,31})(?![\w-])/g;
2627
2831
  function withoutMentions(text) {
@@ -2633,7 +2837,7 @@ var OPS_WAKE_TRIGGER = "ops_event";
2633
2837
  var FRONT_DESK_TRIGGER = "user_post";
2634
2838
  var OPS_CHANNELS = ["governance"];
2635
2839
  var FRONT_DESK_CHANNELS = [ASK_CHANNEL];
2636
- var AlignmentsSchema = z12.object({ applied: z12.array(z12.string()).default([]) });
2840
+ var AlignmentsSchema = z13.object({ applied: z13.array(z13.string()).default([]) });
2637
2841
  var FOLLOW_DECISIONS = "ops-readers-follow-decisions";
2638
2842
  var THREAD_RECORDS = "threads-as-records";
2639
2843
  var DIGEST_CURSORS = "digest-replaces-inbox";
@@ -2645,6 +2849,8 @@ var ASKS_CHANNEL_OPENED = "asks-channel-opened";
2645
2849
  var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
2646
2850
  var CHANNEL_CONVERSATIONS = "channel-conversations";
2647
2851
  var DASHBOARD_UPDATES = "dashboard-updates-granted";
2852
+ var CRON_VERBS = "cron-verbs-granted";
2853
+ var CRON_VERB_NAMES = ["create_cron", "update_cron", "remove_cron"];
2648
2854
  var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
2649
2855
  var DASHBOARD_LINE = "The project's members keep this page current with update_dashboard.";
2650
2856
  var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
@@ -2846,7 +3052,8 @@ var Board = class _Board {
2846
3052
  ASKS_CHANNEL_OPENED,
2847
3053
  KNOWLEDGE_REMOVAL,
2848
3054
  CHANNEL_CONVERSATIONS,
2849
- DASHBOARD_UPDATES
3055
+ DASHBOARD_UPDATES,
3056
+ CRON_VERBS
2850
3057
  ].filter((name) => !applied.includes(name));
2851
3058
  if (pending.length === 0) {
2852
3059
  return;
@@ -2885,6 +3092,9 @@ var Board = class _Board {
2885
3092
  await this.grantBesideKnowledge("update_dashboard");
2886
3093
  await this.rewordDashboards();
2887
3094
  }
3095
+ if (pending.includes(CRON_VERBS)) {
3096
+ await this.grantToEveryRole(CRON_VERB_NAMES);
3097
+ }
2888
3098
  await writeJson(file, { applied: [...applied, ...pending] });
2889
3099
  }
2890
3100
  /**
@@ -2898,6 +3108,15 @@ var Board = class _Board {
2898
3108
  }
2899
3109
  }
2900
3110
  }
3111
+ /** Grants verbs once to every charter but the user's, which holds every verb already. */
3112
+ async grantToEveryRole(verbs) {
3113
+ for (const charter of await this.listRoles()) {
3114
+ const missing = verbs.filter((verb) => !charter.verbs.includes(verb));
3115
+ if (charter.name !== USER_ROLE && missing.length > 0) {
3116
+ await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, ...missing] }, { verbsAdded: missing });
3117
+ }
3118
+ }
3119
+ }
2901
3120
  /** A dashboard still saying agents may edit its file says how it changes now. */
2902
3121
  async rewordDashboards() {
2903
3122
  for (const slug of await listDirs(this.paths.projects())) {
@@ -3042,7 +3261,7 @@ var Board = class _Board {
3042
3261
  }
3043
3262
  }
3044
3263
  for (const file of await listFiles(this.paths.tasks(slug))) {
3045
- const raw = await readMarkdown(path6.join(this.paths.tasks(slug), file), z12.record(z12.string(), z12.unknown()));
3264
+ const raw = await readMarkdown(path6.join(this.paths.tasks(slug), file), z13.record(z13.string(), z13.unknown()));
3046
3265
  const legacy = raw.data["thread"];
3047
3266
  if (legacy === void 0) {
3048
3267
  continue;
@@ -3120,7 +3339,7 @@ var Board = class _Board {
3120
3339
  * turns, so it follows nothing now.
3121
3340
  */
3122
3341
  async renameInboxCursors() {
3123
- const Legacy = z12.object({ inbox: z12.string().nullable() });
3342
+ const Legacy = z13.object({ inbox: z13.string().nullable() });
3124
3343
  for (const agent2 of await this.listAgents()) {
3125
3344
  const file = this.paths.agentCursors(agent2.name);
3126
3345
  if (await exists(file)) {
@@ -3166,7 +3385,7 @@ ${charter.purpose}
3166
3385
  */
3167
3386
  async alignSeedRole(seed) {
3168
3387
  const file = this.paths.role(seed.name);
3169
- const raw = await readMarkdown(file, z12.record(z12.string(), z12.unknown()));
3388
+ const raw = await readMarkdown(file, z13.record(z13.string(), z13.unknown()));
3170
3389
  const current = RoleCharterSchema.parse(raw.data);
3171
3390
  const verbsAdded = seed.verbs.filter((verb) => !current.verbs.includes(verb));
3172
3391
  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));
@@ -3479,14 +3698,14 @@ ${charter.purpose}
3479
3698
  /** The society's norms, the one knowledge topic every turn loads. Empty until the steward writes it. */
3480
3699
  async readSocietyNorms() {
3481
3700
  const file = this.paths.societyKnowledgeFile("norms");
3482
- return await exists(file) ? (await readMarkdown(file, z12.record(z12.string(), z12.unknown()))).body : "";
3701
+ return await exists(file) ? (await readMarkdown(file, z13.record(z13.string(), z13.unknown()))).body : "";
3483
3702
  }
3484
3703
  /** Knowledge topics of a project, or of the society when the project is null. */
3485
3704
  async listKnowledge(project2) {
3486
3705
  const dir = project2 === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project2);
3487
3706
  const topics = [];
3488
3707
  for (const file of await listFiles(dir)) {
3489
- const doc = await readMarkdown(path6.join(dir, file), z12.record(z12.string(), z12.unknown()));
3708
+ const doc = await readMarkdown(path6.join(dir, file), z13.record(z13.string(), z13.unknown()));
3490
3709
  const parsed = KnowledgeSchema.safeParse({
3491
3710
  ...doc.data,
3492
3711
  project: project2,
@@ -3886,6 +4105,21 @@ ${DASHBOARD_LINE}
3886
4105
  await this.events.append("paused.changed", actor.name, { paused });
3887
4106
  });
3888
4107
  }
4108
+ /** Sets the society's time zone, the user's alone: crons that name none are read in it. */
4109
+ async setTimezone(actor, zone) {
4110
+ this.assertUser(actor, "only the user sets the society's time zone");
4111
+ const timezone = TimeZoneSchema.parse(zone);
4112
+ return this.mutex.run(async () => {
4113
+ const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
4114
+ const society = { ...doc.data, timezone };
4115
+ await writeMarkdown(this.paths.societyFile(), society, doc.body);
4116
+ await this.events.append("society.configured", actor.name, {
4117
+ timezone,
4118
+ previousTimezone: doc.data.timezone
4119
+ });
4120
+ return society;
4121
+ });
4122
+ }
3889
4123
  /** Releases every claim whose lease has expired. Called by the scheduler on its tick. */
3890
4124
  async expireLeases() {
3891
4125
  return this.mutex.run(async () => {
@@ -4308,6 +4542,241 @@ ${current.body}`);
4308
4542
  return updated;
4309
4543
  });
4310
4544
  }
4545
+ /** Every cron, ended ones included, oldest first. */
4546
+ async listCrons() {
4547
+ const crons = [];
4548
+ for (const file of await listFiles(this.paths.crons())) {
4549
+ crons.push(await this.readCronFile(path6.join(this.paths.crons(), file)));
4550
+ }
4551
+ return crons;
4552
+ }
4553
+ async readCron(id) {
4554
+ const file = this.paths.cron(id);
4555
+ if (!await exists(file)) {
4556
+ throw new BoardError("NOT_FOUND", `cron ${id} not found`);
4557
+ }
4558
+ return this.readCronFile(file);
4559
+ }
4560
+ /**
4561
+ * Sets a cron: one that wakes its setter, or, set by a planning role, any citizen. Without a
4562
+ * place named it fires in the conversation of the turn that set it.
4563
+ */
4564
+ async createCron(actor, input) {
4565
+ const args = VerbInputs.create_cron.parse(input);
4566
+ await this.authorize(actor, "create_cron");
4567
+ const target = args.agent ?? actor.name;
4568
+ this.assertMayCron(actor, target, "set");
4569
+ if (args.cron === void 0 === (args.at === void 0)) {
4570
+ throw new BoardError("VALIDATION", "a cron needs either cron, a five-field expression, or at, one time");
4571
+ }
4572
+ return this.mutex.run(async () => {
4573
+ const agent2 = await this.readAgent(target);
4574
+ if (agent2.status !== "active" || agent2.cli === null) {
4575
+ throw new BoardError("INVALID_STATE", `${target} takes no turns, so no cron can wake it`);
4576
+ }
4577
+ const where = await this.cronConversation(actor, agent2, args);
4578
+ const now = this.now();
4579
+ const schedule = await this.cronSchedule(args, void 0, now);
4580
+ if (schedule === void 0) {
4581
+ throw new BoardError("VALIDATION", "a cron needs a schedule");
4582
+ }
4583
+ const cron2 = await this.writeCron({
4584
+ id: this.newId(),
4585
+ title: args.title,
4586
+ agent: agent2.name,
4587
+ ...where,
4588
+ schedule,
4589
+ createdBy: actor.name,
4590
+ createdAt: now.toISOString(),
4591
+ since: now.toISOString(),
4592
+ failures: 0,
4593
+ note: args.note
4594
+ });
4595
+ await this.events.append("cron.created", actor.name, {
4596
+ ...this.cronPayload(cron2),
4597
+ title: cron2.title,
4598
+ schedule: cron2.schedule
4599
+ });
4600
+ return cron2;
4601
+ });
4602
+ }
4603
+ /** Pauses, resumes, retitles, rewrites, or reschedules a cron; resuming or rescheduling starts it afresh. */
4604
+ async updateCron(actor, input) {
4605
+ const args = VerbInputs.update_cron.parse(input);
4606
+ await this.authorize(actor, "update_cron");
4607
+ if (args.cron !== void 0 && args.at !== void 0) {
4608
+ throw new BoardError("VALIDATION", "a cron fires on an expression or at one time, not both");
4609
+ }
4610
+ return this.mutex.run(async () => {
4611
+ const current = await this.readCron(args.cron_id);
4612
+ this.assertMayCron(actor, current.agent, "change");
4613
+ if (current.ended !== void 0) {
4614
+ throw new BoardError("INVALID_STATE", `cron ${current.id} has ended (${current.ended.reason})`);
4615
+ }
4616
+ const now = this.now();
4617
+ const schedule = await this.cronSchedule(args, current.schedule, now);
4618
+ const resumed = args.paused === false && current.paused !== void 0;
4619
+ const changed = [
4620
+ ...args.title === void 0 ? [] : ["title"],
4621
+ ...args.note === void 0 ? [] : ["note"],
4622
+ ...schedule === void 0 ? [] : ["schedule"],
4623
+ ...args.paused === true && current.paused === void 0 ? ["paused"] : [],
4624
+ ...resumed ? ["resumed"] : []
4625
+ ];
4626
+ if (changed.length === 0 && args.paused === void 0) {
4627
+ throw new BoardError("VALIDATION", "update_cron needs paused, title, note, cron, timezone, or at");
4628
+ }
4629
+ const ts = now.toISOString();
4630
+ const { paused: wasPaused, ...rest } = current;
4631
+ const paused = args.paused === void 0 ? wasPaused : args.paused ? wasPaused ?? { at: ts, by: actor.name } : void 0;
4632
+ const cron2 = await this.writeCron({
4633
+ ...rest,
4634
+ ...args.title === void 0 ? {} : { title: args.title },
4635
+ ...args.note === void 0 ? {} : { note: args.note },
4636
+ ...schedule === void 0 ? {} : { schedule },
4637
+ ...schedule !== void 0 || resumed ? { since: ts } : {},
4638
+ ...paused === void 0 ? {} : { paused }
4639
+ });
4640
+ if (changed.length > 0) {
4641
+ await this.events.append("cron.updated", actor.name, {
4642
+ ...this.cronPayload(cron2),
4643
+ changed,
4644
+ ...schedule === void 0 ? {} : { schedule }
4645
+ });
4646
+ }
4647
+ return cron2;
4648
+ });
4649
+ }
4650
+ /** Ends a cron for good: its setter's, or anyone's for a planning role. */
4651
+ async removeCron(actor, input) {
4652
+ const args = VerbInputs.remove_cron.parse(input);
4653
+ await this.authorize(actor, "remove_cron");
4654
+ return this.mutex.run(async () => {
4655
+ const current = await this.readCron(args.cron_id);
4656
+ this.assertMayCron(actor, current.agent, "end");
4657
+ if (current.ended !== void 0) {
4658
+ throw new BoardError("INVALID_STATE", `cron ${current.id} has ended already (${current.ended.reason})`);
4659
+ }
4660
+ return this.endCronUnlocked(current, actor.name, args.reason);
4661
+ });
4662
+ }
4663
+ /** A cron wakes its setter; one that wakes another citizen is the planning roles' to set, change, and end. */
4664
+ assertMayCron(actor, agent2, what) {
4665
+ if (agent2 !== actor.name && !PLANNING_ROLES.includes(actor.role)) {
4666
+ throw new BoardError("FORBIDDEN", `only the user, the steward, and the concierge ${what} a cron that wakes another citizen`);
4667
+ }
4668
+ }
4669
+ /**
4670
+ * Where a cron fires for its citizen: the thread, the channel, or the scope's home it names, else
4671
+ * the conversation of the turn that set it when it wakes its setter, else its home outside projects.
4672
+ */
4673
+ async cronConversation(actor, agent2, named) {
4674
+ const own = actor.name === agent2.name;
4675
+ if (named.project === void 0 && named.thread_id === void 0 && named.channel === void 0) {
4676
+ return own && actor.scope !== void 0 ? this.cronConversation(actor, agent2, {
4677
+ project: actor.scope,
4678
+ thread_id: actor.thread,
4679
+ channel: actor.channel
4680
+ }) : { scope: SOCIETY_SCOPE };
4681
+ }
4682
+ if (named.thread_id !== void 0) {
4683
+ const { thread: thread2 } = await this.findThread(named.thread_id);
4684
+ if (thread2.state !== "open") {
4685
+ throw new BoardError("INVALID_STATE", `thread ${thread2.id} is closed`);
4686
+ }
4687
+ const scope2 = wakeScope(agent2, parseChannelRef(thread2.channel).project);
4688
+ if (named.project !== void 0 && named.project !== scope2) {
4689
+ throw new BoardError("VALIDATION", `thread ${thread2.id} is on ${thread2.channel}, so ${agent2.name}'s conversation there is in ${scope2}, not ${named.project}`);
4690
+ }
4691
+ return { scope: scope2, thread: thread2.id };
4692
+ }
4693
+ const scope = named.project ?? (own ? actor.scope : void 0) ?? SOCIETY_SCOPE;
4694
+ if (scope !== SOCIETY_SCOPE) {
4695
+ await this.readActiveProject(scope);
4696
+ if (!agent2.memberships.includes(scope)) {
4697
+ throw new BoardError("VALIDATION", `${agent2.name} is not a member of ${scope}`);
4698
+ }
4699
+ }
4700
+ if (named.channel === void 0 || named.channel === "general") {
4701
+ return { scope };
4702
+ }
4703
+ await this.assertChannelOpen(channelRef(scope === SOCIETY_SCOPE ? null : scope, named.channel));
4704
+ return { scope, channel: named.channel };
4705
+ }
4706
+ /**
4707
+ * The schedule a cron verb asks for, or undefined when it asks for none: an expression, read in
4708
+ * its zone or the society's, which must name a time to come and no two fires closer than the
4709
+ * floor, or one time to come. A zone alone keeps the expression of `existing`.
4710
+ */
4711
+ async cronSchedule(args, existing, now) {
4712
+ if (args.at !== void 0) {
4713
+ if (args.timezone !== void 0) {
4714
+ throw new BoardError("VALIDATION", "at carries its own offset; timezone is for a cron expression");
4715
+ }
4716
+ const at = new Date(args.at);
4717
+ if (at <= now) {
4718
+ throw new BoardError("VALIDATION", `${args.at} has passed; a cron fires at a time to come`);
4719
+ }
4720
+ return { at: at.toISOString() };
4721
+ }
4722
+ if (args.cron === void 0 && args.timezone === void 0) {
4723
+ return void 0;
4724
+ }
4725
+ const expression = args.cron ?? (existing === void 0 || isOneTime(existing) ? void 0 : existing.cron);
4726
+ if (expression === void 0) {
4727
+ throw new BoardError("VALIDATION", "timezone is for a cron expression, which this cron has not");
4728
+ }
4729
+ const kept = args.timezone === void 0 && existing !== void 0 && !isOneTime(existing) ? existing.timezone : args.timezone ?? void 0;
4730
+ const schedule = {
4731
+ cron: expression,
4732
+ ...kept === void 0 ? {} : { timezone: kept }
4733
+ };
4734
+ const { timezone: societyZone } = await this.society();
4735
+ if (fireTimes(schedule, societyZone, now, 1).length === 0) {
4736
+ throw new BoardError("VALIDATION", `${expression} names no time to come`);
4737
+ }
4738
+ const gap = shortestGapMs(schedule, societyZone, now);
4739
+ if (gap !== null && gap < CRON_FLOOR_MS) {
4740
+ throw new BoardError("VALIDATION", `${expression} fires ${Math.round(gap / 6e4)} minute(s) apart at its closest; a cron fires at most every ${CRON_FLOOR_MS / 6e4} minutes, and every fire is a turn`);
4741
+ }
4742
+ return schedule;
4743
+ }
4744
+ /** A cron's events name its citizen and the conversation it fires in, as turn events do. */
4745
+ cronPayload(cron2) {
4746
+ return {
4747
+ cronId: cron2.id,
4748
+ agent: cron2.agent,
4749
+ project: cron2.scope,
4750
+ ...cron2.thread === void 0 ? {} : { thread: cron2.thread },
4751
+ ...cron2.channel === void 0 ? {} : { channel: cron2.channel }
4752
+ };
4753
+ }
4754
+ async readCronFile(file) {
4755
+ const doc = await readMarkdown(file, CronFrontmatterSchema);
4756
+ return { ...doc.data, note: doc.body.trimEnd() };
4757
+ }
4758
+ async writeCron(cron2) {
4759
+ const { note, ...frontmatter } = cron2;
4760
+ const data = CronFrontmatterSchema.parse(Object.fromEntries(Object.entries(frontmatter).filter(([, value]) => value !== void 0)));
4761
+ await writeMarkdown(this.paths.cron(cron2.id), data, note);
4762
+ return { ...data, note };
4763
+ }
4764
+ async endCronUnlocked(cron2, by, reason) {
4765
+ const ended = await this.writeCron({
4766
+ ...cron2,
4767
+ ended: { at: this.now().toISOString(), by, reason }
4768
+ });
4769
+ await this.events.append("cron.ended", by, { ...this.cronPayload(ended), reason });
4770
+ return ended;
4771
+ }
4772
+ /** Ends every running cron `ends` picks, in the transaction that ends the conversation it fires in. */
4773
+ async endCronsUnlocked(by, reason, ends) {
4774
+ for (const cron2 of await this.listCrons()) {
4775
+ if (cron2.ended === void 0 && ends(cron2)) {
4776
+ await this.endCronUnlocked(cron2, by, reason);
4777
+ }
4778
+ }
4779
+ }
4311
4780
  async openThread(actor, input) {
4312
4781
  const args = VerbInputs.open_thread.parse(input);
4313
4782
  await this.authorize(actor, "open_thread");
@@ -4524,6 +4993,7 @@ ${args.summary}`);
4524
4993
  threadId: thread2.id,
4525
4994
  channel: thread2.channel
4526
4995
  });
4996
+ await this.endCronsUnlocked(actor.name, "its thread closed", (cron2) => cron2.thread === thread2.id);
4527
4997
  return summary;
4528
4998
  });
4529
4999
  }
@@ -5025,6 +5495,13 @@ ${args.summary}`);
5025
5495
  proposedBy: actor.name,
5026
5496
  status: "proposed",
5027
5497
  createdAt: this.now().toISOString(),
5498
+ ...actor.scope === void 0 ? {} : {
5499
+ filedFrom: {
5500
+ scope: actor.scope,
5501
+ ...actor.thread === void 0 ? {} : { thread: actor.thread },
5502
+ ...actor.channel === void 0 ? {} : { channel: actor.channel }
5503
+ }
5504
+ },
5028
5505
  charter: parsed.data
5029
5506
  }),
5030
5507
  body: args.rationale
@@ -5196,6 +5673,7 @@ ${rationale}`}`);
5196
5673
  members: project2.members.filter((member) => member !== target)
5197
5674
  }));
5198
5675
  await this.refreshMember(target);
5676
+ await this.endCronsUnlocked(actor.name, `${target} left ${args.project}`, (cron2) => cron2.agent === target && cron2.scope === args.project);
5199
5677
  await this.events.append("agent.left", actor.name, {
5200
5678
  name: target,
5201
5679
  project: args.project,
@@ -5264,6 +5742,12 @@ ${rationale}`}`);
5264
5742
  return this.archiveChannel(actor, VerbInputs.archive_channel.parse(input));
5265
5743
  case "update_dashboard":
5266
5744
  return this.updateDashboard(actor, VerbInputs.update_dashboard.parse(input));
5745
+ case "create_cron":
5746
+ return this.createCron(actor, VerbInputs.create_cron.parse(input));
5747
+ case "update_cron":
5748
+ return this.updateCron(actor, VerbInputs.update_cron.parse(input));
5749
+ case "remove_cron":
5750
+ return this.removeCron(actor, VerbInputs.remove_cron.parse(input));
5267
5751
  default:
5268
5752
  throw new BoardError("VALIDATION", `unknown verb ${String(verb)}`);
5269
5753
  }
@@ -5310,7 +5794,7 @@ ${rationale}`}`);
5310
5794
  }
5311
5795
  async readAgentRoleBody(agent2) {
5312
5796
  const file = this.paths.agentRole(agent2);
5313
- return await exists(file) ? (await readMarkdown(file, z12.record(z12.string(), z12.unknown()))).body : "";
5797
+ return await exists(file) ? (await readMarkdown(file, z13.record(z13.string(), z13.unknown()))).body : "";
5314
5798
  }
5315
5799
  /** The steps of one finished turn, as the runner handed them over when it ended. */
5316
5800
  /**
@@ -5488,6 +5972,9 @@ ${rationale}`}`);
5488
5972
  }
5489
5973
  await this.writeTurnRecord(parsed);
5490
5974
  await this.refreshMember(parsed.agent);
5975
+ for (const id of parsed.crons ?? []) {
5976
+ await this.recordCronRun(id, parsed, failed);
5977
+ }
5491
5978
  if (parsed.exitReason === "stopped") {
5492
5979
  await this.events.append("turn.stopped", parsed.stoppedBy ?? USER_NAME, {
5493
5980
  ...parsed.id === void 0 ? {} : { turnId: parsed.id },
@@ -5525,6 +6012,29 @@ ${rationale}`}`);
5525
6012
  });
5526
6013
  });
5527
6014
  }
6015
+ /**
6016
+ * Records how closing an abandoned `ghpr` task's pull request on GitHub went, once: the task's
6017
+ * link keeps it, so a restart does not try again.
6018
+ */
6019
+ async recordPullRequestClosed(actor, input) {
6020
+ return this.mutex.run(async () => {
6021
+ const location = await this.findTask(input.taskId);
6022
+ const current = location.task;
6023
+ const pull = current.pullRequest;
6024
+ if (pull === void 0 || pull.closed !== void 0) {
6025
+ return current;
6026
+ }
6027
+ const task2 = await this.writeTask(location.project, {
6028
+ ...current,
6029
+ pullRequest: {
6030
+ ...pull,
6031
+ closed: { at: this.now().toISOString(), ok: input.ok, detail: input.detail }
6032
+ }
6033
+ });
6034
+ await this.events.append(input.ok ? "pull_request.closed" : "pull_request.close_failed", actor.name, { project: location.project, taskId: task2.id, url: pull.url, detail: input.detail });
6035
+ return task2;
6036
+ });
6037
+ }
5528
6038
  /**
5529
6039
  * Records that a citizen's workspace in a conversation that has ended holds work on no branch, so
5530
6040
  * the scheduler gives the citizen a closing turn there to decide about it. False, recording
@@ -5546,6 +6056,45 @@ ${rationale}`}`);
5546
6056
  return true;
5547
6057
  });
5548
6058
  }
6059
+ /**
6060
+ * Records a cron's fire, which the scheduler made; a one-time cron ends with it. Null, recording
6061
+ * nothing, for a cron ended or paused since the scheduler read it, which does not fire.
6062
+ */
6063
+ async cronFired(id, at) {
6064
+ return this.mutex.run(async () => {
6065
+ const current = await this.readCron(id);
6066
+ if (current.ended !== void 0 || current.paused !== void 0) {
6067
+ return null;
6068
+ }
6069
+ const cron2 = await this.writeCron({ ...current, lastFiredAt: at });
6070
+ await this.events.append("cron.fired", SYSTEM_ACTOR.name, this.cronPayload(cron2));
6071
+ return isOneTime(cron2.schedule) ? this.endCronUnlocked(cron2, SYSTEM_ACTOR.name, "fired once, as set") : cron2;
6072
+ });
6073
+ }
6074
+ /** Records a time a cron passed because the turn of its previous fire was still queued or running. */
6075
+ async cronSkipped(id, at) {
6076
+ return this.mutex.run(async () => {
6077
+ const cron2 = await this.writeCron({ ...await this.readCron(id), lastSkippedAt: at });
6078
+ await this.events.append("cron.skipped", SYSTEM_ACTOR.name, this.cronPayload(cron2));
6079
+ return cron2;
6080
+ });
6081
+ }
6082
+ /**
6083
+ * How the turn a cron's fire started ended. A failure counts toward `cron_failing`, a completed
6084
+ * turn clears the count, and a stop, the user's decision, leaves it.
6085
+ */
6086
+ async recordCronRun(id, record, failed) {
6087
+ if (record.id === void 0 || !await exists(this.paths.cron(id))) {
6088
+ return;
6089
+ }
6090
+ const cron2 = await this.readCron(id);
6091
+ const outcome = record.exitReason === "stopped" ? "stopped" : failed ? "failed" : "completed";
6092
+ await this.writeCron({
6093
+ ...cron2,
6094
+ lastRun: { turnId: record.id, at: record.endedAt ?? this.now().toISOString(), outcome },
6095
+ failures: outcome === "failed" ? cron2.failures + 1 : outcome === "completed" ? 0 : cron2.failures
6096
+ });
6097
+ }
5549
6098
  /**
5550
6099
  * Whether a conversation with a workspace of its own has ended: a thread closed, a task ended
5551
6100
  * (for one from before tasks had threads), or a channel archived.
@@ -5645,7 +6194,7 @@ ${rationale}`}`);
5645
6194
  return { project: slug, revision: 0, body: "" };
5646
6195
  }
5647
6196
  const { data, body } = await readLooseMarkdown(file);
5648
- const revision = z12.number().int().min(0).safeParse(data["revision"]);
6197
+ const revision = z13.number().int().min(0).safeParse(data["revision"]);
5649
6198
  const updatedBy = NameSchema.safeParse(data["updatedBy"]);
5650
6199
  const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
5651
6200
  return {
@@ -5936,7 +6485,7 @@ ${seed}
5936
6485
  continue;
5937
6486
  }
5938
6487
  const file = this.paths.agentRole(agent2.name);
5939
- const body = await exists(file) ? (await readMarkdown(file, z12.record(z12.string(), z12.unknown()))).body : "";
6488
+ const body = await exists(file) ? (await readMarkdown(file, z13.record(z13.string(), z13.unknown()))).body : "";
5940
6489
  const at = body.indexOf(`
5941
6490
  ${SEED_INSTRUCTIONS_HEADING}
5942
6491
  `);
@@ -6380,6 +6929,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
6380
6929
  archivedChannels: [...current.archivedChannels, archived]
6381
6930
  }));
6382
6931
  }
6932
+ await this.endCronsUnlocked(by, `${ref} was archived`, (cron2) => cron2.scope === scope && cron2.channel === channel || cron2.thread !== void 0 && threadsClosed.includes(cron2.thread));
6383
6933
  await this.events.append("channel.archived", by, {
6384
6934
  channel: ref,
6385
6935
  reason,
@@ -6484,6 +7034,7 @@ ${charter.purpose}
6484
7034
  });
6485
7035
  threadsClosed.push(doc.data.id);
6486
7036
  }
7037
+ await this.endCronsUnlocked(by, `project ${slug} was archived`, (cron2) => cron2.scope === slug || cron2.thread !== void 0 && threadsClosed.includes(cron2.thread));
6487
7038
  const project2 = await this.updateProject(slug, (p) => ({
6488
7039
  ...p,
6489
7040
  members: [],
@@ -6548,6 +7099,7 @@ ${charter.purpose}
6548
7099
  }
6549
7100
  this.tokenIndex.delete(current.tokenHash);
6550
7101
  await this.refreshMember(name);
7102
+ await this.endCronsUnlocked(by, `${name} was retired`, (cron2) => cron2.agent === name);
6551
7103
  await this.events.append("agent.retired", by, {
6552
7104
  name,
6553
7105
  role: agent2.role,
@@ -6757,7 +7309,8 @@ ${charter.purpose}
6757
7309
  proposalId,
6758
7310
  outcome,
6759
7311
  kind: proposal2.kind,
6760
- proposedBy: proposal2.proposedBy
7312
+ proposedBy: proposal2.proposedBy,
7313
+ ...proposal2.filedFrom === void 0 ? {} : { filedFrom: proposal2.filedFrom }
6761
7314
  });
6762
7315
  if (provision !== void 0) {
6763
7316
  await this.events.append("proposal.provisioned", actor.name, {
@@ -6858,6 +7411,7 @@ ${charter.purpose}
6858
7411
  channel: found.thread.channel,
6859
7412
  ended
6860
7413
  });
7414
+ await this.endCronsUnlocked(by, `its thread closed: its ${found.thread.subject?.kind ?? "subject"} was ${ended}`, (cron2) => cron2.thread === subjectId);
6861
7415
  }
6862
7416
  async readMessagesIn(dir, since) {
6863
7417
  const messages = [];
@@ -7293,6 +7847,66 @@ program.command("resume").description("Allow wakeups again").action(async () =>
7293
7847
  await board.setPaused(board.userActor(), false);
7294
7848
  print({ paused: false }, () => "Society resumed");
7295
7849
  });
7850
+ program.command("society").description("The society's settings").command("timezone [zone]").description("Show the society's time zone, or set it: crons that name none are read in it").action(async (zone) => {
7851
+ const board = await open();
7852
+ const society = zone === void 0 ? await board.society() : await board.setTimezone(board.userActor(), zone);
7853
+ print({ timezone: society.timezone }, () => `Time zone: ${society.timezone}`);
7854
+ });
7855
+ function describeCron(cron2, timeZone) {
7856
+ const where = [
7857
+ cron2.scope,
7858
+ cron2.thread,
7859
+ cron2.channel === void 0 ? void 0 : `#${cron2.channel}`
7860
+ ].filter((part) => part !== void 0).join("/");
7861
+ const next = nextFire(cron2, timeZone);
7862
+ const state = cron2.ended !== void 0 ? `ended (${cron2.ended.reason})` : cron2.paused !== void 0 ? "paused" : next === null ? "no time to come" : `next ${next.toISOString()}`;
7863
+ return `${cron2.id} ${cron2.agent} ${where} ${describeSchedule(cron2.schedule, timeZone)} ${state} ${cron2.title}`;
7864
+ }
7865
+ var cron = program.command("cron").description("Crons that wake citizens on a clock");
7866
+ cron.command("list").description("List crons, ended ones too with --all").option("--all", "include ended crons", false).action(async (opts) => {
7867
+ const board = await open();
7868
+ const { timezone } = await board.society();
7869
+ const crons = (await board.listCrons()).filter((each) => opts.all || each.ended === void 0);
7870
+ print(crons, () => crons.length === 0 ? "No crons." : crons.map((each) => describeCron(each, timezone)).join("\n"));
7871
+ });
7872
+ cron.command("add <agent> <title>").description("Set a cron that wakes a citizen, with a note on what to do then").requiredOption("--note <text>", "what the citizen does when it fires").option("--cron <expression>", "five fields: minute hour day-of-month month day-of-week").option("--timezone <zone>", "the zone the expression is read in; the society's otherwise").option("--at <time>", "one time to fire, with its offset, instead of --cron").option("--project <slug>", "where it fires: a project the citizen is in, or society").option("--thread <id>", "an open thread there whose conversation it fires in").option("--channel <name>", "an open channel there, other than general, whose conversation it fires in").option("--as <agent>", "act as this agent instead of the user").action(async (agentName, title, opts) => {
7873
+ const board = await open();
7874
+ const created = await board.createCron(await actorFor(board, opts.as), {
7875
+ title,
7876
+ note: opts.note,
7877
+ agent: agentName,
7878
+ ...opts.cron === void 0 ? {} : { cron: opts.cron },
7879
+ ...opts.timezone === void 0 ? {} : { timezone: opts.timezone },
7880
+ ...opts.at === void 0 ? {} : { at: opts.at },
7881
+ ...opts.project === void 0 ? {} : { project: opts.project },
7882
+ ...opts.thread === void 0 ? {} : { thread_id: opts.thread },
7883
+ ...opts.channel === void 0 ? {} : { channel: opts.channel }
7884
+ });
7885
+ const { timezone } = await board.society();
7886
+ print(created, () => `Cron set: ${describeCron(created, timezone)}`);
7887
+ });
7888
+ for (const [name, paused] of [
7889
+ ["pause", true],
7890
+ ["resume", false]
7891
+ ]) {
7892
+ cron.command(`${name} <id>`).description(paused ? "Pause a cron" : "Resume a paused cron from its next time").option("--as <agent>", "act as this agent instead of the user").action(async (id, opts) => {
7893
+ const board = await open();
7894
+ const updated = await board.updateCron(await actorFor(board, opts.as), {
7895
+ cron_id: id,
7896
+ paused
7897
+ });
7898
+ const { timezone } = await board.society();
7899
+ print(updated, () => describeCron(updated, timezone));
7900
+ });
7901
+ }
7902
+ cron.command("remove <id>").description("End a cron for good").requiredOption("--reason <text>", "why it ends").option("--as <agent>", "act as this agent instead of the user").action(async (id, opts) => {
7903
+ const board = await open();
7904
+ const removed = await board.removeCron(await actorFor(board, opts.as), {
7905
+ cron_id: id,
7906
+ reason: opts.reason
7907
+ });
7908
+ print(removed, () => `Cron ${removed.id} ended`);
7909
+ });
7296
7910
  var knowledge = program.command("knowledge").description("Shared knowledge, by project or society");
7297
7911
  knowledge.command("list [project]").description("List knowledge topics of a project, or of the society when no project is given").action(async (projectSlug) => {
7298
7912
  const board = await open();