@kubitnodes/stellaris 0.3.13 → 0.3.15

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 (54) hide show
  1. package/dist/cli/index.js +1101 -539
  2. package/dist/server/index.js +1262 -592
  3. package/package.json +2 -1
  4. package/web/dist/assets/{BackLink-d0xz7ZLb.js → BackLink-hLg9AYSp.js} +1 -1
  5. package/web/dist/assets/{ChannelForms-BIRwT9CY.js → ChannelForms-b6eKogvP.js} +1 -1
  6. package/web/dist/assets/ChannelView-CHvr0c9I.js +1 -0
  7. package/web/dist/assets/CitizenView-DlqilbFH.js +2 -0
  8. package/web/dist/assets/{CitizensView-Bv04iH-y.js → CitizensView-Dq3V4mpg.js} +1 -1
  9. package/web/dist/assets/{CliIcon-tYTwI_RQ.js → CliIcon-DxDvRRcX.js} +1 -1
  10. package/web/dist/assets/CronsView-CBiPaq-C.js +1 -0
  11. package/web/dist/assets/KnowledgeView-ndWDlRoP.js +1 -0
  12. package/web/dist/assets/{Markdown-BLiIulgZ.js → Markdown-BMm7unnl.js} +4 -4
  13. package/web/dist/assets/{MessageItem-CHrmjF7P.js → MessageItem-Dk1m5NaR.js} +1 -1
  14. package/web/dist/assets/{MetricsView-DG1alTgs.js → MetricsView-B31t_Q2z.js} +1 -1
  15. package/web/dist/assets/{NeedsYouView-Doc0fKPi.js → NeedsYouView-d8_ydJI9.js} +1 -1
  16. package/web/dist/assets/{Overview-ChsOgIEW.js → Overview-D-6YP-tf.js} +1 -1
  17. package/web/dist/assets/{Pane-MJgGXQBb.js → Pane-CODhA5zE.js} +1 -1
  18. package/web/dist/assets/Picker-DB-wxloY.js +1 -0
  19. package/web/dist/assets/ProjectView-CH-nqrAo.js +1 -0
  20. package/web/dist/assets/ProposalView-D-E0Hzdr.js +1 -0
  21. package/web/dist/assets/{ProposalsView-CSA8neXR.js → ProposalsView-Bmuq8Qnh.js} +1 -1
  22. package/web/dist/assets/{RunnersView-Dx21LDFu.js → RunnersView-N4iUU4FM.js} +1 -1
  23. package/web/dist/assets/{SocietyView-CdVW2DgG.js → SocietyView-fSkmFC3z.js} +1 -1
  24. package/web/dist/assets/SubjectThread-akZdGCfo.js +1 -0
  25. package/web/dist/assets/{TaskFileView-BqqzxJKb.js → TaskFileView-DM_JjQQx.js} +2 -2
  26. package/web/dist/assets/TaskView-JLLJKQ0_.js +3 -0
  27. package/web/dist/assets/{TasksView-Ba5fPAvy.js → TasksView-CncU8UNJ.js} +1 -1
  28. package/web/dist/assets/ThreadCard-8OB70j9B.js +1 -0
  29. package/web/dist/assets/{ThreadForms-ChTFRu-P.js → ThreadForms-RnrV7V5X.js} +1 -1
  30. package/web/dist/assets/{ThreadView-CnJPNS_8.js → ThreadView-DrwqggME.js} +1 -1
  31. package/web/dist/assets/citizen-BRSvyK2X.js +1 -0
  32. package/web/dist/assets/{compose-BgpUWGYE.js → compose-D7DOz6kC.js} +1 -1
  33. package/web/dist/assets/format-BORdp4w-.js +55 -0
  34. package/web/dist/assets/{governance-Di4MijNG.js → governance-WrDQawLN.js} +1 -1
  35. package/web/dist/assets/index-Dw96wkep.css +2 -0
  36. package/web/dist/assets/index-ww_EHgVF.js +23 -0
  37. package/web/dist/assets/{link-DAMxBItI.js → link-1v5lugnf.js} +1 -1
  38. package/web/dist/assets/{tasks-bwj_EdwF.js → tasks-C-9dAYQV.js} +1 -1
  39. package/web/dist/assets/useMatch-CyeFfbcR.js +1 -0
  40. package/web/dist/assets/{useStickyScroll-BILPUgGj.js → useStickyScroll-CE7dyz0Q.js} +1 -1
  41. package/web/dist/index.html +9 -9
  42. package/web/dist/assets/ChannelView-BvMlzMiN.js +0 -1
  43. package/web/dist/assets/CitizenView-DNQwjpIp.js +0 -2
  44. package/web/dist/assets/KnowledgeView-L0pdaM_2.js +0 -1
  45. package/web/dist/assets/ProjectView-CQTn7yDo.js +0 -1
  46. package/web/dist/assets/ProposalView-C92OJ_w1.js +0 -1
  47. package/web/dist/assets/SubjectThread-egXeFhT1.js +0 -1
  48. package/web/dist/assets/TaskView-rks26vuF.js +0 -3
  49. package/web/dist/assets/ThreadCard-D6i69_15.js +0 -1
  50. package/web/dist/assets/citizen-D-dxX00V.js +0 -1
  51. package/web/dist/assets/format-C3aawMM6.js +0 -55
  52. package/web/dist/assets/index-VVnGdAXc.css +0 -2
  53. package/web/dist/assets/index-krHkeIbX.js +0 -23
  54. package/web/dist/assets/useMatch-B-8IcuU6.js +0 -1
@@ -9,7 +9,7 @@ import { serve } from "@hono/node-server";
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,141 @@ 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 CRON_FAILING_RUNS = 3;
178
+ var DEFAULT_TIME_ZONE = "UTC";
179
+ function canonicalTimeZone(zone) {
180
+ try {
181
+ return new Intl.DateTimeFormat("en-US", { timeZone: zone }).resolvedOptions().timeZone;
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+ var TimeZoneSchema = z3.string().trim().min(1).max(64).transform((zone, ctx) => {
187
+ const canonical = canonicalTimeZone(zone);
188
+ if (canonical === null) {
189
+ ctx.addIssue({
190
+ code: "custom",
191
+ message: `${zone} is not a time zone; name one as IANA does, such as Europe/Berlin, America/New_York, or UTC`
192
+ });
193
+ return z3.NEVER;
194
+ }
195
+ return canonical;
196
+ });
197
+ function pattern(expression, timeZone) {
198
+ return new Pattern(expression, { mode: "5-part", timezone: timeZone, paused: true });
199
+ }
200
+ function cronExpressionError(expression) {
201
+ try {
202
+ pattern(expression, DEFAULT_TIME_ZONE);
203
+ return null;
204
+ } catch (error) {
205
+ return error instanceof Error ? error.message : String(error);
206
+ }
207
+ }
208
+ var CronExpressionSchema = z3.string().trim().min(1).max(120).superRefine((expression, ctx) => {
209
+ const error = cronExpressionError(expression);
210
+ if (error !== null) {
211
+ ctx.addIssue({
212
+ code: "custom",
213
+ message: `not a five-field cron expression (minute, hour, day of month, month, day of week): ${error}`
214
+ });
215
+ }
216
+ });
217
+ var CronScheduleSchema = z3.union([
218
+ z3.object({ cron: CronExpressionSchema, timezone: TimeZoneSchema.optional() }),
219
+ z3.object({ at: IsoDateTimeSchema })
220
+ ]);
221
+ var CronRunSchema = z3.object({
222
+ turnId: UlidSchema,
223
+ at: IsoDateTimeSchema,
224
+ outcome: z3.enum(["completed", "failed", "stopped"])
225
+ });
226
+ var CronFrontmatterSchema = z3.object({
227
+ id: UlidSchema,
228
+ title: z3.string().trim().min(1).max(120),
229
+ agent: NameSchema,
230
+ scope: NameSchema,
231
+ thread: UlidSchema.optional(),
232
+ channel: NameSchema.optional(),
233
+ schedule: CronScheduleSchema,
234
+ createdBy: NameSchema,
235
+ createdAt: IsoDateTimeSchema,
236
+ /** When the current schedule took effect: set, changed, or resumed. It fires only after this. */
237
+ since: IsoDateTimeSchema,
238
+ paused: z3.object({ at: IsoDateTimeSchema, by: NameSchema }).optional(),
239
+ lastFiredAt: IsoDateTimeSchema.optional(),
240
+ /** When a time came while its previous fire's turn was still queued or running, so it passed. */
241
+ lastSkippedAt: IsoDateTimeSchema.optional(),
242
+ /** How the turn of its latest fire ended. */
243
+ lastRun: CronRunSchema.optional(),
244
+ /** Its turns that failed in a row, up to the latest. */
245
+ failures: z3.number().int().nonnegative().default(0),
246
+ ended: z3.object({ at: IsoDateTimeSchema, by: NameSchema, reason: z3.string().min(1) }).optional()
247
+ });
248
+ function isOneTime(schedule) {
249
+ return "at" in schedule;
250
+ }
251
+ function scheduleZone(schedule, societyZone) {
252
+ return isOneTime(schedule) ? societyZone : schedule.timezone ?? societyZone;
253
+ }
254
+ function fireTimes(schedule, societyZone, after, count) {
255
+ if (isOneTime(schedule)) {
256
+ const at = new Date(schedule.at);
257
+ return at > after && count > 0 ? [at] : [];
258
+ }
259
+ return pattern(schedule.cron, scheduleZone(schedule, societyZone)).nextRuns(count, after);
260
+ }
261
+ function shortestGapMs(schedule, societyZone, after) {
262
+ const times = fireTimes(schedule, societyZone, after, FLOOR_LOOKAHEAD);
263
+ let shortest = null;
264
+ for (let index = 1; index < times.length; index += 1) {
265
+ const gap = (times[index]?.getTime() ?? 0) - (times[index - 1]?.getTime() ?? 0);
266
+ shortest = shortest === null ? gap : Math.min(shortest, gap);
267
+ }
268
+ return shortest;
269
+ }
270
+ function nextFire(cron, societyZone, floorMs = CRON_FLOOR_MS) {
271
+ if (cron.ended !== void 0 || cron.paused !== void 0) {
272
+ return null;
273
+ }
274
+ const fired = cron.lastFiredAt === void 0 ? null : Date.parse(cron.lastFiredAt);
275
+ const since = Date.parse(cron.since);
276
+ if (isOneTime(cron.schedule)) {
277
+ return fired !== null && fired >= since ? null : new Date(cron.schedule.at);
278
+ }
279
+ const from = Math.max(since, fired === null ? since : fired + floorMs - FIRE_SLACK_MS, cron.lastSkippedAt === void 0 ? since : Date.parse(cron.lastSkippedAt));
280
+ return fireTimes(cron.schedule, societyZone, new Date(from), 1)[0] ?? null;
281
+ }
282
+ function describeSchedule(schedule, societyZone) {
283
+ if (isOneTime(schedule)) {
284
+ return `once at ${schedule.at}`;
285
+ }
286
+ return schedule.timezone === void 0 ? `\`${schedule.cron}\` in the society's time zone, ${societyZone}` : `\`${schedule.cron}\` in ${schedule.timezone}`;
287
+ }
288
+ function localTime(date, timeZone) {
289
+ const parts = new Intl.DateTimeFormat("en-US", {
290
+ timeZone,
291
+ weekday: "long",
292
+ year: "numeric",
293
+ month: "2-digit",
294
+ day: "2-digit",
295
+ hour: "2-digit",
296
+ minute: "2-digit",
297
+ hourCycle: "h23"
298
+ }).formatToParts(date);
299
+ const part = (type) => parts.find((each) => each.type === type)?.value ?? "";
300
+ return `${part("weekday")} ${part("year")}-${part("month")}-${part("day")} ${part("hour")}:${part("minute")}`;
301
+ }
302
+
303
+ // packages/shared/dist/board.js
164
304
  var ASK_CHANNEL = "asks";
165
305
  var SOCIETY_CHANNELS = ["general", "governance", ASK_CHANNEL];
166
306
  var PROJECT_DEFAULT_CHANNELS = ["general"];
@@ -172,30 +312,32 @@ var SOCIETY_SCOPE = "society";
172
312
  function wakeScope(member, project) {
173
313
  return project !== null && member.memberships.includes(project) ? project : SOCIETY_SCOPE;
174
314
  }
175
- var CliKindSchema = z3.enum(["claude", "codex"]);
176
- var ArchivedChannelSchema = z3.object({
315
+ var CliKindSchema = z4.enum(["claude", "codex"]);
316
+ var ArchivedChannelSchema = z4.object({
177
317
  name: NameSchema,
178
318
  at: IsoDateTimeSchema,
179
319
  by: NameSchema,
180
- reason: z3.string().min(1)
320
+ reason: z4.string().min(1)
181
321
  });
182
- var SocietySchema = z3.object({
183
- name: z3.string().min(1),
184
- version: z3.literal(1),
322
+ var SocietySchema = z4.object({
323
+ name: z4.string().min(1),
324
+ version: z4.literal(1),
185
325
  createdAt: IsoDateTimeSchema,
186
326
  /** The channels that take posts; an archived one moves to `archivedChannels`. */
187
- channels: z3.array(NameSchema),
188
- archivedChannels: z3.array(ArchivedChannelSchema).default([])
327
+ channels: z4.array(NameSchema),
328
+ archivedChannels: z4.array(ArchivedChannelSchema).default([]),
329
+ /** The time zone crons are read in when they name none, and every prompt tells the time in; the user's to set. */
330
+ timezone: TimeZoneSchema.default(DEFAULT_TIME_ZONE)
189
331
  });
190
- var CompletionEffectSchema = z3.enum(["none", "merge", "ghpr"]);
332
+ var CompletionEffectSchema = z4.enum(["none", "merge", "ghpr"]);
191
333
  var GITHUB_REMOTE = [
192
334
  /^https:\/\/github\.com\/([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?$/,
193
335
  /^git@github\.com:([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?$/,
194
336
  /^ssh:\/\/git@github\.com\/([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?$/
195
337
  ];
196
338
  function githubRepository(remote) {
197
- for (const pattern of GITHUB_REMOTE) {
198
- const match = remote === null ? null : pattern.exec(remote);
339
+ for (const pattern2 of GITHUB_REMOTE) {
340
+ const match = remote === null ? null : pattern2.exec(remote);
199
341
  if (match?.[1] !== void 0 && match[2] !== void 0) {
200
342
  return { owner: match[1], name: match[2] };
201
343
  }
@@ -203,7 +345,7 @@ function githubRepository(remote) {
203
345
  return null;
204
346
  }
205
347
  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>");
348
+ var PullRequestUrlSchema = z4.string().trim().regex(PULL_REQUEST_URL, "a pull request's address, https://github.com/<owner>/<repo>/pull/<number>");
207
349
  function parsePullRequestUrl(url) {
208
350
  const match = PULL_REQUEST_URL.exec(url.trim());
209
351
  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,65 +353,65 @@ function parsePullRequestUrl(url) {
211
353
  function sameRepository(a, b) {
212
354
  return a.owner.toLowerCase() === b.owner.toLowerCase() && a.name.toLowerCase() === b.name.toLowerCase();
213
355
  }
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({
356
+ var MergeSubjectSchema = z4.string().trim().min(1).max(300).regex(/^[^\n]+$/, "a merge commit's subject is one line");
357
+ var PullRequestLinkSchema = z4.object({
216
358
  url: PullRequestUrlSchema,
217
- number: z3.number().int().positive(),
359
+ number: z4.number().int().positive(),
218
360
  subject: MergeSubjectSchema.optional(),
219
- body: z3.string().max(2e4).optional(),
361
+ body: z4.string().max(2e4).optional(),
220
362
  linkedBy: NameSchema,
221
363
  linkedAt: IsoDateTimeSchema,
222
364
  /**
223
365
  * Once its `ghpr` task was abandoned, how the board's closing of it on GitHub went: `ok` false
224
366
  * leaves it open, for the reason `detail` gives.
225
367
  */
226
- closed: z3.object({ at: IsoDateTimeSchema, ok: z3.boolean(), detail: z3.string() }).optional()
368
+ closed: z4.object({ at: IsoDateTimeSchema, ok: z4.boolean(), detail: z4.string() }).optional()
227
369
  });
228
- 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");
229
- var StageIdSchema = z3.string().regex(/^s[1-9][0-9]*$/, "expected a stage id such as s1");
370
+ 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");
371
+ var StageIdSchema = z4.string().regex(/^s[1-9][0-9]*$/, "expected a stage id such as s1");
230
372
  var stageFields = {
231
- name: z3.string().trim().min(1).max(120),
373
+ name: z4.string().trim().min(1).max(120),
232
374
  role: NameSchema.optional(),
233
375
  agent: NameSchema.optional(),
234
- gate: z3.boolean().default(false)
376
+ gate: z4.boolean().default(false)
235
377
  };
236
378
  function oneAssignee(stage) {
237
379
  return stage.role === void 0 || stage.agent === void 0;
238
380
  }
239
381
  var ONE_ASSIGNEE = { message: "a stage names a role or an agent, not both" };
240
- var PlanStageSchema = z3.object(stageFields).refine(oneAssignee, ONE_ASSIGNEE);
241
- var PlanEditStageSchema = z3.object({ id: StageIdSchema.optional(), ...stageFields }).refine(oneAssignee, ONE_ASSIGNEE);
242
- var PlanSchema = z3.array(PlanStageSchema);
243
- var PlanEditSchema = z3.array(PlanEditStageSchema);
244
- var ProjectSchema = z3.object({
382
+ var PlanStageSchema = z4.object(stageFields).refine(oneAssignee, ONE_ASSIGNEE);
383
+ var PlanEditStageSchema = z4.object({ id: StageIdSchema.optional(), ...stageFields }).refine(oneAssignee, ONE_ASSIGNEE);
384
+ var PlanSchema = z4.array(PlanStageSchema);
385
+ var PlanEditSchema = z4.array(PlanEditStageSchema);
386
+ var ProjectSchema = z4.object({
245
387
  slug: NameSchema,
246
- name: z3.string().min(1),
247
- repo: z3.string().nullable(),
248
- defaultBranch: z3.string().min(1),
388
+ name: z4.string().min(1),
389
+ repo: z4.string().nullable(),
390
+ defaultBranch: z4.string().min(1),
249
391
  /** The channels that take posts; an archived one moves to `archivedChannels`. */
250
- channels: z3.array(NameSchema),
251
- archivedChannels: z3.array(ArchivedChannelSchema).default([]),
252
- members: z3.array(NameSchema),
253
- approvers: z3.array(NameSchema),
254
- requiredCapabilities: z3.array(z3.string()),
392
+ channels: z4.array(NameSchema),
393
+ archivedChannels: z4.array(ArchivedChannelSchema).default([]),
394
+ members: z4.array(NameSchema),
395
+ approvers: z4.array(NameSchema),
396
+ requiredCapabilities: z4.array(z4.string()),
255
397
  createdAt: IsoDateTimeSchema,
256
398
  onDone: CompletionEffectSchema.default("none"),
257
399
  /** The runner the project lives on, which holds its repository; set on the project's first turn. */
258
400
  runner: NameSchema.optional(),
259
401
  /** Set once the project is archived: nobody belongs to it, and nothing more is posted, filed, or joined there. */
260
- archived: z3.object({ at: IsoDateTimeSchema, by: NameSchema, reason: z3.string().min(1) }).optional()
402
+ archived: z4.object({ at: IsoDateTimeSchema, by: NameSchema, reason: z4.string().min(1) }).optional()
261
403
  });
262
- var TaskStepSchema = z3.object({
263
- action: z3.enum(["advanced", "returned", "abandoned", "landed", "reopened"]),
404
+ var TaskStepSchema = z4.object({
405
+ action: z4.enum(["advanced", "returned", "abandoned", "landed", "reopened"]),
264
406
  stage: StageIdSchema,
265
407
  to: StageIdSchema.nullable()
266
408
  });
267
- var ProposalStepSchema = z3.object({ action: z3.enum(["approved", "rejected"]) });
268
- var MessageStepSchema = z3.union([TaskStepSchema, ProposalStepSchema]);
409
+ var ProposalStepSchema = z4.object({ action: z4.enum(["approved", "rejected"]) });
410
+ var MessageStepSchema = z4.union([TaskStepSchema, ProposalStepSchema]);
269
411
  function isTaskStep(step) {
270
412
  return "stage" in step;
271
413
  }
272
- var MessageFrontmatterSchema = z3.object({
414
+ var MessageFrontmatterSchema = z4.object({
273
415
  id: UlidSchema,
274
416
  author: NameSchema,
275
417
  channel: ChannelRefSchema,
@@ -281,18 +423,18 @@ var MessageFrontmatterSchema = z3.object({
281
423
  /** On a post a task verb made in the task's thread, or a decision in a proposal's: the step it recorded. */
282
424
  step: MessageStepSchema.optional(),
283
425
  ts: IsoDateTimeSchema,
284
- mentions: z3.array(NameSchema)
426
+ mentions: z4.array(NameSchema)
285
427
  });
286
- var TaskStatusSchema = z3.enum(["open", "claimed", "done", "abandoned"]);
287
- var ThreadStateSchema = z3.enum(["open", "closed"]);
288
- var ThreadSubjectSchema = z3.object({
289
- kind: z3.enum(["task", "proposal"]),
428
+ var TaskStatusSchema = z4.enum(["open", "claimed", "done", "abandoned"]);
429
+ var ThreadStateSchema = z4.enum(["open", "closed"]);
430
+ var ThreadSubjectSchema = z4.object({
431
+ kind: z4.enum(["task", "proposal"]),
290
432
  id: UlidSchema
291
433
  });
292
- var ThreadFrontmatterSchema = z3.object({
434
+ var ThreadFrontmatterSchema = z4.object({
293
435
  id: UlidSchema,
294
436
  channel: ChannelRefSchema,
295
- title: z3.string().min(1).max(200),
437
+ title: z4.string().min(1).max(200),
296
438
  subject: ThreadSubjectSchema.optional(),
297
439
  state: ThreadStateSchema,
298
440
  openedBy: NameSchema,
@@ -300,28 +442,28 @@ var ThreadFrontmatterSchema = z3.object({
300
442
  closedBy: NameSchema.optional(),
301
443
  closedAt: IsoDateTimeSchema.optional()
302
444
  });
303
- var StageSchema = z3.object({
445
+ var StageSchema = z4.object({
304
446
  id: StageIdSchema,
305
- name: z3.string().min(1),
447
+ name: z4.string().min(1),
306
448
  role: NameSchema.optional(),
307
449
  agent: NameSchema.optional(),
308
- gate: z3.boolean().default(false),
450
+ gate: z4.boolean().default(false),
309
451
  /** Everyone who has held the stage, oldest first. */
310
- holders: z3.array(NameSchema).default([]),
452
+ holders: z4.array(NameSchema).default([]),
311
453
  completedBy: NameSchema.optional(),
312
454
  completedAt: IsoDateTimeSchema.optional()
313
455
  });
314
- var TaskReturnSchema = z3.object({
456
+ var TaskReturnSchema = z4.object({
315
457
  from: StageIdSchema,
316
458
  by: NameSchema,
317
459
  at: IsoDateTimeSchema
318
460
  });
319
- var TaskFrontmatterSchema = z3.object({
461
+ var TaskFrontmatterSchema = z4.object({
320
462
  id: UlidSchema,
321
463
  project: NameSchema,
322
464
  /** The channel of the project the task is filed in, where its thread hangs. */
323
465
  channel: NameSchema.default("general"),
324
- title: z3.string().min(1),
466
+ title: z4.string().min(1),
325
467
  status: TaskStatusSchema,
326
468
  createdBy: NameSchema,
327
469
  createdAt: IsoDateTimeSchema,
@@ -330,19 +472,19 @@ var TaskFrontmatterSchema = z3.object({
330
472
  claimedBy: NameSchema.optional(),
331
473
  leaseExpiresAt: IsoDateTimeSchema.optional(),
332
474
  parentId: UlidSchema.optional(),
333
- blockedBy: z3.array(UlidSchema),
334
- requiredCapabilities: z3.array(z3.string()),
335
- stages: z3.array(StageSchema).min(1),
475
+ blockedBy: z4.array(UlidSchema),
476
+ requiredCapabilities: z4.array(z4.string()),
477
+ stages: z4.array(StageSchema).min(1),
336
478
  stage: StageIdSchema,
337
479
  /** When the current stage last became current or lost its holder: the waiting clock. */
338
480
  stageSince: IsoDateTimeSchema,
339
481
  /** The highest stage number issued, so a removed stage's id is never reused. */
340
- stageSeq: z3.number().int().positive(),
482
+ stageSeq: z4.number().int().positive(),
341
483
  onDone: CompletionEffectSchema.default("none"),
342
484
  /** The pull request a `ghpr` task lands through. */
343
485
  pullRequest: PullRequestLinkSchema.optional(),
344
486
  /** The last stage is complete and the completion effect is running. */
345
- completing: z3.boolean().default(false),
487
+ completing: z4.boolean().default(false),
346
488
  /** The latest send-back while its rework lasts: cleared once the task reaches that stage again. */
347
489
  returned: TaskReturnSchema.optional()
348
490
  });
@@ -366,79 +508,79 @@ function mayHoldStage(member, task) {
366
508
  }
367
509
  return !(stage.gate && task.stages.slice(0, index).some((s) => s.holders.includes(member.name)));
368
510
  }
369
- var ModelNameSchema = z3.string().trim().min(1).max(120).regex(/^[\w.:/[\]-]+$/, "a model name is one word of letters, digits, and . _ : / - [ ]");
370
- var EffortSchema = z3.string().trim().min(1).max(32).regex(/^[a-z][a-z0-9_-]*$/, "an effort is one lowercase word, such as high");
371
- var EffortOptionSchema = z3.object({
511
+ var ModelNameSchema = z4.string().trim().min(1).max(120).regex(/^[\w.:/[\]-]+$/, "a model name is one word of letters, digits, and . _ : / - [ ]");
512
+ var EffortSchema = z4.string().trim().min(1).max(32).regex(/^[a-z][a-z0-9_-]*$/, "an effort is one lowercase word, such as high");
513
+ var EffortOptionSchema = z4.object({
372
514
  id: EffortSchema,
373
- description: z3.string().default("")
515
+ description: z4.string().default("")
374
516
  });
375
- var ModelOptionSchema = z3.object({
517
+ var ModelOptionSchema = z4.object({
376
518
  id: ModelNameSchema,
377
- name: z3.string(),
378
- description: z3.string().default(""),
379
- isDefault: z3.boolean().default(false),
380
- efforts: z3.array(EffortOptionSchema).default([]),
519
+ name: z4.string(),
520
+ description: z4.string().default(""),
521
+ isDefault: z4.boolean().default(false),
522
+ efforts: z4.array(EffortOptionSchema).default([]),
381
523
  defaultEffort: EffortSchema.optional()
382
524
  });
383
- var RunnerModelsSchema = z3.object({
525
+ var RunnerModelsSchema = z4.object({
384
526
  runner: NameSchema,
385
527
  cli: CliKindSchema,
386
- models: z3.array(ModelOptionSchema)
528
+ models: z4.array(ModelOptionSchema)
387
529
  });
388
- var AgentStatusSchema = z3.enum(["active", "retired"]);
389
- var AgentSchema = z3.object({
530
+ var AgentStatusSchema = z4.enum(["active", "retired"]);
531
+ var AgentSchema = z4.object({
390
532
  name: NameSchema,
391
533
  role: NameSchema,
392
534
  cli: CliKindSchema.nullable(),
393
- model: z3.string().optional(),
535
+ model: z4.string().optional(),
394
536
  /** The reasoning effort set for the agent's turns; the model's own default applies otherwise. */
395
- effort: z3.string().optional(),
537
+ effort: z4.string().optional(),
396
538
  /** The runner preferred for the agent's turns where nothing else decides; any runner with its CLI otherwise. */
397
539
  homeRunner: NameSchema.optional(),
398
- memberships: z3.array(NameSchema),
399
- subscriptions: z3.array(ChannelRefSchema),
540
+ memberships: z4.array(NameSchema),
541
+ subscriptions: z4.array(ChannelRefSchema),
400
542
  status: AgentStatusSchema,
401
543
  createdAt: IsoDateTimeSchema,
402
- tokenHash: z3.string().min(1),
544
+ tokenHash: z4.string().min(1),
403
545
  retiredAt: IsoDateTimeSchema.optional(),
404
- retiredReason: z3.string().optional()
546
+ retiredReason: z4.string().optional()
405
547
  });
406
- var MemberSchema = z3.object({
548
+ var MemberSchema = z4.object({
407
549
  name: NameSchema,
408
550
  role: NameSchema,
409
551
  cli: CliKindSchema.nullable(),
410
552
  homeRunner: NameSchema.optional(),
411
553
  status: AgentStatusSchema,
412
- resident: z3.boolean().default(false),
554
+ resident: z4.boolean().default(false),
413
555
  /** The model configured on the record, if any; the CLI's own default applies otherwise. */
414
- model: z3.string().optional(),
556
+ model: z4.string().optional(),
415
557
  /** The model the CLI reported on the citizen's last turn. */
416
- lastModel: z3.string().optional(),
558
+ lastModel: z4.string().optional(),
417
559
  /** The reasoning effort configured on the record, if any; the model's own default applies otherwise. */
418
- effort: z3.string().optional(),
560
+ effort: z4.string().optional(),
419
561
  /** The citizen's own skills, by name. */
420
- skills: z3.array(NameSchema).default([]),
421
- memberships: z3.array(NameSchema),
422
- subscriptions: z3.array(ChannelRefSchema),
423
- claimsHeld: z3.number().int().nonnegative().default(0),
424
- tasksDone: z3.number().int().nonnegative().default(0),
562
+ skills: z4.array(NameSchema).default([]),
563
+ memberships: z4.array(NameSchema),
564
+ subscriptions: z4.array(ChannelRefSchema),
565
+ claimsHeld: z4.number().int().nonnegative().default(0),
566
+ tasksDone: z4.number().int().nonnegative().default(0),
425
567
  lastTurnAt: IsoDateTimeSchema.optional(),
426
- lastTurnOutcome: z3.string().optional(),
568
+ lastTurnOutcome: z4.string().optional(),
427
569
  createdAt: IsoDateTimeSchema,
428
570
  retiredAt: IsoDateTimeSchema.optional()
429
571
  });
430
- var RunnerOsSchema = z3.enum(["linux", "windows", "darwin"]);
431
- var RunnerSchema = z3.object({
572
+ var RunnerOsSchema = z4.enum(["linux", "windows", "darwin"]);
573
+ var RunnerSchema = z4.object({
432
574
  name: NameSchema,
433
575
  os: RunnerOsSchema,
434
- clis: z3.array(CliKindSchema),
435
- capabilities: z3.array(z3.string()),
576
+ clis: z4.array(CliKindSchema),
577
+ capabilities: z4.array(z4.string()),
436
578
  /** The release the runner reported when it last registered; absent until it has. */
437
- version: z3.string().optional(),
438
- status: z3.enum(["connected", "disconnected"]),
579
+ version: z4.string().optional(),
580
+ status: z4.enum(["connected", "disconnected"]),
439
581
  lastSeen: IsoDateTimeSchema.optional()
440
582
  });
441
- var ProposalKindSchema = z3.enum([
583
+ var ProposalKindSchema = z4.enum([
442
584
  "role",
443
585
  "member",
444
586
  "channel",
@@ -447,43 +589,43 @@ var ProposalKindSchema = z3.enum([
447
589
  "skill",
448
590
  "archive"
449
591
  ]);
450
- var ProposalStatusSchema = z3.enum([
592
+ var ProposalStatusSchema = z4.enum([
451
593
  "proposed",
452
594
  "approved",
453
595
  "rejected",
454
596
  "provisioned",
455
597
  "retired"
456
598
  ]);
457
- var MemberProposalSchema = z3.object({
599
+ var MemberProposalSchema = z4.object({
458
600
  name: NameSchema,
459
601
  role: NameSchema,
460
602
  cli: CliKindSchema,
461
- model: z3.string().optional(),
603
+ model: z4.string().optional(),
462
604
  homeRunner: NameSchema.optional(),
463
- memberships: z3.array(NameSchema).default([]),
464
- subscriptions: z3.array(ChannelRefSchema).default([]),
465
- seedInstructions: z3.string().optional()
605
+ memberships: z4.array(NameSchema).default([]),
606
+ subscriptions: z4.array(ChannelRefSchema).default([]),
607
+ seedInstructions: z4.string().optional()
466
608
  });
467
- var ChannelProposalSchema = z3.object({
609
+ var ChannelProposalSchema = z4.object({
468
610
  project: NameSchema.nullable().default(null),
469
611
  name: NameSchema,
470
- purpose: z3.string().min(1)
612
+ purpose: z4.string().min(1)
471
613
  });
472
- var ReallocationProposalSchema = z3.object({
473
- description: z3.string().min(1)
614
+ var ReallocationProposalSchema = z4.object({
615
+ description: z4.string().min(1)
474
616
  });
475
- var RetirementProposalSchema = z3.object({
617
+ var RetirementProposalSchema = z4.object({
476
618
  agent: NameSchema,
477
- reason: z3.string().min(1)
619
+ reason: z4.string().min(1)
478
620
  });
479
- var ArchiveProposalSchema = z3.object({
621
+ var ArchiveProposalSchema = z4.object({
480
622
  project: NameSchema,
481
- reason: z3.string().min(1)
623
+ reason: z4.string().min(1)
482
624
  });
483
- var SkillProposalSchema = z3.object({
625
+ var SkillProposalSchema = z4.object({
484
626
  name: NameSchema,
485
- summary: z3.string().min(1).max(200),
486
- body: z3.string().min(1)
627
+ summary: z4.string().min(1).max(200),
628
+ body: z4.string().min(1)
487
629
  });
488
630
  var ProposalCharterSchemas = {
489
631
  role: RoleCharterSchema,
@@ -555,17 +697,17 @@ function describeCharter(kind, charter) {
555
697
  }
556
698
  return `${kind} ${JSON.stringify(charter)}`;
557
699
  }
558
- var SkillSchema = z3.object({
700
+ var SkillSchema = z4.object({
559
701
  name: NameSchema,
560
- summary: z3.string().default(""),
561
- scope: z3.enum(["own", "society"]),
562
- path: z3.string().min(1)
702
+ summary: z4.string().default(""),
703
+ scope: z4.enum(["own", "society"]),
704
+ path: z4.string().min(1)
563
705
  });
564
- var HomeConflictSchema = z3.object({
706
+ var HomeConflictSchema = z4.object({
565
707
  /** The copy, relative to the home. */
566
- path: z3.string().min(1),
708
+ path: z4.string().min(1),
567
709
  /** The file beside it, whose version the board kept. */
568
- file: z3.string().min(1),
710
+ file: z4.string().min(1),
569
711
  /** When the copy reached the board. */
570
712
  since: IsoDateTimeSchema
571
713
  });
@@ -573,60 +715,60 @@ var CONFLICT_COPY = /^(.+)\.conflict-[^/.]+$/;
573
715
  function conflictCopyOf(copy) {
574
716
  return CONFLICT_COPY.exec(copy)?.[1] ?? null;
575
717
  }
576
- var CommitIdSchema = z3.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/);
577
- var HomeFileChangeSchema = z3.object({
578
- path: z3.string().min(1),
579
- status: z3.enum(["added", "modified", "deleted"]),
718
+ var CommitIdSchema = z4.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/);
719
+ var HomeFileChangeSchema = z4.object({
720
+ path: z4.string().min(1),
721
+ status: z4.enum(["added", "modified", "deleted"]),
580
722
  /** Lines added and removed, or null for a binary file. */
581
- added: z3.number().int().nonnegative().nullable(),
582
- removed: z3.number().int().nonnegative().nullable()
723
+ added: z4.number().int().nonnegative().nullable(),
724
+ removed: z4.number().int().nonnegative().nullable()
583
725
  });
584
- var HomeChangeSchema = z3.object({
726
+ var HomeChangeSchema = z4.object({
585
727
  commit: CommitIdSchema,
586
728
  at: IsoDateTimeSchema,
587
- kind: z3.enum(["turn", "board", "merge"]),
588
- author: z3.string(),
729
+ kind: z4.enum(["turn", "board", "merge"]),
730
+ author: z4.string(),
589
731
  /** The turn that made it, from the runner's commit message. */
590
732
  turnId: UlidSchema.optional(),
591
- subject: z3.string(),
592
- files: z3.array(HomeFileChangeSchema)
733
+ subject: z4.string(),
734
+ files: z4.array(HomeFileChangeSchema)
593
735
  });
594
- var HomeHistorySchema = z3.object({
595
- changes: z3.array(HomeChangeSchema),
736
+ var HomeHistorySchema = z4.object({
737
+ changes: z4.array(HomeChangeSchema),
596
738
  /** Whether older commits remain past the ones read. */
597
- more: z3.boolean()
739
+ more: z4.boolean()
598
740
  });
599
741
  var HomeFileDiffSchema = HomeFileChangeSchema.pick({ path: true, status: true }).extend({
600
- patch: z3.string().nullable(),
742
+ patch: z4.string().nullable(),
601
743
  /** The patch was cut at its size limit. */
602
- truncated: z3.boolean()
744
+ truncated: z4.boolean()
603
745
  });
604
- var KnowledgeSchema = z3.object({
746
+ var KnowledgeSchema = z4.object({
605
747
  topic: NameSchema,
606
748
  project: NameSchema.nullable(),
607
749
  updatedBy: NameSchema,
608
750
  updatedAt: IsoDateTimeSchema
609
751
  });
610
- var RemovedKnowledgeSchema = z3.object({
752
+ var RemovedKnowledgeSchema = z4.object({
611
753
  topic: NameSchema,
612
754
  project: NameSchema.nullable(),
613
755
  removedBy: NameSchema,
614
756
  removedAt: IsoDateTimeSchema
615
757
  });
616
- var DashboardSchema = z3.object({
758
+ var DashboardSchema = z4.object({
617
759
  project: NameSchema,
618
- revision: z3.number().int().min(0),
760
+ revision: z4.number().int().min(0),
619
761
  /** Absent until someone updates the dashboard the project began with. */
620
762
  updatedBy: NameSchema.optional(),
621
763
  updatedAt: IsoDateTimeSchema.optional(),
622
- body: z3.string()
764
+ body: z4.string()
623
765
  });
624
- var ProposalOriginSchema = z3.object({
766
+ var ProposalOriginSchema = z4.object({
625
767
  scope: NameSchema,
626
768
  thread: UlidSchema.optional(),
627
769
  channel: NameSchema.optional()
628
770
  });
629
- var ProposalFrontmatterSchema = z3.object({
771
+ var ProposalFrontmatterSchema = z4.object({
630
772
  id: UlidSchema,
631
773
  kind: ProposalKindSchema,
632
774
  proposedBy: NameSchema,
@@ -636,140 +778,175 @@ var ProposalFrontmatterSchema = z3.object({
636
778
  filedFrom: ProposalOriginSchema.optional(),
637
779
  decidedBy: NameSchema.optional(),
638
780
  decidedAt: IsoDateTimeSchema.optional(),
639
- reason: z3.string().optional(),
640
- charter: z3.record(z3.string(), z3.unknown()),
781
+ reason: z4.string().optional(),
782
+ charter: z4.record(z4.string(), z4.unknown()),
641
783
  /** What approval created, for kinds the board provisions: the agent, channel, role, or retirement. */
642
784
  provisionedAt: IsoDateTimeSchema.optional(),
643
- provision: z3.record(z3.string(), z3.unknown()).optional()
785
+ provision: z4.record(z4.string(), z4.unknown()).optional()
644
786
  });
645
- var DecisionSchema = z3.object({
787
+ var DecisionSchema = z4.object({
646
788
  id: UlidSchema,
647
789
  proposalId: UlidSchema,
648
790
  decidedBy: NameSchema,
649
- outcome: z3.enum(["approved", "rejected"]),
650
- reason: z3.string().optional(),
791
+ outcome: z4.enum(["approved", "rejected"]),
792
+ reason: z4.string().optional(),
651
793
  ts: IsoDateTimeSchema
652
794
  });
653
795
 
654
796
  // packages/shared/dist/verbs.js
655
- import { z as z4 } from "zod";
797
+ import { z as z5 } from "zod";
798
+ var MomentSchema = z5.iso.datetime({ offset: true });
799
+ var CronTitleSchema = z5.string().trim().min(1).max(120);
800
+ var CronNoteSchema = z5.string().trim().min(1).max(4e3);
656
801
  var VerbInputs = {
657
- post_message: z4.object({
802
+ post_message: z5.object({
658
803
  /** Required outside a thread; in one it may be left out, and must otherwise be the thread's. */
659
804
  channel: ChannelRefSchema.optional(),
660
- body: z4.string().min(1),
805
+ body: z5.string().min(1),
661
806
  thread_id: UlidSchema.optional()
662
807
  }),
663
- read_inbox: z4.object({
808
+ read_inbox: z5.object({
664
809
  since_cursor: UlidSchema.nullable().optional(),
665
- limit: z4.number().int().positive().max(200).default(50),
666
- advance: z4.boolean().default(true)
810
+ limit: z5.number().int().positive().max(200).default(50),
811
+ advance: z5.boolean().default(true)
667
812
  }),
668
- search: z4.object({
669
- query: z4.string().min(1),
813
+ search: z5.object({
814
+ query: z5.string().min(1),
670
815
  project: NameSchema.optional(),
671
816
  channel: ChannelRefSchema.optional(),
672
- limit: z4.number().int().positive().max(100).default(20)
817
+ limit: z5.number().int().positive().max(100).default(20)
673
818
  }),
674
819
  /** On a task or a proposal, whose id the thread takes, or on a channel with a title. */
675
- open_thread: z4.object({
820
+ open_thread: z5.object({
676
821
  task_id: UlidSchema.optional(),
677
822
  proposal_id: UlidSchema.optional(),
678
823
  channel: ChannelRefSchema.optional(),
679
- title: z4.string().min(1).max(200).optional()
824
+ title: z5.string().min(1).max(200).optional()
680
825
  }),
681
- close_thread: z4.object({ thread_id: UlidSchema, summary: z4.string().min(1) }),
682
- create_task: z4.object({
826
+ close_thread: z5.object({ thread_id: UlidSchema, summary: z5.string().min(1) }),
827
+ create_task: z5.object({
683
828
  project: NameSchema,
684
- title: z4.string().min(1).max(200),
685
- body: z4.string().default(""),
829
+ title: z5.string().min(1).max(200),
830
+ body: z5.string().default(""),
686
831
  parent_id: UlidSchema.optional(),
687
- required_capabilities: z4.array(z4.string()).default([]),
832
+ required_capabilities: z5.array(z5.string()).default([]),
688
833
  /** The plan; without it the task gets one stage, work, that anyone in the project may take. */
689
- stages: z4.array(PlanStageSchema).min(1).optional(),
834
+ stages: z5.array(PlanStageSchema).min(1).optional(),
690
835
  /** A channel of the project the task belongs to, such as a release's; its thread opens there. */
691
836
  channel: NameSchema.default("general")
692
837
  }),
693
- claim_task: z4.object({ task_id: UlidSchema }),
694
- release_task: z4.object({ task_id: UlidSchema }),
695
- update_task: z4.object({
838
+ claim_task: z5.object({ task_id: UlidSchema }),
839
+ release_task: z5.object({ task_id: UlidSchema }),
840
+ update_task: z5.object({
696
841
  task_id: UlidSchema,
697
842
  /** Move the task back to this earlier stage. */
698
843
  stage: StageIdSchema.optional(),
699
- status: z4.enum(["abandoned"]).optional(),
844
+ status: z5.enum(["abandoned"]).optional(),
700
845
  /** Posted to the task's thread as the caller, marked with the step when the task moved. */
701
- note: z4.string().min(1).optional(),
702
- blocked_by: z4.array(UlidSchema).optional(),
846
+ note: z5.string().min(1).optional(),
847
+ blocked_by: z5.array(UlidSchema).optional(),
703
848
  /**
704
849
  * The pull request a `ghpr` task lands through, on its project's repository, with the merge
705
850
  * commit's subject and body; null unlinks it.
706
851
  */
707
- pull_request: z4.object({
852
+ pull_request: z5.object({
708
853
  url: PullRequestUrlSchema,
709
854
  merge_subject: MergeSubjectSchema.optional(),
710
- merge_body: z4.string().max(2e4).optional()
855
+ merge_body: z5.string().max(2e4).optional()
711
856
  }).nullable().optional()
712
857
  }),
713
- get_task: z4.object({ task_id: UlidSchema }),
714
- subscribe: z4.object({ channel: ChannelRefSchema }),
715
- unsubscribe: z4.object({ channel: ChannelRefSchema }),
716
- propose: z4.object({
858
+ get_task: z5.object({ task_id: UlidSchema }),
859
+ subscribe: z5.object({ channel: ChannelRefSchema }),
860
+ unsubscribe: z5.object({ channel: ChannelRefSchema }),
861
+ propose: z5.object({
717
862
  kind: ProposalKindSchema,
718
- charter: z4.record(z4.string(), z4.unknown()),
719
- rationale: z4.string().default("")
863
+ charter: z5.record(z5.string(), z5.unknown()),
864
+ rationale: z5.string().default("")
720
865
  }),
721
- approve: z4.object({ proposal_id: UlidSchema, reason: z4.string().optional() }),
722
- reject: z4.object({ proposal_id: UlidSchema, reason: z4.string().min(1) }),
723
- create_project: z4.object({
866
+ approve: z5.object({ proposal_id: UlidSchema, reason: z5.string().optional() }),
867
+ reject: z5.object({ proposal_id: UlidSchema, reason: z5.string().min(1) }),
868
+ create_project: z5.object({
724
869
  slug: NameSchema,
725
- name: z4.string().min(1).optional(),
726
- repo: z4.string().min(1).nullable().default(null),
870
+ name: z5.string().min(1).optional(),
871
+ repo: z5.string().min(1).nullable().default(null),
727
872
  default_branch: DefaultBranchSchema.default("main"),
728
873
  on_done: CompletionEffectSchema.optional()
729
874
  }),
730
- join_project: z4.object({ project: NameSchema, agent: NameSchema.optional() }),
731
- leave_project: z4.object({ project: NameSchema, agent: NameSchema.optional() }),
732
- write_knowledge: z4.object({
875
+ join_project: z5.object({ project: NameSchema, agent: NameSchema.optional() }),
876
+ leave_project: z5.object({ project: NameSchema, agent: NameSchema.optional() }),
877
+ write_knowledge: z5.object({
733
878
  /** A project you belong to, or null for society knowledge, which the steward and the user curate. */
734
879
  project: NameSchema.nullable().default(null),
735
880
  topic: NameSchema,
736
- body: z4.string().min(1)
881
+ body: z5.string().min(1)
737
882
  }),
738
- remove_knowledge: z4.object({
883
+ remove_knowledge: z5.object({
739
884
  /** As for write_knowledge: a project you belong to, or null for society knowledge. */
740
885
  project: NameSchema.nullable().default(null),
741
886
  topic: NameSchema
742
887
  }),
743
- plan_task: z4.object({
888
+ plan_task: z5.object({
744
889
  task_id: UlidSchema,
745
- stages: z4.array(PlanEditStageSchema),
890
+ stages: z5.array(PlanEditStageSchema),
746
891
  on_done: CompletionEffectSchema.optional()
747
892
  }),
748
893
  /** The note is posted to the task's thread as the caller, marked with the step. */
749
- advance_task: z4.object({ task_id: UlidSchema, note: z4.string().min(1).optional() }),
750
- configure_project: z4.object({
894
+ advance_task: z5.object({ task_id: UlidSchema, note: z5.string().min(1).optional() }),
895
+ configure_project: z5.object({
751
896
  project: NameSchema,
752
897
  on_done: CompletionEffectSchema.optional(),
753
898
  /** A new display name; the slug, which everything refers to the project by, never changes. */
754
- name: z4.string().trim().min(1).max(120).optional(),
899
+ name: z5.string().trim().min(1).max(120).optional(),
755
900
  /** The branch tasks start from and land on from now; tasks in play keep their branches. */
756
901
  default_branch: DefaultBranchSchema.optional()
757
902
  }),
758
- archive_project: z4.object({ project: NameSchema, reason: z4.string().min(1) }),
759
- create_channel: z4.object({
903
+ archive_project: z5.object({ project: NameSchema, reason: z5.string().min(1) }),
904
+ create_channel: z5.object({
760
905
  /** A project, or null for a channel of the society. */
761
906
  project: NameSchema.nullable().default(null),
762
907
  name: NameSchema,
763
- purpose: z4.string().min(1)
908
+ purpose: z5.string().min(1)
764
909
  }),
765
- archive_channel: z4.object({ channel: ChannelRefSchema, reason: z4.string().min(1) }),
766
- update_dashboard: z4.object({
910
+ archive_channel: z5.object({ channel: ChannelRefSchema, reason: z5.string().min(1) }),
911
+ update_dashboard: z5.object({
767
912
  project: NameSchema,
768
913
  /** The whole dashboard as markdown; the board keeps its frontmatter. */
769
- body: z4.string().min(1),
914
+ body: z5.string().min(1),
770
915
  /** The `revision` in the frontmatter of the dashboard you read, 0 when it names none. */
771
- revision: z4.number().int().min(0)
772
- })
916
+ revision: z5.number().int().min(0)
917
+ }),
918
+ create_cron: z5.object({
919
+ title: CronTitleSchema,
920
+ /** What to do when it fires; quoted in the prompt of every turn it starts. */
921
+ note: CronNoteSchema,
922
+ /** Minute, hour, day of month, month, day of week, such as `0 9 * * 1-5`; or `at` instead. */
923
+ cron: CronExpressionSchema.optional(),
924
+ /** The IANA time zone `cron` is read in; the society's when left out. */
925
+ timezone: TimeZoneSchema.optional(),
926
+ /** One time to fire, instead of `cron`; the cron ends once it has fired. */
927
+ at: MomentSchema.optional(),
928
+ /** The citizen it wakes: yourself when left out. */
929
+ agent: NameSchema.optional(),
930
+ /** Where it fires: a project the citizen belongs to, or `society`; this turn's scope when left out. */
931
+ project: NameSchema.optional(),
932
+ /** An open thread there, whose conversation it fires in. */
933
+ thread_id: UlidSchema.optional(),
934
+ /** An open channel of that place other than general, whose conversation it fires in. */
935
+ channel: NameSchema.optional()
936
+ }),
937
+ update_cron: z5.object({
938
+ cron_id: UlidSchema,
939
+ /** Pause it, or resume it, which starts its schedule afresh from now. */
940
+ paused: z5.boolean().optional(),
941
+ title: CronTitleSchema.optional(),
942
+ note: CronNoteSchema.optional(),
943
+ /** A new expression, or `at` for one time; either starts the schedule afresh. */
944
+ cron: CronExpressionSchema.optional(),
945
+ /** The zone `cron` is read in; null for the society's. */
946
+ timezone: TimeZoneSchema.nullable().optional(),
947
+ at: MomentSchema.optional()
948
+ }),
949
+ remove_cron: z5.object({ cron_id: UlidSchema, reason: z5.string().trim().min(1) })
773
950
  };
774
951
  var VERB_DESCRIPTIONS = {
775
952
  post_message: "Post a markdown message to a channel, or into a thread when thread_id is given; a thread's message goes to the thread's channel.",
@@ -798,11 +975,14 @@ var VERB_DESCRIPTIONS = {
798
975
  archive_project: "Archive a project whose work is finished or has moved: its members leave, its open threads close, and nothing more is posted, filed, or joined there, while its files and history stay. Refused while a task there is in play.",
799
976
  create_channel: "Open a channel in a project, or with project null in the society, for a workstream such as a release, whose tasks are filed there. You follow it; a notice in the place's general announces it, and members subscribe when it concerns them. User, steward, concierge.",
800
977
  archive_channel: "Archive a channel whose workstream is done: its open threads close, its followers stop following it, and nothing more is posted or filed there, while its history stays readable. Refused while a task filed there is in play, and for the channels the board itself uses. User, steward, concierge.",
801
- update_dashboard: "Replace the dashboard of a project you are a member of, the status page the user reads on its view, with a whole markdown body. Pass the revision from the frontmatter of the dashboard you read; if it was updated since, the call is refused with the current text to merge yours into. dashboard.md in the board projection is a copy, so editing it changes nothing."
978
+ update_dashboard: "Replace the dashboard of a project you are a member of, the status page the user reads on its view, with a whole markdown body. Pass the revision from the frontmatter of the dashboard you read; if it was updated since, the call is refused with the current text to merge yours into. dashboard.md in the board projection is a copy, so editing it changes nothing.",
979
+ create_cron: "Set a cron that wakes you later: a title, a note on what to do then, and either cron, a five-field expression (minute hour day-of-month month day-of-week) read in timezone or the society's, or at, one time. It fires in this turn's conversation unless you name a project, thread_id, or channel, at most every 15 minutes, and every fire is a turn. Only the user, the steward, and the concierge set one that wakes another citizen (agent).",
980
+ update_cron: "Pause or resume a cron, or change its title, note, or schedule; resuming or rescheduling starts it afresh from now. Yours, or anyone's for the user, the steward, and the concierge.",
981
+ remove_cron: "End a cron for good, with a reason. Yours, or anyone's for the user, the steward, and the concierge. A cron also ends with its conversation, and a one-time cron once it fires."
802
982
  };
803
983
 
804
984
  // packages/shared/dist/events.js
805
- import { z as z5 } from "zod";
985
+ import { z as z6 } from "zod";
806
986
  var BOARD_EVENT_TYPES = [
807
987
  "society.initialized",
808
988
  "project.added",
@@ -857,30 +1037,36 @@ var BOARD_EVENT_TYPES = [
857
1037
  "merge.failed",
858
1038
  "pull_request.closed",
859
1039
  "pull_request.close_failed",
860
- "workspace.leftovers"
1040
+ "workspace.leftovers",
1041
+ "society.configured",
1042
+ "cron.created",
1043
+ "cron.updated",
1044
+ "cron.fired",
1045
+ "cron.skipped",
1046
+ "cron.ended"
861
1047
  ];
862
- var BoardEventTypeSchema = z5.enum(BOARD_EVENT_TYPES);
863
- var BoardEventSchema = z5.object({
1048
+ var BoardEventTypeSchema = z6.enum(BOARD_EVENT_TYPES);
1049
+ var BoardEventSchema = z6.object({
864
1050
  id: UlidSchema,
865
1051
  ts: IsoDateTimeSchema,
866
1052
  type: BoardEventTypeSchema,
867
1053
  actor: NameSchema,
868
- payload: z5.record(z5.string(), z5.unknown())
1054
+ payload: z6.record(z6.string(), z6.unknown())
869
1055
  });
870
- var UsageSchema = z5.object({
871
- inputTokens: z5.number().int().nonnegative(),
872
- outputTokens: z5.number().int().nonnegative(),
873
- cacheReadTokens: z5.number().int().nonnegative().default(0),
874
- cacheWriteTokens: z5.number().int().nonnegative().default(0)
1056
+ var UsageSchema = z6.object({
1057
+ inputTokens: z6.number().int().nonnegative(),
1058
+ outputTokens: z6.number().int().nonnegative(),
1059
+ cacheReadTokens: z6.number().int().nonnegative().default(0),
1060
+ cacheWriteTokens: z6.number().int().nonnegative().default(0)
875
1061
  });
876
- var TurnStatusSchema = z5.object({
877
- summary: z5.string().min(1),
878
- claimsHeld: z5.array(UlidSchema).default([]),
879
- blockedOn: z5.array(z5.string()).default([]),
880
- needsUserDecision: z5.boolean().default(false),
881
- memoryUpdated: z5.boolean().default(false)
1062
+ var TurnStatusSchema = z6.object({
1063
+ summary: z6.string().min(1),
1064
+ claimsHeld: z6.array(UlidSchema).default([]),
1065
+ blockedOn: z6.array(z6.string()).default([]),
1066
+ needsUserDecision: z6.boolean().default(false),
1067
+ memoryUpdated: z6.boolean().default(false)
882
1068
  });
883
- var TurnExitReasonSchema = z5.enum([
1069
+ var TurnExitReasonSchema = z6.enum([
884
1070
  "completed",
885
1071
  "timeout",
886
1072
  "error",
@@ -888,50 +1074,50 @@ var TurnExitReasonSchema = z5.enum([
888
1074
  "blocked",
889
1075
  "stopped"
890
1076
  ]);
891
- var AgentEventSchema = z5.discriminatedUnion("type", [
892
- z5.object({
893
- type: z5.literal("turn_started"),
1077
+ var AgentEventSchema = z6.discriminatedUnion("type", [
1078
+ z6.object({
1079
+ type: z6.literal("turn_started"),
894
1080
  agent: NameSchema,
895
- session: z5.string(),
1081
+ session: z6.string(),
896
1082
  runner: NameSchema,
897
1083
  /** The model the CLI reports running, when it reports one. */
898
- model: z5.string().optional()
1084
+ model: z6.string().optional()
899
1085
  }),
900
1086
  /**
901
1087
  * The CLI took a steer into the running turn. Adapters name the steer; the server adds the
902
1088
  * messages it delivered and the text the CLI was given.
903
1089
  */
904
- z5.object({
905
- type: z5.literal("steered"),
906
- steer: z5.string().min(1),
907
- messages: z5.array(UlidSchema).optional(),
908
- text: z5.string().optional()
1090
+ z6.object({
1091
+ type: z6.literal("steered"),
1092
+ steer: z6.string().min(1),
1093
+ messages: z6.array(UlidSchema).optional(),
1094
+ text: z6.string().optional()
909
1095
  }),
910
- z5.object({ type: z5.literal("text"), delta: z5.string() }),
911
- z5.object({ type: z5.literal("tool_call"), name: z5.string(), input: z5.unknown() }),
912
- z5.object({
913
- type: z5.literal("tool_result"),
914
- name: z5.string(),
915
- ok: z5.boolean(),
1096
+ z6.object({ type: z6.literal("text"), delta: z6.string() }),
1097
+ z6.object({ type: z6.literal("tool_call"), name: z6.string(), input: z6.unknown() }),
1098
+ z6.object({
1099
+ type: z6.literal("tool_result"),
1100
+ name: z6.string(),
1101
+ ok: z6.boolean(),
916
1102
  /** What the tool returned, cut to `OUTPUT_LIMIT` characters by `capOutput`. */
917
- output: z5.string().optional()
1103
+ output: z6.string().optional()
918
1104
  }),
919
- z5.object({ type: z5.literal("approval_requested"), kind: z5.string(), detail: z5.unknown() }),
920
- z5.object({
921
- type: z5.literal("turn_completed"),
1105
+ z6.object({ type: z6.literal("approval_requested"), kind: z6.string(), detail: z6.unknown() }),
1106
+ z6.object({
1107
+ type: z6.literal("turn_completed"),
922
1108
  usage: UsageSchema,
923
- costUsd: z5.number().nonnegative(),
1109
+ costUsd: z6.number().nonnegative(),
924
1110
  status: TurnStatusSchema.nullable(),
925
1111
  exitReason: TurnExitReasonSchema
926
1112
  }),
927
- z5.object({ type: z5.literal("error"), message: z5.string() })
1113
+ z6.object({ type: z6.literal("error"), message: z6.string() })
928
1114
  ]);
929
- var TranscriptEntrySchema = z5.object({
1115
+ var TranscriptEntrySchema = z6.object({
930
1116
  ts: IsoDateTimeSchema,
931
1117
  event: AgentEventSchema
932
1118
  });
933
- var LiveTurnEventSchema = z5.object({
934
- seq: z5.number().int().positive(),
1119
+ var LiveTurnEventSchema = z6.object({
1120
+ seq: z6.number().int().positive(),
935
1121
  ts: IsoDateTimeSchema,
936
1122
  agent: NameSchema,
937
1123
  project: NameSchema,
@@ -943,10 +1129,10 @@ var LiveTurnEventSchema = z5.object({
943
1129
  });
944
1130
 
945
1131
  // packages/shared/dist/turns.js
946
- import { z as z7 } from "zod";
1132
+ import { z as z8 } from "zod";
947
1133
 
948
1134
  // packages/shared/dist/runner.js
949
- import { z as z6 } from "zod";
1135
+ import { z as z7 } from "zod";
950
1136
  var RUNNER_PROTOCOL = 1;
951
1137
  var PATH_TOKENS = {
952
1138
  home: "{{stellaris:home}}",
@@ -957,67 +1143,67 @@ var SERVER_TOKEN = "{{stellaris:server}}";
957
1143
  var WORKSPACE_TOKEN = "{{stellaris:workspace}}";
958
1144
  var HOME_SCRATCH = "scratch";
959
1145
  var HOME_FILE_LIMIT_BYTES = 8 * 1024 * 1024;
960
- var RunnerHelloSchema = z6.object({
961
- protocol: z6.number().int().positive(),
962
- version: z6.string().min(1),
1146
+ var RunnerHelloSchema = z7.object({
1147
+ protocol: z7.number().int().positive(),
1148
+ version: z7.string().min(1),
963
1149
  os: RunnerOsSchema,
964
- clis: z6.array(CliKindSchema),
1150
+ clis: z7.array(CliKindSchema),
965
1151
  /** The CLIs whose adapter can keep a session warm between turns. */
966
- residentClis: z6.array(CliKindSchema).default([]),
1152
+ residentClis: z7.array(CliKindSchema).default([]),
967
1153
  /** The CLIs whose adapter can take input into a running turn; the server steers only these. */
968
- steerableClis: z6.array(CliKindSchema).default([]),
1154
+ steerableClis: z7.array(CliKindSchema).default([]),
969
1155
  /** The CLIs whose adapter can stop a running turn; the server stops only these. */
970
- stoppableClis: z6.array(CliKindSchema).default([]),
1156
+ stoppableClis: z7.array(CliKindSchema).default([]),
971
1157
  /** Whether the runner answers file reads on its projects' branches; one from before never does. */
972
- branchReads: z6.boolean().default(false),
1158
+ branchReads: z7.boolean().default(false),
973
1159
  /** Whether the runner lists what a branch changed; one from before never does. */
974
- branchChanges: z6.boolean().default(false),
975
- capabilities: z6.array(z6.string()).default([]),
1160
+ branchChanges: z7.boolean().default(false),
1161
+ capabilities: z7.array(z7.string()).default([]),
976
1162
  /** Turns the runner runs at once, or null for no limit. */
977
- slots: z6.number().int().positive().nullable(),
1163
+ slots: z7.number().int().positive().nullable(),
978
1164
  /** Turns the runner is still running, when it registers again; the server fails the rest of its turns. */
979
- turns: z6.array(UlidSchema).default([])
1165
+ turns: z7.array(UlidSchema).default([])
980
1166
  });
981
- var RunnerWelcomeSchema = z6.object({ name: NameSchema, version: z6.string() });
982
- var RunnerEnrollRequestSchema = z6.object({
983
- protocol: z6.number().int().positive(),
984
- version: z6.string().min(1).max(64),
985
- hostname: z6.string().min(1).max(253),
1167
+ var RunnerWelcomeSchema = z7.object({ name: NameSchema, version: z7.string() });
1168
+ var RunnerEnrollRequestSchema = z7.object({
1169
+ protocol: z7.number().int().positive(),
1170
+ version: z7.string().min(1).max(64),
1171
+ hostname: z7.string().min(1).max(253),
986
1172
  os: RunnerOsSchema,
987
- clis: z6.array(CliKindSchema).max(8),
988
- capabilities: z6.array(z6.string().min(1).max(64)).max(32).default([])
1173
+ clis: z7.array(CliKindSchema).max(8),
1174
+ capabilities: z7.array(z7.string().min(1).max(64)).max(32).default([])
989
1175
  });
990
- var RunnerEnrollmentSchema = z6.object({
991
- deviceCode: z6.string().min(1),
992
- userCode: z6.string().min(1),
1176
+ var RunnerEnrollmentSchema = z7.object({
1177
+ deviceCode: z7.string().min(1),
1178
+ userCode: z7.string().min(1),
993
1179
  expiresAt: IsoDateTimeSchema,
994
- intervalMs: z6.number().int().positive()
1180
+ intervalMs: z7.number().int().positive()
995
1181
  });
996
- var RunnerEnrollPollSchema = z6.object({ deviceCode: z6.string().min(1) });
997
- var RunnerEnrollStatusSchema = z6.discriminatedUnion("status", [
998
- z6.object({ status: z6.literal("pending") }),
999
- z6.object({ status: z6.literal("approved"), name: NameSchema, token: z6.string().min(1) }),
1000
- z6.object({ status: z6.literal("denied") })
1182
+ var RunnerEnrollPollSchema = z7.object({ deviceCode: z7.string().min(1) });
1183
+ var RunnerEnrollStatusSchema = z7.discriminatedUnion("status", [
1184
+ z7.object({ status: z7.literal("pending") }),
1185
+ z7.object({ status: z7.literal("approved"), name: NameSchema, token: z7.string().min(1) }),
1186
+ z7.object({ status: z7.literal("denied") })
1001
1187
  ]);
1002
- var PendingEnrollmentSchema = z6.object({
1003
- userCode: z6.string().min(1),
1004
- hostname: z6.string(),
1188
+ var PendingEnrollmentSchema = z7.object({
1189
+ userCode: z7.string().min(1),
1190
+ hostname: z7.string(),
1005
1191
  os: RunnerOsSchema,
1006
- clis: z6.array(CliKindSchema),
1007
- capabilities: z6.array(z6.string()),
1008
- version: z6.string(),
1192
+ clis: z7.array(CliKindSchema),
1193
+ capabilities: z7.array(z7.string()),
1194
+ version: z7.string(),
1009
1195
  requestedAt: IsoDateTimeSchema,
1010
1196
  expiresAt: IsoDateTimeSchema
1011
1197
  });
1012
- var ProjectRepoSchema = z6.object({
1198
+ var ProjectRepoSchema = z7.object({
1013
1199
  slug: NameSchema,
1014
- origin: z6.string().nullable(),
1015
- defaultBranch: z6.string().min(1),
1200
+ origin: z7.string().nullable(),
1201
+ defaultBranch: z7.string().min(1),
1016
1202
  /**
1017
1203
  * The board lands finished tasks on the default branch, itself or through their pull requests,
1018
1204
  * so the runner lets nothing else move it.
1019
1205
  */
1020
- boardLands: z6.boolean()
1206
+ boardLands: z7.boolean()
1021
1207
  });
1022
1208
  function projectRepo(project) {
1023
1209
  return {
@@ -1027,29 +1213,29 @@ function projectRepo(project) {
1027
1213
  boardLands: project.onDone !== "none"
1028
1214
  };
1029
1215
  }
1030
- var TurnWorkspaceSchema = z6.discriminatedUnion("kind", [
1031
- z6.object({
1032
- kind: z6.literal("home"),
1216
+ var TurnWorkspaceSchema = z7.discriminatedUnion("kind", [
1217
+ z7.object({
1218
+ kind: z7.literal("home"),
1033
1219
  thread: UlidSchema.optional(),
1034
1220
  channel: NameSchema.optional()
1035
1221
  }),
1036
- z6.object({
1037
- kind: z6.literal("project"),
1222
+ z7.object({
1223
+ kind: z7.literal("project"),
1038
1224
  repo: ProjectRepoSchema,
1039
- branches: z6.array(z6.string().min(1)),
1225
+ branches: z7.array(z7.string().min(1)),
1040
1226
  thread: UlidSchema.optional(),
1041
1227
  channel: NameSchema.optional()
1042
1228
  }),
1043
- z6.object({ kind: z6.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
1229
+ z7.object({ kind: z7.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
1044
1230
  ]);
1045
- var TurnJobSchema = z6.object({
1231
+ var TurnJobSchema = z7.object({
1046
1232
  turnId: UlidSchema,
1047
1233
  agent: NameSchema,
1048
1234
  role: NameSchema,
1049
1235
  cli: CliKindSchema,
1050
- model: z6.string().optional(),
1236
+ model: z7.string().optional(),
1051
1237
  /** The reasoning effort set on the agent; absent for the model's own default. */
1052
- effort: z6.string().optional(),
1238
+ effort: z7.string().optional(),
1053
1239
  /** A project slug, or the society scope. */
1054
1240
  scope: NameSchema,
1055
1241
  /** The thread whose conversation the turn is in; absent for the home conversation. */
@@ -1060,117 +1246,117 @@ var TurnJobSchema = z6.object({
1060
1246
  * The session to resume, or null to start one: the CLI's adapter picks a fresh session's id, and
1061
1247
  * the outcome reports it.
1062
1248
  */
1063
- session: z6.string().min(1).nullable(),
1249
+ session: z7.string().min(1).nullable(),
1064
1250
  /** The running total the session reported after its last turn, which a resumed session starts from. */
1065
- costSoFarUsd: z6.number().nonnegative(),
1251
+ costSoFarUsd: z7.number().nonnegative(),
1066
1252
  /** The rendered charter, norms, memory core, and skills index, with `PATH_TOKENS` for places. */
1067
- instructions: z6.string(),
1068
- prompt: z6.string(),
1069
- mcp: z6.object({ url: z6.string().min(1), token: z6.string().min(1) }),
1070
- limits: z6.object({
1071
- timeoutMs: z6.number().int().positive().nullable(),
1072
- maxTurns: z6.number().int().positive().nullable()
1253
+ instructions: z7.string(),
1254
+ prompt: z7.string(),
1255
+ mcp: z7.object({ url: z7.string().min(1), token: z7.string().min(1) }),
1256
+ limits: z7.object({
1257
+ timeoutMs: z7.number().int().positive().nullable(),
1258
+ maxTurns: z7.number().int().positive().nullable()
1073
1259
  }),
1074
1260
  /** Present when the runner keeps the conversation's session warm between turns, under `key`. */
1075
- resident: z6.object({ key: z6.string().min(1), idleMs: z6.number().int().positive() }).optional(),
1261
+ resident: z7.object({ key: z7.string().min(1), idleMs: z7.number().int().positive() }).optional(),
1076
1262
  workspace: TurnWorkspaceSchema
1077
1263
  });
1078
- var TurnWorkSchema = z6.object({ branch: z6.string().min(1), head: z6.string().min(1) });
1079
- var TurnEventsSchema = z6.object({ entries: z6.array(TranscriptEntrySchema) });
1080
- var TurnOutcomeSchema = z6.object({
1264
+ var TurnWorkSchema = z7.object({ branch: z7.string().min(1), head: z7.string().min(1) });
1265
+ var TurnEventsSchema = z7.object({ entries: z7.array(TranscriptEntrySchema) });
1266
+ var TurnOutcomeSchema = z7.object({
1081
1267
  exitReason: TurnExitReasonSchema,
1082
1268
  status: TurnStatusSchema.nullable(),
1083
- error: z6.string().nullable(),
1269
+ error: z7.string().nullable(),
1084
1270
  usage: UsageSchema,
1085
- costUsd: z6.number().nonnegative(),
1271
+ costUsd: z7.number().nonnegative(),
1086
1272
  /** The session's running total after this turn, for CLIs that report one. */
1087
- sessionCostUsd: z6.number().nonnegative().optional(),
1273
+ sessionCostUsd: z7.number().nonnegative().optional(),
1088
1274
  /** The session the CLI used, which differs from the job's for CLIs that assign their own ids. */
1089
- session: z6.string().min(1),
1275
+ session: z7.string().min(1),
1090
1276
  /** The model the CLI reported, when it reported one. */
1091
- model: z6.string().nullable(),
1277
+ model: z7.string().nullable(),
1092
1278
  work: TurnWorkSchema.nullable(),
1093
1279
  /**
1094
1280
  * Whether the turn's own workspace, a thread's, a channel's, or a task's, holds work on no
1095
1281
  * branch after the hand-back: uncommitted changes, or commits no branch contains.
1096
1282
  */
1097
- leftovers: z6.boolean().default(false)
1283
+ leftovers: z7.boolean().default(false)
1098
1284
  });
1099
- var TurnAckSchema = z6.object({ dropWorkspace: z6.boolean() });
1100
- var LandRequestSchema = z6.object({
1285
+ var TurnAckSchema = z7.object({ dropWorkspace: z7.boolean() });
1286
+ var LandRequestSchema = z7.object({
1101
1287
  repo: ProjectRepoSchema,
1102
- branch: z6.string().min(1),
1288
+ branch: z7.string().min(1),
1103
1289
  /**
1104
1290
  * For a `ghpr` task, the pull request to merge in place of the branch, which must hold the
1105
1291
  * branch's files, and the merge commit's subject and body, the pull request's title and an
1106
1292
  * empty body when left out.
1107
1293
  */
1108
- pullRequest: z6.object({
1109
- url: z6.string().min(1),
1110
- subject: z6.string().min(1).optional(),
1111
- body: z6.string().optional()
1294
+ pullRequest: z7.object({
1295
+ url: z7.string().min(1),
1296
+ subject: z7.string().min(1).optional(),
1297
+ body: z7.string().optional()
1112
1298
  }).optional()
1113
1299
  });
1114
- var CloseRequestSchema = z6.object({
1300
+ var CloseRequestSchema = z7.object({
1115
1301
  repo: ProjectRepoSchema,
1116
- url: z6.string().min(1),
1117
- comment: z6.string().min(1)
1302
+ url: z7.string().min(1),
1303
+ comment: z7.string().min(1)
1118
1304
  });
1119
- var MergeOutcomeSchema = z6.object({ ok: z6.boolean(), detail: z6.string() });
1120
- var ModelListSchema = z6.array(ModelOptionSchema);
1305
+ var MergeOutcomeSchema = z7.object({ ok: z7.boolean(), detail: z7.string() });
1306
+ var ModelListSchema = z7.array(ModelOptionSchema);
1121
1307
  var BRANCH_FILE_LIMIT_BYTES = 8 * 1024 * 1024;
1122
- 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" });
1123
- var BranchNameSchema = z6.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/);
1124
- var BranchReadSchema = z6.object({
1308
+ 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" });
1309
+ var BranchNameSchema = z7.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/);
1310
+ var BranchReadSchema = z7.object({
1125
1311
  repo: ProjectRepoSchema,
1126
1312
  branch: BranchNameSchema,
1127
1313
  path: BranchPathSchema
1128
1314
  });
1129
- var BranchCommitSchema = z6.object({
1130
- id: z6.string().min(1),
1315
+ var BranchCommitSchema = z7.object({
1316
+ id: z7.string().min(1),
1131
1317
  at: IsoDateTimeSchema,
1132
- author: z6.string()
1318
+ author: z7.string()
1133
1319
  });
1134
- var BranchFileSchema = z6.discriminatedUnion("kind", [
1135
- z6.object({
1136
- kind: z6.literal("file"),
1320
+ var BranchFileSchema = z7.discriminatedUnion("kind", [
1321
+ z7.object({
1322
+ kind: z7.literal("file"),
1137
1323
  path: BranchPathSchema,
1138
1324
  commit: BranchCommitSchema,
1139
- size: z6.number().int().nonnegative(),
1140
- content: z6.string().nullable()
1325
+ size: z7.number().int().nonnegative(),
1326
+ content: z7.string().nullable()
1141
1327
  }),
1142
- z6.object({
1143
- kind: z6.literal("dir"),
1328
+ z7.object({
1329
+ kind: z7.literal("dir"),
1144
1330
  path: BranchPathSchema,
1145
1331
  commit: BranchCommitSchema,
1146
- entries: z6.array(z6.object({
1147
- name: z6.string().min(1),
1148
- kind: z6.enum(["file", "dir"]),
1149
- size: z6.number().int().nonnegative().nullable()
1332
+ entries: z7.array(z7.object({
1333
+ name: z7.string().min(1),
1334
+ kind: z7.enum(["file", "dir"]),
1335
+ size: z7.number().int().nonnegative().nullable()
1150
1336
  }))
1151
1337
  })
1152
1338
  ]);
1153
- var BranchChangesReadSchema = z6.object({
1339
+ var BranchChangesReadSchema = z7.object({
1154
1340
  repo: ProjectRepoSchema,
1155
1341
  branch: BranchNameSchema
1156
1342
  });
1157
- var BranchFileChangeSchema = z6.object({
1158
- path: z6.string().min(1),
1159
- status: z6.enum(["added", "modified", "deleted"]),
1343
+ var BranchFileChangeSchema = z7.object({
1344
+ path: z7.string().min(1),
1345
+ status: z7.enum(["added", "modified", "deleted"]),
1160
1346
  /** Lines added and removed, or null for a binary file. */
1161
- added: z6.number().int().nonnegative().nullable(),
1162
- removed: z6.number().int().nonnegative().nullable(),
1347
+ added: z7.number().int().nonnegative().nullable(),
1348
+ removed: z7.number().int().nonnegative().nullable(),
1163
1349
  /** Absent when the file changed only through a merge of another branch. */
1164
- lastChange: z6.object({ author: z6.string(), at: IsoDateTimeSchema }).optional()
1350
+ lastChange: z7.object({ author: z7.string(), at: IsoDateTimeSchema }).optional()
1165
1351
  });
1166
- var BranchChangesSchema = z6.object({
1352
+ var BranchChangesSchema = z7.object({
1167
1353
  head: BranchCommitSchema,
1168
- files: z6.array(BranchFileChangeSchema),
1169
- total: z6.number().int().nonnegative()
1354
+ files: z7.array(BranchFileChangeSchema),
1355
+ total: z7.number().int().nonnegative()
1170
1356
  });
1171
- var WorkspaceConversationSchema = z6.discriminatedUnion("kind", [
1172
- z6.object({ kind: z6.literal("thread"), id: UlidSchema }),
1173
- z6.object({ kind: z6.literal("channel"), scope: NameSchema, name: NameSchema })
1357
+ var WorkspaceConversationSchema = z7.discriminatedUnion("kind", [
1358
+ z7.object({ kind: z7.literal("thread"), id: UlidSchema }),
1359
+ z7.object({ kind: z7.literal("channel"), scope: NameSchema, name: NameSchema })
1174
1360
  ]);
1175
1361
  function workspaceConversationOf(turn) {
1176
1362
  if (turn.thread !== void 0) {
@@ -1178,55 +1364,55 @@ function workspaceConversationOf(turn) {
1178
1364
  }
1179
1365
  return turn.channel === void 0 ? null : { kind: "channel", scope: turn.scope, name: turn.channel };
1180
1366
  }
1181
- var HeldWorkspaceSchema = z6.object({
1367
+ var HeldWorkspaceSchema = z7.object({
1182
1368
  agent: NameSchema,
1183
1369
  scope: NameSchema,
1184
1370
  conversation: WorkspaceConversationSchema
1185
1371
  });
1186
- var HeldWorkspacesSchema = z6.object({ workspaces: z6.array(HeldWorkspaceSchema) });
1187
- var SweepResultSchema = z6.object({ kept: z6.array(HeldWorkspaceSchema) });
1188
- var TurnSteerSchema = z6.object({ id: z6.uuid(), text: z6.string().min(1) });
1189
- var RunnerMessageSchema = z6.discriminatedUnion("type", [
1190
- z6.object({ type: z6.literal("turn"), job: TurnJobSchema }),
1191
- z6.object({ type: z6.literal("land"), request: z6.string().min(1), land: LandRequestSchema }),
1372
+ var HeldWorkspacesSchema = z7.object({ workspaces: z7.array(HeldWorkspaceSchema) });
1373
+ var SweepResultSchema = z7.object({ kept: z7.array(HeldWorkspaceSchema) });
1374
+ var TurnSteerSchema = z7.object({ id: z7.uuid(), text: z7.string().min(1) });
1375
+ var RunnerMessageSchema = z7.discriminatedUnion("type", [
1376
+ z7.object({ type: z7.literal("turn"), job: TurnJobSchema }),
1377
+ z7.object({ type: z7.literal("land"), request: z7.string().min(1), land: LandRequestSchema }),
1192
1378
  /** Answered with a `MergeOutcome`. */
1193
- z6.object({ type: z6.literal("close"), request: z6.string().min(1), close: CloseRequestSchema }),
1194
- z6.object({ type: z6.literal("models"), request: z6.string().min(1), cli: CliKindSchema }),
1379
+ z7.object({ type: z7.literal("close"), request: z7.string().min(1), close: CloseRequestSchema }),
1380
+ z7.object({ type: z7.literal("models"), request: z7.string().min(1), cli: CliKindSchema }),
1195
1381
  /** Answered with a `BranchFile`, or null when the branch or the path does not exist. */
1196
- z6.object({ type: z6.literal("file"), request: z6.string().min(1), read: BranchReadSchema }),
1382
+ z7.object({ type: z7.literal("file"), request: z7.string().min(1), read: BranchReadSchema }),
1197
1383
  /** Answered with `BranchChanges`, or null when the repository or the branch does not exist. */
1198
- z6.object({
1199
- type: z6.literal("changes"),
1200
- request: z6.string().min(1),
1384
+ z7.object({
1385
+ type: z7.literal("changes"),
1386
+ request: z7.string().min(1),
1201
1387
  read: BranchChangesReadSchema
1202
1388
  }),
1203
1389
  /** Answered with whether the CLI accepted it; that it took it comes as a `steered` event. */
1204
- z6.object({
1205
- type: z6.literal("steer"),
1206
- request: z6.string().min(1),
1390
+ z7.object({
1391
+ type: z7.literal("steer"),
1392
+ request: z7.string().min(1),
1207
1393
  turnId: UlidSchema,
1208
1394
  steer: TurnSteerSchema
1209
1395
  }),
1210
1396
  /** Answered with whether the turn was there to stop; it ends `stopped`. */
1211
- z6.object({ type: z6.literal("stop"), request: z6.string().min(1), turnId: UlidSchema }),
1397
+ z7.object({ type: z7.literal("stop"), request: z7.string().min(1), turnId: UlidSchema }),
1212
1398
  /**
1213
1399
  * A conversation ended: the runner removes every citizen's workspace for it but those a turn
1214
1400
  * still uses, and answers with a `SweepResult` naming those it kept for holding work on no branch.
1215
1401
  */
1216
- z6.object({
1217
- type: z6.literal("sweep"),
1218
- request: z6.string().min(1),
1402
+ z7.object({
1403
+ type: z7.literal("sweep"),
1404
+ request: z7.string().min(1),
1219
1405
  conversation: WorkspaceConversationSchema
1220
1406
  })
1221
1407
  ]);
1222
- var RunnerAnswerSchema = z6.discriminatedUnion("ok", [
1223
- z6.object({ ok: z6.literal(true), value: z6.unknown() }),
1224
- z6.object({ ok: z6.literal(false), error: z6.string() })
1408
+ var RunnerAnswerSchema = z7.discriminatedUnion("ok", [
1409
+ z7.object({ ok: z7.literal(true), value: z7.unknown() }),
1410
+ z7.object({ ok: z7.literal(false), error: z7.string() })
1225
1411
  ]);
1226
- var WarmSessionsSchema = z6.object({ warm: z6.array(z6.string()) });
1227
- var FileManifestSchema = z6.object({ files: z6.record(z6.string(), z6.string()) });
1228
- var FileReadSchema = z6.object({ paths: z6.array(z6.string()) });
1229
- var FileContentsSchema = z6.object({ files: z6.record(z6.string(), z6.string()) });
1412
+ var WarmSessionsSchema = z7.object({ warm: z7.array(z7.string()) });
1413
+ var FileManifestSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
1414
+ var FileReadSchema = z7.object({ paths: z7.array(z7.string()) });
1415
+ var FileContentsSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
1230
1416
  function isSafeRelativePath(file) {
1231
1417
  if (file.length === 0 || file.startsWith("/") || file.includes("\\") || file.includes("\0")) {
1232
1418
  return false;
@@ -1235,7 +1421,7 @@ function isSafeRelativePath(file) {
1235
1421
  }
1236
1422
 
1237
1423
  // packages/shared/dist/turns.js
1238
- var TriggerKindSchema = z7.enum([
1424
+ var TriggerKindSchema = z8.enum([
1239
1425
  "mention",
1240
1426
  "stage",
1241
1427
  "task_done",
@@ -1246,27 +1432,32 @@ var TriggerKindSchema = z7.enum([
1246
1432
  "manual",
1247
1433
  "ops_event",
1248
1434
  "user_post",
1249
- "closing"
1435
+ "closing",
1436
+ "cron"
1250
1437
  ]);
1251
- var TriggerSchema = z7.object({
1438
+ var TriggerSchema = z8.object({
1252
1439
  kind: TriggerKindSchema,
1253
1440
  from: NameSchema.optional(),
1254
- fromUser: z7.boolean().default(false),
1255
- reason: z7.string().default(""),
1441
+ fromUser: z8.boolean().default(false),
1442
+ reason: z8.string().default(""),
1256
1443
  taskId: UlidSchema.optional(),
1257
1444
  messageId: UlidSchema.optional(),
1258
1445
  /** The channel the waking message was posted in. */
1259
- channel: ChannelRefSchema.optional()
1446
+ channel: ChannelRefSchema.optional(),
1447
+ /** The cron that came due, for a `cron` wake. */
1448
+ cronId: UlidSchema.optional()
1260
1449
  });
1261
- var TurnThreadSchema = z7.object({ id: UlidSchema, task: z7.boolean() });
1262
- var TurnDispatchSchema = z7.object({
1450
+ var TurnThreadSchema = z8.object({ id: UlidSchema, task: z8.boolean() });
1451
+ var TurnDispatchSchema = z8.object({
1263
1452
  agent: NameSchema,
1264
1453
  project: NameSchema,
1265
1454
  thread: TurnThreadSchema.optional(),
1266
1455
  channel: NameSchema.optional(),
1267
1456
  trigger: TriggerSchema,
1268
- priority: z7.number().int().min(0).max(2),
1269
- onboarding: z7.boolean().default(false)
1457
+ priority: z8.number().int().min(0).max(2),
1458
+ onboarding: z8.boolean().default(false),
1459
+ /** Every cron that came due for this turn, whatever wake it merged into; its prompt quotes each. */
1460
+ crons: z8.array(UlidSchema).optional()
1270
1461
  });
1271
1462
  function conversationPart(conversation) {
1272
1463
  return conversation.thread ?? (conversation.channel === void 0 ? void 0 : `#${conversation.channel}`);
@@ -1290,7 +1481,7 @@ function channelConversation(scope, channel) {
1290
1481
  const name = at < 0 ? channel : channel.slice(at + 1);
1291
1482
  return place === scope && name !== "general" ? name : void 0;
1292
1483
  }
1293
- var TurnRecordSchema = z7.object({
1484
+ var TurnRecordSchema = z8.object({
1294
1485
  /** Assigned by the board when the turn begins; the turn's transcript is filed under it. */
1295
1486
  id: UlidSchema.optional(),
1296
1487
  agent: NameSchema,
@@ -1301,29 +1492,31 @@ var TurnRecordSchema = z7.object({
1301
1492
  channel: NameSchema.optional(),
1302
1493
  runner: NameSchema,
1303
1494
  cli: CliKindSchema.nullable(),
1304
- session: z7.string().nullable(),
1495
+ session: z8.string().nullable(),
1305
1496
  trigger: TriggerSchema,
1306
1497
  startedAt: IsoDateTimeSchema,
1307
1498
  endedAt: IsoDateTimeSchema.nullable(),
1308
1499
  exitReason: TurnExitReasonSchema.nullable(),
1309
1500
  status: TurnStatusSchema.nullable(),
1310
- error: z7.string().nullable(),
1501
+ error: z8.string().nullable(),
1311
1502
  usage: UsageSchema.nullable(),
1312
- costUsd: z7.number().nonnegative().default(0),
1503
+ costUsd: z8.number().nonnegative().default(0),
1313
1504
  /**
1314
1505
  * The running total the CLI reported for the session after this turn, when it reports one. A
1315
1506
  * resumed session starts from it, so the next turn's own cost is measured from here.
1316
1507
  */
1317
- sessionCostUsd: z7.number().nonnegative().optional(),
1318
- toolCalls: z7.number().int().nonnegative().default(0),
1508
+ sessionCostUsd: z8.number().nonnegative().optional(),
1509
+ toolCalls: z8.number().int().nonnegative().default(0),
1319
1510
  /** The model the CLI reported for this turn, else the model configured for the agent, else null. */
1320
- model: z7.string().nullable().default(null),
1511
+ model: z8.string().nullable().default(null),
1321
1512
  /** The task branch the turn left work on, with its commit, as the runner reported it. */
1322
1513
  work: TurnWorkSchema.optional(),
1323
1514
  /** Who stopped the turn, for one that ended `stopped`. */
1324
- stoppedBy: NameSchema.optional()
1515
+ stoppedBy: NameSchema.optional(),
1516
+ /** The crons whose fire started the turn, which record how it ended. */
1517
+ crons: z8.array(UlidSchema).optional()
1325
1518
  });
1326
- var RunningTurnSchema = z7.object({
1519
+ var RunningTurnSchema = z8.object({
1327
1520
  turnId: UlidSchema,
1328
1521
  agent: NameSchema,
1329
1522
  scope: NameSchema,
@@ -1331,49 +1524,49 @@ var RunningTurnSchema = z7.object({
1331
1524
  channel: NameSchema.optional(),
1332
1525
  cli: CliKindSchema,
1333
1526
  askedIn: ChannelRefSchema.optional(),
1334
- steerable: z7.boolean(),
1335
- stoppable: z7.boolean()
1527
+ steerable: z8.boolean(),
1528
+ stoppable: z8.boolean()
1336
1529
  });
1337
- var SessionsFileSchema = z7.object({
1338
- claude: z7.string().optional(),
1339
- codex: z7.string().optional(),
1530
+ var SessionsFileSchema = z8.object({
1531
+ claude: z8.string().optional(),
1532
+ codex: z8.string().optional(),
1340
1533
  runner: NameSchema.optional()
1341
1534
  });
1342
- var WakeRequestSchema = z7.object({
1535
+ var WakeRequestSchema = z8.object({
1343
1536
  agent: NameSchema,
1344
1537
  project: NameSchema,
1345
- reason: z7.string().default("manual wake"),
1538
+ reason: z8.string().default("manual wake"),
1346
1539
  /** A manual wake, or a reflection turn requested ahead of the cadence. */
1347
- kind: z7.enum(["manual", "reflection"]).default("manual")
1540
+ kind: z8.enum(["manual", "reflection"]).default("manual")
1348
1541
  });
1349
- var TurnHistoryEntrySchema = z7.object({
1542
+ var TurnHistoryEntrySchema = z8.object({
1350
1543
  /** The id of the event that ended the turn. */
1351
1544
  id: UlidSchema,
1352
1545
  /** The turn's own id, when it has a transcript to open; turns before transcripts have none. */
1353
1546
  turnId: UlidSchema.optional(),
1354
1547
  ts: IsoDateTimeSchema,
1355
- outcome: z7.enum(["completed", "failed"]),
1548
+ outcome: z8.enum(["completed", "failed"]),
1356
1549
  /** A project slug, or the society scope. */
1357
1550
  project: NameSchema,
1358
1551
  /** The thread whose conversation the turn was in; absent for the home conversation. */
1359
1552
  thread: UlidSchema.optional(),
1360
1553
  /** The channel, never general, whose conversation the turn was in. */
1361
1554
  channel: NameSchema.optional(),
1362
- trigger: z7.string(),
1363
- exitReason: z7.string().nullable(),
1364
- costUsd: z7.number(),
1555
+ trigger: z8.string(),
1556
+ exitReason: z8.string().nullable(),
1557
+ costUsd: z8.number(),
1365
1558
  /** The tokens the turn used; absent for turns logged before turns recorded them. */
1366
1559
  usage: UsageSchema.optional(),
1367
- model: z7.string().nullable(),
1368
- summary: z7.string().nullable(),
1369
- error: z7.string().nullable(),
1560
+ model: z8.string().nullable(),
1561
+ summary: z8.string().nullable(),
1562
+ error: z8.string().nullable(),
1370
1563
  /** When the log recorded the turn's start; absent when the start is missing from the log. */
1371
1564
  startedAt: IsoDateTimeSchema.optional(),
1372
- toolCalls: z7.number().int().nonnegative().optional()
1565
+ toolCalls: z8.number().int().nonnegative().optional()
1373
1566
  });
1374
1567
 
1375
1568
  // packages/shared/dist/ops.js
1376
- import { z as z8 } from "zod";
1569
+ import { z as z9 } from "zod";
1377
1570
  var OPS_SIGNAL_KINDS = [
1378
1571
  "waiting_stage",
1379
1572
  "backlog",
@@ -1386,9 +1579,10 @@ var OPS_SIGNAL_KINDS = [
1386
1579
  "scaled",
1387
1580
  "runner",
1388
1581
  "home_conflict",
1389
- "stuck_workspace"
1582
+ "stuck_workspace",
1583
+ "cron_failing"
1390
1584
  ];
1391
- var OpsSignalKindSchema = z8.enum(OPS_SIGNAL_KINDS);
1585
+ var OpsSignalKindSchema = z9.enum(OPS_SIGNAL_KINDS);
1392
1586
  var WAKING_SIGNAL_KINDS = [
1393
1587
  "backlog",
1394
1588
  "role_gap",
@@ -1398,15 +1592,15 @@ var WAKING_SIGNAL_KINDS = [
1398
1592
  "blocked_capability",
1399
1593
  "scaled"
1400
1594
  ];
1401
- var OpsSignalSchema = z8.object({
1595
+ var OpsSignalSchema = z9.object({
1402
1596
  kind: OpsSignalKindSchema,
1403
1597
  /** Identifies the condition, for example `backlog:demo:engineer`, so a persisting one is not logged again every pass. */
1404
- key: z8.string().min(1),
1598
+ key: z9.string().min(1),
1405
1599
  /** One line a reader can act on. */
1406
- summary: z8.string().min(1),
1600
+ summary: z9.string().min(1),
1407
1601
  /** The counter or timer value behind the signal. */
1408
- value: z8.number(),
1409
- threshold: z8.number().optional(),
1602
+ value: z9.number(),
1603
+ threshold: z9.number().optional(),
1410
1604
  project: NameSchema.optional(),
1411
1605
  agent: NameSchema.optional(),
1412
1606
  role: NameSchema.optional(),
@@ -1414,8 +1608,8 @@ var OpsSignalSchema = z8.object({
1414
1608
  });
1415
1609
 
1416
1610
  // packages/shared/dist/metrics.js
1417
- import { z as z9 } from "zod";
1418
- var MetricsWindowSchema = z9.enum(["24h", "7d", "all"]);
1611
+ import { z as z10 } from "zod";
1612
+ var MetricsWindowSchema = z10.enum(["24h", "7d", "all"]);
1419
1613
  var METRICS_WINDOW_MS = {
1420
1614
  "24h": 24 * 36e5,
1421
1615
  "7d": 7 * 24 * 36e5,
@@ -1426,9 +1620,9 @@ function isBoardAction(tool) {
1426
1620
  const verb = /^mcp__board__(.+)$/.exec(tool)?.[1];
1427
1621
  return verb !== void 0 && !READ_VERBS.some((read2) => read2 === verb);
1428
1622
  }
1429
- var CountSchema = z9.number().int().nonnegative();
1430
- var MsSchema = z9.number().nonnegative();
1431
- var MetricsSchema = z9.object({
1623
+ var CountSchema = z10.number().int().nonnegative();
1624
+ var MsSchema = z10.number().nonnegative();
1625
+ var MetricsSchema = z10.object({
1432
1626
  window: MetricsWindowSchema,
1433
1627
  since: IsoDateTimeSchema.nullable(),
1434
1628
  until: IsoDateTimeSchema,
@@ -1436,12 +1630,12 @@ var MetricsSchema = z9.object({
1436
1630
  * Finished turns that changed nothing on the board. Reflections are left out, since they write
1437
1631
  * memory, not the board; turns without a transcript are counted apart, their actions unknown.
1438
1632
  */
1439
- idle: z9.object({
1633
+ idle: z10.object({
1440
1634
  turns: CountSchema,
1441
1635
  idle: CountSchema,
1442
1636
  unknown: CountSchema,
1443
- byTrigger: z9.array(z9.object({ trigger: z9.string(), turns: CountSchema, idle: CountSchema })),
1444
- byAgent: z9.array(z9.object({
1637
+ byTrigger: z10.array(z10.object({ trigger: z10.string(), turns: CountSchema, idle: CountSchema })),
1638
+ byAgent: z10.array(z10.object({
1445
1639
  agent: NameSchema,
1446
1640
  role: NameSchema.nullable(),
1447
1641
  turns: CountSchema,
@@ -1449,33 +1643,33 @@ var MetricsSchema = z9.object({
1449
1643
  }))
1450
1644
  }),
1451
1645
  /** Work sent back to an earlier stage: over the tasks finished in the window, and by where. */
1452
- sentBack: z9.object({
1646
+ sentBack: z10.object({
1453
1647
  finished: CountSchema,
1454
1648
  sendBacks: CountSchema,
1455
1649
  tasksSentBack: CountSchema,
1456
- byStage: z9.array(z9.object({ project: NameSchema, stage: z9.string(), count: CountSchema })),
1457
- bySender: z9.array(z9.object({ agent: NameSchema, count: CountSchema }))
1650
+ byStage: z10.array(z10.object({ project: NameSchema, stage: z10.string(), count: CountSchema })),
1651
+ bySender: z10.array(z10.object({ agent: NameSchema, count: CountSchema }))
1458
1652
  }),
1459
1653
  /** Posts in the threads of the tasks finished in the window, the board's own notices left out. */
1460
- messages: z9.object({
1654
+ messages: z10.object({
1461
1655
  finished: CountSchema,
1462
1656
  messages: CountSchema,
1463
- byProject: z9.array(z9.object({ project: NameSchema, finished: CountSchema, messages: CountSchema })),
1464
- busiest: z9.array(z9.object({
1657
+ byProject: z10.array(z10.object({ project: NameSchema, finished: CountSchema, messages: CountSchema })),
1658
+ busiest: z10.array(z10.object({
1465
1659
  taskId: UlidSchema,
1466
- title: z9.string(),
1660
+ title: z10.string(),
1467
1661
  project: NameSchema,
1468
1662
  messages: CountSchema
1469
1663
  }))
1470
1664
  }),
1471
1665
  /** Time from a mention to the start of the turn it woke, in the conversation it was made in. */
1472
- latency: z9.object({
1666
+ latency: z10.object({
1473
1667
  mentions: CountSchema,
1474
1668
  /** Mentions no turn has followed yet. */
1475
1669
  unanswered: CountSchema,
1476
1670
  medianMs: MsSchema.nullable(),
1477
1671
  slowestMs: MsSchema.nullable(),
1478
- byAgent: z9.array(z9.object({
1672
+ byAgent: z10.array(z10.object({
1479
1673
  agent: NameSchema,
1480
1674
  mentions: CountSchema,
1481
1675
  medianMs: MsSchema,
@@ -1483,61 +1677,61 @@ var MetricsSchema = z9.object({
1483
1677
  }))
1484
1678
  }),
1485
1679
  /** The user's decisions: proposals decided, questions answered, and stages passed or sent back. */
1486
- decisions: z9.object({
1680
+ decisions: z10.object({
1487
1681
  total: CountSchema,
1488
- byDay: z9.array(z9.object({
1489
- day: z9.string(),
1682
+ byDay: z10.array(z10.object({
1683
+ day: z10.string(),
1490
1684
  proposals: CountSchema,
1491
1685
  answers: CountSchema,
1492
1686
  stages: CountSchema
1493
1687
  }))
1494
1688
  }),
1495
1689
  /** Tasks that needed a capability no connected runner offered, and whether they still do. */
1496
- blocked: z9.array(z9.object({
1690
+ blocked: z10.array(z10.object({
1497
1691
  taskId: UlidSchema,
1498
- title: z9.string().nullable(),
1692
+ title: z10.string().nullable(),
1499
1693
  project: NameSchema.nullable(),
1500
- summary: z9.string(),
1694
+ summary: z10.string(),
1501
1695
  firstAt: IsoDateTimeSchema,
1502
1696
  lastAt: IsoDateTimeSchema,
1503
- holds: z9.boolean()
1697
+ holds: z10.boolean()
1504
1698
  }))
1505
1699
  });
1506
1700
 
1507
1701
  // packages/shared/dist/config.js
1508
- import { z as z10 } from "zod";
1509
- var LogLevelSchema = z10.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
1510
- var GithubSignInSchema = z10.object({
1511
- clientId: z10.string().min(1),
1512
- clientSecret: z10.string().min(1),
1702
+ import { z as z11 } from "zod";
1703
+ var LogLevelSchema = z11.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
1704
+ var GithubSignInSchema = z11.object({
1705
+ clientId: z11.string().min(1),
1706
+ clientSecret: z11.string().min(1),
1513
1707
  /** Logins, lowercased; GitHub logins are case-insensitive. */
1514
- users: z10.array(z10.string().min(1)).min(1)
1708
+ users: z11.array(z11.string().min(1)).min(1)
1515
1709
  });
1516
- var ServerConfigSchema = z10.object({
1517
- dataDir: z10.string().min(1),
1518
- host: z10.string().min(1),
1519
- port: z10.number().int().positive().max(65535),
1710
+ var ServerConfigSchema = z11.object({
1711
+ dataDir: z11.string().min(1),
1712
+ host: z11.string().min(1),
1713
+ port: z11.number().int().positive().max(65535),
1520
1714
  /**
1521
1715
  * An address every runner's agents reach this server at, for example https://board.example, or
1522
1716
  * null for each runner to use the address it reaches the server at itself.
1523
1717
  */
1524
- publicUrl: z10.string().min(1).nullable(),
1718
+ publicUrl: z11.string().min(1).nullable(),
1525
1719
  logLevel: LogLevelSchema,
1526
1720
  /** Turns running at once across every runner, or null for no limit beyond each runner's own. */
1527
- concurrency: z10.number().int().positive().nullable(),
1721
+ concurrency: z11.number().int().positive().nullable(),
1528
1722
  /** How long one turn may run before it is stopped, or null for no limit. */
1529
- turnTimeoutMs: z10.number().int().positive().nullable(),
1723
+ turnTimeoutMs: z11.number().int().positive().nullable(),
1530
1724
  /** Rounds of tool calls one Claude turn may take, or null for no limit. */
1531
- toolRounds: z10.number().int().positive().nullable(),
1725
+ toolRounds: z11.number().int().positive().nullable(),
1532
1726
  /** How long a resident session stays warm after its last turn. */
1533
- residentIdleMs: z10.number().int().positive(),
1727
+ residentIdleMs: z11.number().int().positive(),
1534
1728
  /** Signing in to the board with GitHub, or null when only a token lets anyone in. */
1535
1729
  github: GithubSignInSchema.nullable(),
1536
1730
  /**
1537
1731
  * Reverse proxies in front of the server whose X-Forwarded-For entries name the client, for the
1538
1732
  * rate limits on what anyone may ask without a token: 0 when clients connect directly.
1539
1733
  */
1540
- trustedProxies: z10.number().int().min(0).max(16)
1734
+ trustedProxies: z11.number().int().min(0).max(16)
1541
1735
  });
1542
1736
  function limit(value, fallback) {
1543
1737
  if (value === void 0 || value.trim() === "") {
@@ -1575,25 +1769,25 @@ function loadServerConfig(env) {
1575
1769
  trustedProxies: Number(env["STELLARIS_TRUSTED_PROXIES"] ?? "0")
1576
1770
  });
1577
1771
  }
1578
- var RunnerConfigSchema = z10.object({
1772
+ var RunnerConfigSchema = z11.object({
1579
1773
  /** The board server, for example http://127.0.0.1:4700. */
1580
- serverUrl: z10.string().url(),
1774
+ serverUrl: z11.string().url(),
1581
1775
  /**
1582
1776
  * The runner's token, from `runner add`, or null for the one it saved when it was enrolled, or
1583
1777
  * to enroll it.
1584
1778
  */
1585
- token: z10.string().min(1).nullable(),
1779
+ token: z11.string().min(1).nullable(),
1586
1780
  /** The runner's own data directory: the board mirror, agent homes, repositories, worktrees. */
1587
- dataDir: z10.string().min(1),
1781
+ dataDir: z11.string().min(1),
1588
1782
  logLevel: LogLevelSchema,
1589
1783
  /** Turns this machine runs at once, or null for no limit. */
1590
- slots: z10.number().int().positive().nullable(),
1591
- clis: z10.array(CliKindSchema).min(1),
1592
- capabilities: z10.array(z10.string())
1784
+ slots: z11.number().int().positive().nullable(),
1785
+ clis: z11.array(CliKindSchema).min(1),
1786
+ capabilities: z11.array(z11.string())
1593
1787
  });
1594
1788
 
1595
1789
  // packages/shared/dist/version.js
1596
- var STELLARIS_VERSION = "0.3.13";
1790
+ var STELLARIS_VERSION = "0.3.15";
1597
1791
 
1598
1792
  // packages/board-core/dist/errors.js
1599
1793
  var BoardError = class extends Error {
@@ -2141,32 +2335,32 @@ function filesOf(lines, status) {
2141
2335
  }
2142
2336
 
2143
2337
  // packages/board-core/dist/metrics.js
2144
- import { z as z11 } from "zod";
2145
- var TurnPayload = z11.object({
2146
- turnId: z11.string().optional(),
2147
- project: z11.string(),
2148
- thread: z11.string().optional(),
2149
- channel: z11.string().optional(),
2150
- trigger: z11.string()
2338
+ import { z as z12 } from "zod";
2339
+ var TurnPayload = z12.object({
2340
+ turnId: z12.string().optional(),
2341
+ project: z12.string(),
2342
+ thread: z12.string().optional(),
2343
+ channel: z12.string().optional(),
2344
+ trigger: z12.string()
2151
2345
  });
2152
- var TaskPayload = z11.object({
2153
- taskId: z11.string(),
2154
- project: z11.string(),
2155
- from: z11.string().optional()
2346
+ var TaskPayload = z12.object({
2347
+ taskId: z12.string(),
2348
+ project: z12.string(),
2349
+ from: z12.string().optional()
2156
2350
  });
2157
- var MessagePayload = z11.object({
2158
- id: z11.string().optional(),
2159
- channel: z11.string(),
2160
- thread: z11.string().nullable().optional(),
2161
- mentions: z11.array(z11.string()).default([])
2351
+ var MessagePayload = z12.object({
2352
+ id: z12.string().optional(),
2353
+ channel: z12.string(),
2354
+ thread: z12.string().nullable().optional(),
2355
+ mentions: z12.array(z12.string()).default([])
2162
2356
  });
2163
- var SteeredPayload = z11.object({ messages: z11.array(z11.string()) });
2164
- var SignalPayload = z11.object({
2165
- kind: z11.string(),
2166
- key: z11.string(),
2167
- summary: z11.string(),
2168
- taskId: z11.string().optional(),
2169
- project: z11.string().optional()
2357
+ var SteeredPayload = z12.object({ messages: z12.array(z12.string()) });
2358
+ var SignalPayload = z12.object({
2359
+ kind: z12.string(),
2360
+ key: z12.string(),
2361
+ summary: z12.string(),
2362
+ taskId: z12.string().optional(),
2363
+ project: z12.string().optional()
2170
2364
  });
2171
2365
  function median(values) {
2172
2366
  const sorted = values.toSorted((a, b) => a - b);
@@ -2535,6 +2729,13 @@ var BoardPaths = class {
2535
2729
  members() {
2536
2730
  return path5.join(this.society, "members");
2537
2731
  }
2732
+ /** Crons, one file each with its note as the body; ended ones stay. */
2733
+ crons() {
2734
+ return path5.join(this.society, "crons");
2735
+ }
2736
+ cron(id) {
2737
+ return path5.join(this.crons(), `${id}.md`);
2738
+ }
2538
2739
  member(name) {
2539
2740
  return path5.join(this.members(), `${name}.md`);
2540
2741
  }
@@ -2681,41 +2882,41 @@ function namesList(names) {
2681
2882
  return names.length <= 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}`;
2682
2883
  }
2683
2884
  var SKIPPED_IN_HOME = /* @__PURE__ */ new Set([".git", "turns", ".claude", ".codex"]);
2684
- var RunnerTokensSchema = z12.object({
2685
- runners: z12.record(z12.string(), z12.object({ tokenHash: z12.string().min(1) }))
2885
+ var RunnerTokensSchema = z13.object({
2886
+ runners: z13.record(z13.string(), z13.object({ tokenHash: z13.string().min(1) }))
2686
2887
  });
2687
- var SignInsSchema = z12.object({
2688
- signIns: z12.array(z12.object({
2689
- tokenHash: z12.string().min(1),
2690
- login: z12.string().min(1),
2888
+ var SignInsSchema = z13.object({
2889
+ signIns: z13.array(z13.object({
2890
+ tokenHash: z13.string().min(1),
2891
+ login: z13.string().min(1),
2691
2892
  /** The account's picture at the identity provider; sign-ins from before it have none. */
2692
- avatarUrl: z12.url().optional(),
2893
+ avatarUrl: z13.url().optional(),
2693
2894
  createdAt: IsoDateTimeSchema,
2694
2895
  expiresAt: IsoDateTimeSchema
2695
2896
  })).default([])
2696
2897
  });
2697
- var TurnHistoryPayloadSchema = z12.object({
2898
+ var TurnHistoryPayloadSchema = z13.object({
2698
2899
  project: NameSchema,
2699
2900
  thread: UlidSchema.optional(),
2700
2901
  channel: NameSchema.optional(),
2701
- trigger: z12.string().default("manual"),
2702
- exitReason: z12.string().nullable().default(null),
2703
- costUsd: z12.number().default(0),
2902
+ trigger: z13.string().default("manual"),
2903
+ exitReason: z13.string().nullable().default(null),
2904
+ costUsd: z13.number().default(0),
2704
2905
  usage: UsageSchema.optional(),
2705
- model: z12.string().nullable().default(null),
2706
- summary: z12.string().nullable().default(null),
2707
- error: z12.string().nullable().default(null),
2708
- toolCalls: z12.number().int().nonnegative().optional(),
2906
+ model: z13.string().nullable().default(null),
2907
+ summary: z13.string().nullable().default(null),
2908
+ error: z13.string().nullable().default(null),
2909
+ toolCalls: z13.number().int().nonnegative().optional(),
2709
2910
  turnId: UlidSchema.optional()
2710
2911
  });
2711
- var TurnStartPayloadSchema = z12.object({ project: NameSchema, turnId: UlidSchema.optional() });
2712
- var CursorsSchema = z12.object({
2713
- digest: z12.string().nullable(),
2714
- scopes: z12.record(z12.string(), z12.string()).default({}),
2715
- threadsFrom: z12.record(z12.string(), z12.string().nullable()).default({}),
2716
- channelsFrom: z12.record(z12.string(), z12.string().nullable()).default({}),
2912
+ var TurnStartPayloadSchema = z13.object({ project: NameSchema, turnId: UlidSchema.optional() });
2913
+ var CursorsSchema = z13.object({
2914
+ digest: z13.string().nullable(),
2915
+ scopes: z13.record(z13.string(), z13.string()).default({}),
2916
+ threadsFrom: z13.record(z13.string(), z13.string().nullable()).default({}),
2917
+ channelsFrom: z13.record(z13.string(), z13.string().nullable()).default({}),
2717
2918
  /** How far a work role has read news, which goes to whichever of its general conversations reads first. */
2718
- news: z12.string().optional()
2919
+ news: z13.string().optional()
2719
2920
  });
2720
2921
  var NO_CURSORS = { digest: null, scopes: {}, threadsFrom: {}, channelsFrom: {} };
2721
2922
  function conversationKey(scope, part) {
@@ -2754,7 +2955,7 @@ function byId(a, b) {
2754
2955
  function turnTime(record) {
2755
2956
  return record.endedAt ?? record.startedAt;
2756
2957
  }
2757
- var PausedSchema = z12.object({ paused: z12.boolean() });
2958
+ var PausedSchema = z13.object({ paused: z13.boolean() });
2758
2959
  var DEFAULT_LEASE_MS = 30 * 60 * 1e3;
2759
2960
  var MENTION_PATTERN = /(^|[^\w@])@([a-z0-9][a-z0-9-]{0,31})(?![\w-])/g;
2760
2961
  function withoutMentions(text) {
@@ -2766,7 +2967,7 @@ var OPS_WAKE_TRIGGER = "ops_event";
2766
2967
  var FRONT_DESK_TRIGGER = "user_post";
2767
2968
  var OPS_CHANNELS = ["governance"];
2768
2969
  var FRONT_DESK_CHANNELS = [ASK_CHANNEL];
2769
- var AlignmentsSchema = z12.object({ applied: z12.array(z12.string()).default([]) });
2970
+ var AlignmentsSchema = z13.object({ applied: z13.array(z13.string()).default([]) });
2770
2971
  var FOLLOW_DECISIONS = "ops-readers-follow-decisions";
2771
2972
  var THREAD_RECORDS = "threads-as-records";
2772
2973
  var DIGEST_CURSORS = "digest-replaces-inbox";
@@ -2778,6 +2979,8 @@ var ASKS_CHANNEL_OPENED = "asks-channel-opened";
2778
2979
  var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
2779
2980
  var CHANNEL_CONVERSATIONS = "channel-conversations";
2780
2981
  var DASHBOARD_UPDATES = "dashboard-updates-granted";
2982
+ var CRON_VERBS = "cron-verbs-granted";
2983
+ var CRON_VERB_NAMES = ["create_cron", "update_cron", "remove_cron"];
2781
2984
  var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
2782
2985
  var DASHBOARD_LINE = "The project's members keep this page current with update_dashboard.";
2783
2986
  var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
@@ -2979,7 +3182,8 @@ var Board = class _Board {
2979
3182
  ASKS_CHANNEL_OPENED,
2980
3183
  KNOWLEDGE_REMOVAL,
2981
3184
  CHANNEL_CONVERSATIONS,
2982
- DASHBOARD_UPDATES
3185
+ DASHBOARD_UPDATES,
3186
+ CRON_VERBS
2983
3187
  ].filter((name) => !applied.includes(name));
2984
3188
  if (pending.length === 0) {
2985
3189
  return;
@@ -3018,6 +3222,9 @@ var Board = class _Board {
3018
3222
  await this.grantBesideKnowledge("update_dashboard");
3019
3223
  await this.rewordDashboards();
3020
3224
  }
3225
+ if (pending.includes(CRON_VERBS)) {
3226
+ await this.grantToEveryRole(CRON_VERB_NAMES);
3227
+ }
3021
3228
  await writeJson(file, { applied: [...applied, ...pending] });
3022
3229
  }
3023
3230
  /**
@@ -3031,6 +3238,15 @@ var Board = class _Board {
3031
3238
  }
3032
3239
  }
3033
3240
  }
3241
+ /** Grants verbs once to every charter but the user's, which holds every verb already. */
3242
+ async grantToEveryRole(verbs) {
3243
+ for (const charter of await this.listRoles()) {
3244
+ const missing = verbs.filter((verb) => !charter.verbs.includes(verb));
3245
+ if (charter.name !== USER_ROLE && missing.length > 0) {
3246
+ await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, ...missing] }, { verbsAdded: missing });
3247
+ }
3248
+ }
3249
+ }
3034
3250
  /** A dashboard still saying agents may edit its file says how it changes now. */
3035
3251
  async rewordDashboards() {
3036
3252
  for (const slug of await listDirs(this.paths.projects())) {
@@ -3175,7 +3391,7 @@ var Board = class _Board {
3175
3391
  }
3176
3392
  }
3177
3393
  for (const file of await listFiles(this.paths.tasks(slug))) {
3178
- const raw = await readMarkdown(path6.join(this.paths.tasks(slug), file), z12.record(z12.string(), z12.unknown()));
3394
+ const raw = await readMarkdown(path6.join(this.paths.tasks(slug), file), z13.record(z13.string(), z13.unknown()));
3179
3395
  const legacy = raw.data["thread"];
3180
3396
  if (legacy === void 0) {
3181
3397
  continue;
@@ -3253,7 +3469,7 @@ var Board = class _Board {
3253
3469
  * turns, so it follows nothing now.
3254
3470
  */
3255
3471
  async renameInboxCursors() {
3256
- const Legacy = z12.object({ inbox: z12.string().nullable() });
3472
+ const Legacy = z13.object({ inbox: z13.string().nullable() });
3257
3473
  for (const agent of await this.listAgents()) {
3258
3474
  const file = this.paths.agentCursors(agent.name);
3259
3475
  if (await exists(file)) {
@@ -3299,7 +3515,7 @@ ${charter.purpose}
3299
3515
  */
3300
3516
  async alignSeedRole(seed) {
3301
3517
  const file = this.paths.role(seed.name);
3302
- const raw = await readMarkdown(file, z12.record(z12.string(), z12.unknown()));
3518
+ const raw = await readMarkdown(file, z13.record(z13.string(), z13.unknown()));
3303
3519
  const current = RoleCharterSchema.parse(raw.data);
3304
3520
  const verbsAdded = seed.verbs.filter((verb) => !current.verbs.includes(verb));
3305
3521
  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));
@@ -3612,14 +3828,14 @@ ${charter.purpose}
3612
3828
  /** The society's norms, the one knowledge topic every turn loads. Empty until the steward writes it. */
3613
3829
  async readSocietyNorms() {
3614
3830
  const file = this.paths.societyKnowledgeFile("norms");
3615
- return await exists(file) ? (await readMarkdown(file, z12.record(z12.string(), z12.unknown()))).body : "";
3831
+ return await exists(file) ? (await readMarkdown(file, z13.record(z13.string(), z13.unknown()))).body : "";
3616
3832
  }
3617
3833
  /** Knowledge topics of a project, or of the society when the project is null. */
3618
3834
  async listKnowledge(project) {
3619
3835
  const dir = project === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project);
3620
3836
  const topics = [];
3621
3837
  for (const file of await listFiles(dir)) {
3622
- const doc = await readMarkdown(path6.join(dir, file), z12.record(z12.string(), z12.unknown()));
3838
+ const doc = await readMarkdown(path6.join(dir, file), z13.record(z13.string(), z13.unknown()));
3623
3839
  const parsed = KnowledgeSchema.safeParse({
3624
3840
  ...doc.data,
3625
3841
  project,
@@ -4019,6 +4235,21 @@ ${DASHBOARD_LINE}
4019
4235
  await this.events.append("paused.changed", actor.name, { paused });
4020
4236
  });
4021
4237
  }
4238
+ /** Sets the society's time zone, the user's alone: crons that name none are read in it. */
4239
+ async setTimezone(actor, zone) {
4240
+ this.assertUser(actor, "only the user sets the society's time zone");
4241
+ const timezone = TimeZoneSchema.parse(zone);
4242
+ return this.mutex.run(async () => {
4243
+ const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
4244
+ const society = { ...doc.data, timezone };
4245
+ await writeMarkdown(this.paths.societyFile(), society, doc.body);
4246
+ await this.events.append("society.configured", actor.name, {
4247
+ timezone,
4248
+ previousTimezone: doc.data.timezone
4249
+ });
4250
+ return society;
4251
+ });
4252
+ }
4022
4253
  /** Releases every claim whose lease has expired. Called by the scheduler on its tick. */
4023
4254
  async expireLeases() {
4024
4255
  return this.mutex.run(async () => {
@@ -4441,6 +4672,241 @@ ${current.body}`);
4441
4672
  return updated;
4442
4673
  });
4443
4674
  }
4675
+ /** Every cron, ended ones included, oldest first. */
4676
+ async listCrons() {
4677
+ const crons = [];
4678
+ for (const file of await listFiles(this.paths.crons())) {
4679
+ crons.push(await this.readCronFile(path6.join(this.paths.crons(), file)));
4680
+ }
4681
+ return crons;
4682
+ }
4683
+ async readCron(id) {
4684
+ const file = this.paths.cron(id);
4685
+ if (!await exists(file)) {
4686
+ throw new BoardError("NOT_FOUND", `cron ${id} not found`);
4687
+ }
4688
+ return this.readCronFile(file);
4689
+ }
4690
+ /**
4691
+ * Sets a cron: one that wakes its setter, or, set by a planning role, any citizen. Without a
4692
+ * place named it fires in the conversation of the turn that set it.
4693
+ */
4694
+ async createCron(actor, input) {
4695
+ const args = VerbInputs.create_cron.parse(input);
4696
+ await this.authorize(actor, "create_cron");
4697
+ const target = args.agent ?? actor.name;
4698
+ this.assertMayCron(actor, target, "set");
4699
+ if (args.cron === void 0 === (args.at === void 0)) {
4700
+ throw new BoardError("VALIDATION", "a cron needs either cron, a five-field expression, or at, one time");
4701
+ }
4702
+ return this.mutex.run(async () => {
4703
+ const agent = await this.readAgent(target);
4704
+ if (agent.status !== "active" || agent.cli === null) {
4705
+ throw new BoardError("INVALID_STATE", `${target} takes no turns, so no cron can wake it`);
4706
+ }
4707
+ const where = await this.cronConversation(actor, agent, args);
4708
+ const now = this.now();
4709
+ const schedule = await this.cronSchedule(args, void 0, now);
4710
+ if (schedule === void 0) {
4711
+ throw new BoardError("VALIDATION", "a cron needs a schedule");
4712
+ }
4713
+ const cron = await this.writeCron({
4714
+ id: this.newId(),
4715
+ title: args.title,
4716
+ agent: agent.name,
4717
+ ...where,
4718
+ schedule,
4719
+ createdBy: actor.name,
4720
+ createdAt: now.toISOString(),
4721
+ since: now.toISOString(),
4722
+ failures: 0,
4723
+ note: args.note
4724
+ });
4725
+ await this.events.append("cron.created", actor.name, {
4726
+ ...this.cronPayload(cron),
4727
+ title: cron.title,
4728
+ schedule: cron.schedule
4729
+ });
4730
+ return cron;
4731
+ });
4732
+ }
4733
+ /** Pauses, resumes, retitles, rewrites, or reschedules a cron; resuming or rescheduling starts it afresh. */
4734
+ async updateCron(actor, input) {
4735
+ const args = VerbInputs.update_cron.parse(input);
4736
+ await this.authorize(actor, "update_cron");
4737
+ if (args.cron !== void 0 && args.at !== void 0) {
4738
+ throw new BoardError("VALIDATION", "a cron fires on an expression or at one time, not both");
4739
+ }
4740
+ return this.mutex.run(async () => {
4741
+ const current = await this.readCron(args.cron_id);
4742
+ this.assertMayCron(actor, current.agent, "change");
4743
+ if (current.ended !== void 0) {
4744
+ throw new BoardError("INVALID_STATE", `cron ${current.id} has ended (${current.ended.reason})`);
4745
+ }
4746
+ const now = this.now();
4747
+ const schedule = await this.cronSchedule(args, current.schedule, now);
4748
+ const resumed = args.paused === false && current.paused !== void 0;
4749
+ const changed = [
4750
+ ...args.title === void 0 ? [] : ["title"],
4751
+ ...args.note === void 0 ? [] : ["note"],
4752
+ ...schedule === void 0 ? [] : ["schedule"],
4753
+ ...args.paused === true && current.paused === void 0 ? ["paused"] : [],
4754
+ ...resumed ? ["resumed"] : []
4755
+ ];
4756
+ if (changed.length === 0 && args.paused === void 0) {
4757
+ throw new BoardError("VALIDATION", "update_cron needs paused, title, note, cron, timezone, or at");
4758
+ }
4759
+ const ts = now.toISOString();
4760
+ const { paused: wasPaused, ...rest } = current;
4761
+ const paused = args.paused === void 0 ? wasPaused : args.paused ? wasPaused ?? { at: ts, by: actor.name } : void 0;
4762
+ const cron = await this.writeCron({
4763
+ ...rest,
4764
+ ...args.title === void 0 ? {} : { title: args.title },
4765
+ ...args.note === void 0 ? {} : { note: args.note },
4766
+ ...schedule === void 0 ? {} : { schedule },
4767
+ ...schedule !== void 0 || resumed ? { since: ts } : {},
4768
+ ...paused === void 0 ? {} : { paused }
4769
+ });
4770
+ if (changed.length > 0) {
4771
+ await this.events.append("cron.updated", actor.name, {
4772
+ ...this.cronPayload(cron),
4773
+ changed,
4774
+ ...schedule === void 0 ? {} : { schedule }
4775
+ });
4776
+ }
4777
+ return cron;
4778
+ });
4779
+ }
4780
+ /** Ends a cron for good: its setter's, or anyone's for a planning role. */
4781
+ async removeCron(actor, input) {
4782
+ const args = VerbInputs.remove_cron.parse(input);
4783
+ await this.authorize(actor, "remove_cron");
4784
+ return this.mutex.run(async () => {
4785
+ const current = await this.readCron(args.cron_id);
4786
+ this.assertMayCron(actor, current.agent, "end");
4787
+ if (current.ended !== void 0) {
4788
+ throw new BoardError("INVALID_STATE", `cron ${current.id} has ended already (${current.ended.reason})`);
4789
+ }
4790
+ return this.endCronUnlocked(current, actor.name, args.reason);
4791
+ });
4792
+ }
4793
+ /** A cron wakes its setter; one that wakes another citizen is the planning roles' to set, change, and end. */
4794
+ assertMayCron(actor, agent, what) {
4795
+ if (agent !== actor.name && !PLANNING_ROLES.includes(actor.role)) {
4796
+ throw new BoardError("FORBIDDEN", `only the user, the steward, and the concierge ${what} a cron that wakes another citizen`);
4797
+ }
4798
+ }
4799
+ /**
4800
+ * Where a cron fires for its citizen: the thread, the channel, or the scope's home it names, else
4801
+ * the conversation of the turn that set it when it wakes its setter, else its home outside projects.
4802
+ */
4803
+ async cronConversation(actor, agent, named) {
4804
+ const own = actor.name === agent.name;
4805
+ if (named.project === void 0 && named.thread_id === void 0 && named.channel === void 0) {
4806
+ return own && actor.scope !== void 0 ? this.cronConversation(actor, agent, {
4807
+ project: actor.scope,
4808
+ thread_id: actor.thread,
4809
+ channel: actor.channel
4810
+ }) : { scope: SOCIETY_SCOPE };
4811
+ }
4812
+ if (named.thread_id !== void 0) {
4813
+ const { thread } = await this.findThread(named.thread_id);
4814
+ if (thread.state !== "open") {
4815
+ throw new BoardError("INVALID_STATE", `thread ${thread.id} is closed`);
4816
+ }
4817
+ const scope2 = wakeScope(agent, parseChannelRef(thread.channel).project);
4818
+ if (named.project !== void 0 && named.project !== scope2) {
4819
+ throw new BoardError("VALIDATION", `thread ${thread.id} is on ${thread.channel}, so ${agent.name}'s conversation there is in ${scope2}, not ${named.project}`);
4820
+ }
4821
+ return { scope: scope2, thread: thread.id };
4822
+ }
4823
+ const scope = named.project ?? (own ? actor.scope : void 0) ?? SOCIETY_SCOPE;
4824
+ if (scope !== SOCIETY_SCOPE) {
4825
+ await this.readActiveProject(scope);
4826
+ if (!agent.memberships.includes(scope)) {
4827
+ throw new BoardError("VALIDATION", `${agent.name} is not a member of ${scope}`);
4828
+ }
4829
+ }
4830
+ if (named.channel === void 0 || named.channel === "general") {
4831
+ return { scope };
4832
+ }
4833
+ await this.assertChannelOpen(channelRef(scope === SOCIETY_SCOPE ? null : scope, named.channel));
4834
+ return { scope, channel: named.channel };
4835
+ }
4836
+ /**
4837
+ * The schedule a cron verb asks for, or undefined when it asks for none: an expression, read in
4838
+ * its zone or the society's, which must name a time to come and no two fires closer than the
4839
+ * floor, or one time to come. A zone alone keeps the expression of `existing`.
4840
+ */
4841
+ async cronSchedule(args, existing, now) {
4842
+ if (args.at !== void 0) {
4843
+ if (args.timezone !== void 0) {
4844
+ throw new BoardError("VALIDATION", "at carries its own offset; timezone is for a cron expression");
4845
+ }
4846
+ const at = new Date(args.at);
4847
+ if (at <= now) {
4848
+ throw new BoardError("VALIDATION", `${args.at} has passed; a cron fires at a time to come`);
4849
+ }
4850
+ return { at: at.toISOString() };
4851
+ }
4852
+ if (args.cron === void 0 && args.timezone === void 0) {
4853
+ return void 0;
4854
+ }
4855
+ const expression = args.cron ?? (existing === void 0 || isOneTime(existing) ? void 0 : existing.cron);
4856
+ if (expression === void 0) {
4857
+ throw new BoardError("VALIDATION", "timezone is for a cron expression, which this cron has not");
4858
+ }
4859
+ const kept = args.timezone === void 0 && existing !== void 0 && !isOneTime(existing) ? existing.timezone : args.timezone ?? void 0;
4860
+ const schedule = {
4861
+ cron: expression,
4862
+ ...kept === void 0 ? {} : { timezone: kept }
4863
+ };
4864
+ const { timezone: societyZone } = await this.society();
4865
+ if (fireTimes(schedule, societyZone, now, 1).length === 0) {
4866
+ throw new BoardError("VALIDATION", `${expression} names no time to come`);
4867
+ }
4868
+ const gap = shortestGapMs(schedule, societyZone, now);
4869
+ if (gap !== null && gap < CRON_FLOOR_MS) {
4870
+ 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`);
4871
+ }
4872
+ return schedule;
4873
+ }
4874
+ /** A cron's events name its citizen and the conversation it fires in, as turn events do. */
4875
+ cronPayload(cron) {
4876
+ return {
4877
+ cronId: cron.id,
4878
+ agent: cron.agent,
4879
+ project: cron.scope,
4880
+ ...cron.thread === void 0 ? {} : { thread: cron.thread },
4881
+ ...cron.channel === void 0 ? {} : { channel: cron.channel }
4882
+ };
4883
+ }
4884
+ async readCronFile(file) {
4885
+ const doc = await readMarkdown(file, CronFrontmatterSchema);
4886
+ return { ...doc.data, note: doc.body.trimEnd() };
4887
+ }
4888
+ async writeCron(cron) {
4889
+ const { note, ...frontmatter } = cron;
4890
+ const data = CronFrontmatterSchema.parse(Object.fromEntries(Object.entries(frontmatter).filter(([, value]) => value !== void 0)));
4891
+ await writeMarkdown(this.paths.cron(cron.id), data, note);
4892
+ return { ...data, note };
4893
+ }
4894
+ async endCronUnlocked(cron, by, reason) {
4895
+ const ended = await this.writeCron({
4896
+ ...cron,
4897
+ ended: { at: this.now().toISOString(), by, reason }
4898
+ });
4899
+ await this.events.append("cron.ended", by, { ...this.cronPayload(ended), reason });
4900
+ return ended;
4901
+ }
4902
+ /** Ends every running cron `ends` picks, in the transaction that ends the conversation it fires in. */
4903
+ async endCronsUnlocked(by, reason, ends) {
4904
+ for (const cron of await this.listCrons()) {
4905
+ if (cron.ended === void 0 && ends(cron)) {
4906
+ await this.endCronUnlocked(cron, by, reason);
4907
+ }
4908
+ }
4909
+ }
4444
4910
  async openThread(actor, input) {
4445
4911
  const args = VerbInputs.open_thread.parse(input);
4446
4912
  await this.authorize(actor, "open_thread");
@@ -4657,6 +5123,7 @@ ${args.summary}`);
4657
5123
  threadId: thread.id,
4658
5124
  channel: thread.channel
4659
5125
  });
5126
+ await this.endCronsUnlocked(actor.name, "its thread closed", (cron) => cron.thread === thread.id);
4660
5127
  return summary;
4661
5128
  });
4662
5129
  }
@@ -5336,6 +5803,7 @@ ${rationale}`}`);
5336
5803
  members: project.members.filter((member) => member !== target)
5337
5804
  }));
5338
5805
  await this.refreshMember(target);
5806
+ await this.endCronsUnlocked(actor.name, `${target} left ${args.project}`, (cron) => cron.agent === target && cron.scope === args.project);
5339
5807
  await this.events.append("agent.left", actor.name, {
5340
5808
  name: target,
5341
5809
  project: args.project,
@@ -5404,6 +5872,12 @@ ${rationale}`}`);
5404
5872
  return this.archiveChannel(actor, VerbInputs.archive_channel.parse(input));
5405
5873
  case "update_dashboard":
5406
5874
  return this.updateDashboard(actor, VerbInputs.update_dashboard.parse(input));
5875
+ case "create_cron":
5876
+ return this.createCron(actor, VerbInputs.create_cron.parse(input));
5877
+ case "update_cron":
5878
+ return this.updateCron(actor, VerbInputs.update_cron.parse(input));
5879
+ case "remove_cron":
5880
+ return this.removeCron(actor, VerbInputs.remove_cron.parse(input));
5407
5881
  default:
5408
5882
  throw new BoardError("VALIDATION", `unknown verb ${String(verb)}`);
5409
5883
  }
@@ -5450,7 +5924,7 @@ ${rationale}`}`);
5450
5924
  }
5451
5925
  async readAgentRoleBody(agent) {
5452
5926
  const file = this.paths.agentRole(agent);
5453
- return await exists(file) ? (await readMarkdown(file, z12.record(z12.string(), z12.unknown()))).body : "";
5927
+ return await exists(file) ? (await readMarkdown(file, z13.record(z13.string(), z13.unknown()))).body : "";
5454
5928
  }
5455
5929
  /** The steps of one finished turn, as the runner handed them over when it ended. */
5456
5930
  /**
@@ -5628,6 +6102,9 @@ ${rationale}`}`);
5628
6102
  }
5629
6103
  await this.writeTurnRecord(parsed);
5630
6104
  await this.refreshMember(parsed.agent);
6105
+ for (const id of parsed.crons ?? []) {
6106
+ await this.recordCronRun(id, parsed, failed);
6107
+ }
5631
6108
  if (parsed.exitReason === "stopped") {
5632
6109
  await this.events.append("turn.stopped", parsed.stoppedBy ?? USER_NAME, {
5633
6110
  ...parsed.id === void 0 ? {} : { turnId: parsed.id },
@@ -5709,6 +6186,45 @@ ${rationale}`}`);
5709
6186
  return true;
5710
6187
  });
5711
6188
  }
6189
+ /**
6190
+ * Records a cron's fire, which the scheduler made; a one-time cron ends with it. Null, recording
6191
+ * nothing, for a cron ended or paused since the scheduler read it, which does not fire.
6192
+ */
6193
+ async cronFired(id, at) {
6194
+ return this.mutex.run(async () => {
6195
+ const current = await this.readCron(id);
6196
+ if (current.ended !== void 0 || current.paused !== void 0) {
6197
+ return null;
6198
+ }
6199
+ const cron = await this.writeCron({ ...current, lastFiredAt: at });
6200
+ await this.events.append("cron.fired", SYSTEM_ACTOR.name, this.cronPayload(cron));
6201
+ return isOneTime(cron.schedule) ? this.endCronUnlocked(cron, SYSTEM_ACTOR.name, "fired once, as set") : cron;
6202
+ });
6203
+ }
6204
+ /** Records a time a cron passed because the turn of its previous fire was still queued or running. */
6205
+ async cronSkipped(id, at) {
6206
+ return this.mutex.run(async () => {
6207
+ const cron = await this.writeCron({ ...await this.readCron(id), lastSkippedAt: at });
6208
+ await this.events.append("cron.skipped", SYSTEM_ACTOR.name, this.cronPayload(cron));
6209
+ return cron;
6210
+ });
6211
+ }
6212
+ /**
6213
+ * How the turn a cron's fire started ended. A failure counts toward `cron_failing`, a completed
6214
+ * turn clears the count, and a stop, the user's decision, leaves it.
6215
+ */
6216
+ async recordCronRun(id, record, failed) {
6217
+ if (record.id === void 0 || !await exists(this.paths.cron(id))) {
6218
+ return;
6219
+ }
6220
+ const cron = await this.readCron(id);
6221
+ const outcome = record.exitReason === "stopped" ? "stopped" : failed ? "failed" : "completed";
6222
+ await this.writeCron({
6223
+ ...cron,
6224
+ lastRun: { turnId: record.id, at: record.endedAt ?? this.now().toISOString(), outcome },
6225
+ failures: outcome === "failed" ? cron.failures + 1 : outcome === "completed" ? 0 : cron.failures
6226
+ });
6227
+ }
5712
6228
  /**
5713
6229
  * Whether a conversation with a workspace of its own has ended: a thread closed, a task ended
5714
6230
  * (for one from before tasks had threads), or a channel archived.
@@ -5808,7 +6324,7 @@ ${rationale}`}`);
5808
6324
  return { project: slug, revision: 0, body: "" };
5809
6325
  }
5810
6326
  const { data, body } = await readLooseMarkdown(file);
5811
- const revision = z12.number().int().min(0).safeParse(data["revision"]);
6327
+ const revision = z13.number().int().min(0).safeParse(data["revision"]);
5812
6328
  const updatedBy = NameSchema.safeParse(data["updatedBy"]);
5813
6329
  const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
5814
6330
  return {
@@ -6099,7 +6615,7 @@ ${seed}
6099
6615
  continue;
6100
6616
  }
6101
6617
  const file = this.paths.agentRole(agent.name);
6102
- const body = await exists(file) ? (await readMarkdown(file, z12.record(z12.string(), z12.unknown()))).body : "";
6618
+ const body = await exists(file) ? (await readMarkdown(file, z13.record(z13.string(), z13.unknown()))).body : "";
6103
6619
  const at = body.indexOf(`
6104
6620
  ${SEED_INSTRUCTIONS_HEADING}
6105
6621
  `);
@@ -6543,6 +7059,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
6543
7059
  archivedChannels: [...current.archivedChannels, archived]
6544
7060
  }));
6545
7061
  }
7062
+ await this.endCronsUnlocked(by, `${ref} was archived`, (cron) => cron.scope === scope && cron.channel === channel || cron.thread !== void 0 && threadsClosed.includes(cron.thread));
6546
7063
  await this.events.append("channel.archived", by, {
6547
7064
  channel: ref,
6548
7065
  reason,
@@ -6647,6 +7164,7 @@ ${charter.purpose}
6647
7164
  });
6648
7165
  threadsClosed.push(doc.data.id);
6649
7166
  }
7167
+ await this.endCronsUnlocked(by, `project ${slug} was archived`, (cron) => cron.scope === slug || cron.thread !== void 0 && threadsClosed.includes(cron.thread));
6650
7168
  const project = await this.updateProject(slug, (p) => ({
6651
7169
  ...p,
6652
7170
  members: [],
@@ -6711,6 +7229,7 @@ ${charter.purpose}
6711
7229
  }
6712
7230
  this.tokenIndex.delete(current.tokenHash);
6713
7231
  await this.refreshMember(name);
7232
+ await this.endCronsUnlocked(by, `${name} was retired`, (cron) => cron.agent === name);
6714
7233
  await this.events.append("agent.retired", by, {
6715
7234
  name,
6716
7235
  role: agent.role,
@@ -7022,6 +7541,7 @@ ${charter.purpose}
7022
7541
  channel: found.thread.channel,
7023
7542
  ended
7024
7543
  });
7544
+ await this.endCronsUnlocked(by, `its thread closed: its ${found.thread.subject?.kind ?? "subject"} was ${ended}`, (cron) => cron.thread === subjectId);
7025
7545
  }
7026
7546
  async readMessagesIn(dir, since) {
7027
7547
  const messages = [];
@@ -7085,6 +7605,8 @@ function decideWake(input) {
7085
7605
  return { wake: true, reason: "user post", priority: 2 };
7086
7606
  case "closing":
7087
7607
  return { wake: true, reason: "an ended conversation left work on no branch", priority: 1 };
7608
+ case "cron":
7609
+ return { wake: true, reason: "a cron came due", priority: 1 };
7088
7610
  case "heartbeat":
7089
7611
  break;
7090
7612
  }
@@ -7099,7 +7621,7 @@ function decideWake(input) {
7099
7621
  }
7100
7622
 
7101
7623
  // packages/scheduler/dist/scheduler.js
7102
- import { z as z13 } from "zod";
7624
+ import { z as z14 } from "zod";
7103
7625
  function besideFields(beside) {
7104
7626
  if (beside === void 0) {
7105
7627
  return {};
@@ -7126,21 +7648,21 @@ var DEFAULT_TIMINGS = Object.freeze({
7126
7648
  scaleCooldownMs: 36e5,
7127
7649
  reflectionMs: 24 * 36e5
7128
7650
  });
7129
- var SchedulerTimingsSchema = z13.object({
7130
- pollMs: z13.number().positive(),
7131
- debounceMs: z13.number().nonnegative(),
7132
- userDebounceMs: z13.number().nonnegative(),
7133
- heartbeatMs: z13.number().positive(),
7134
- waitingStageMs: z13.number().positive(),
7135
- leaseSweepMs: z13.number().positive(),
7136
- opsIntervalMs: z13.number().positive(),
7137
- signalRepeatMs: z13.number().positive(),
7138
- costReportMs: z13.number().positive(),
7139
- staleThreadMs: z13.number().positive(),
7140
- idleMemberMs: z13.number().positive(),
7141
- homeConflictMs: z13.number().positive(),
7142
- scaleCooldownMs: z13.number().positive(),
7143
- reflectionMs: z13.number().positive()
7651
+ var SchedulerTimingsSchema = z14.object({
7652
+ pollMs: z14.number().positive(),
7653
+ debounceMs: z14.number().nonnegative(),
7654
+ userDebounceMs: z14.number().nonnegative(),
7655
+ heartbeatMs: z14.number().positive(),
7656
+ waitingStageMs: z14.number().positive(),
7657
+ leaseSweepMs: z14.number().positive(),
7658
+ opsIntervalMs: z14.number().positive(),
7659
+ signalRepeatMs: z14.number().positive(),
7660
+ costReportMs: z14.number().positive(),
7661
+ staleThreadMs: z14.number().positive(),
7662
+ idleMemberMs: z14.number().positive(),
7663
+ homeConflictMs: z14.number().positive(),
7664
+ scaleCooldownMs: z14.number().positive(),
7665
+ reflectionMs: z14.number().positive()
7144
7666
  }).partial();
7145
7667
  function parseTimings(input) {
7146
7668
  const parsed = SchedulerTimingsSchema.parse(input);
@@ -7152,31 +7674,32 @@ function parseTimings(input) {
7152
7674
  }
7153
7675
  return overrides;
7154
7676
  }
7155
- var PendingSchema = z13.object({
7677
+ var PendingSchema = z14.object({
7156
7678
  dispatch: TurnDispatchSchema,
7157
- readyAt: z13.number(),
7158
- stageWakes: z13.array(TriggerSchema).optional(),
7159
- holder: z13.boolean().optional(),
7679
+ readyAt: z14.number(),
7680
+ stageWakes: z14.array(TriggerSchema).optional(),
7681
+ holder: z14.boolean().optional(),
7160
7682
  newestMessage: UlidSchema.optional(),
7161
- onlyMessages: z13.boolean().optional(),
7162
- steeredThrough: UlidSchema.optional()
7683
+ onlyMessages: z14.boolean().optional(),
7684
+ steeredThrough: UlidSchema.optional(),
7685
+ onlyCrons: z14.boolean().optional()
7163
7686
  });
7164
- var StateSchema = z13.object({
7165
- cursor: z13.string().nullable(),
7166
- lastHeartbeat: z13.record(z13.string(), z13.string()),
7687
+ var StateSchema = z14.object({
7688
+ cursor: z14.string().nullable(),
7689
+ lastHeartbeat: z14.record(z14.string(), z14.string()),
7167
7690
  /** Waiting stages already signalled, as `task:stage`, so each is logged once while it waits. */
7168
- waitingSeen: z13.record(z13.string(), z13.string()).default({}),
7691
+ waitingSeen: z14.record(z14.string(), z14.string()).default({}),
7169
7692
  /** Queued dispatches survive a restart; the cursor has already moved past the events that made them. */
7170
- pending: z13.record(z13.string(), PendingSchema).default({}),
7171
- opsReported: z13.record(z13.string(), z13.string()).default({}),
7172
- releaseCounts: z13.record(z13.string(), z13.number().int().nonnegative()).default({}),
7173
- lastOps: z13.string().nullable().default(null),
7174
- lastScaled: z13.record(z13.string(), z13.string()).default({}),
7175
- lastCostReport: z13.string().nullable().default(null),
7176
- costSinceReport: z13.number().nonnegative().default(0),
7177
- turnsSinceReport: z13.number().int().nonnegative().default(0),
7693
+ pending: z14.record(z14.string(), PendingSchema).default({}),
7694
+ opsReported: z14.record(z14.string(), z14.string()).default({}),
7695
+ releaseCounts: z14.record(z14.string(), z14.number().int().nonnegative()).default({}),
7696
+ lastOps: z14.string().nullable().default(null),
7697
+ lastScaled: z14.record(z14.string(), z14.string()).default({}),
7698
+ lastCostReport: z14.string().nullable().default(null),
7699
+ costSinceReport: z14.number().nonnegative().default(0),
7700
+ turnsSinceReport: z14.number().int().nonnegative().default(0),
7178
7701
  /** When each member last reflected, or was first seen; the cadence counts from here. */
7179
- lastReflection: z13.record(z13.string(), z13.string()).default({})
7702
+ lastReflection: z14.record(z14.string(), z14.string()).default({})
7180
7703
  });
7181
7704
  function messageOf(trigger) {
7182
7705
  return trigger.kind === "mention" || trigger.kind === "user_post" ? trigger.messageId : void 0;
@@ -7247,6 +7770,8 @@ var Scheduler = class {
7247
7770
  landing = /* @__PURE__ */ new Set();
7248
7771
  /** Sweeps of ended conversations' workspaces under way, which `drain` and `stop` wait for. */
7249
7772
  sweeping = /* @__PURE__ */ new Set();
7773
+ /** Crons whose fire's turn is running; a cron's next time passes while one is. */
7774
+ cronsInFlight = /* @__PURE__ */ new Set();
7250
7775
  state = StateSchema.parse({
7251
7776
  cursor: null,
7252
7777
  lastHeartbeat: {}
@@ -7326,6 +7851,7 @@ var Scheduler = class {
7326
7851
  this.state.cursor = event.id;
7327
7852
  }
7328
7853
  if (!paused) {
7854
+ await this.checkCrons(now);
7329
7855
  await this.checkHeartbeats(now);
7330
7856
  await this.checkWaitingStages(now);
7331
7857
  await this.checkReflections(now);
@@ -7583,6 +8109,13 @@ var Scheduler = class {
7583
8109
  }
7584
8110
  return;
7585
8111
  }
8112
+ case "cron.ended": {
8113
+ const cronId = stringOf(payload["cronId"]);
8114
+ if (cronId !== null && event.actor !== SYSTEM_ACTOR.name) {
8115
+ this.dropCronWakes(cronId);
8116
+ }
8117
+ return;
8118
+ }
7586
8119
  case "proposal.decided": {
7587
8120
  const proposalId = stringOf(payload["proposalId"]);
7588
8121
  const proposer = stringOf(payload["proposedBy"]);
@@ -7914,6 +8447,7 @@ var Scheduler = class {
7914
8447
  const readyAt = now + debounce;
7915
8448
  const existing = this.pending.get(key);
7916
8449
  const message = messageOf(trigger);
8450
+ const cron = trigger.kind === "cron" ? trigger.cronId : void 0;
7917
8451
  if (existing === void 0) {
7918
8452
  this.pending.set(key, {
7919
8453
  dispatch: {
@@ -7922,24 +8456,30 @@ var Scheduler = class {
7922
8456
  ...besideFields(beside),
7923
8457
  trigger,
7924
8458
  priority: decision.priority,
7925
- onboarding: trigger.kind === "onboarding"
8459
+ onboarding: trigger.kind === "onboarding",
8460
+ ...cron === void 0 ? {} : { crons: [cron] }
7926
8461
  },
7927
8462
  readyAt,
7928
8463
  ...trigger.kind === "stage" ? { stageWakes: [trigger] } : {},
7929
8464
  ...message === void 0 ? {} : { newestMessage: message },
7930
- onlyMessages: message !== void 0
8465
+ onlyMessages: message !== void 0,
8466
+ onlyCrons: cron !== void 0
7931
8467
  });
7932
8468
  return;
7933
8469
  }
7934
8470
  const newestMessage = later(existing.newestMessage, message);
7935
8471
  const keepNew = decision.priority > existing.dispatch.priority;
7936
8472
  const priority = keepNew ? decision.priority : existing.dispatch.priority;
8473
+ const crons = [
8474
+ .../* @__PURE__ */ new Set([...existing.dispatch.crons ?? [], ...cron === void 0 ? [] : [cron]])
8475
+ ];
7937
8476
  this.pending.set(key, {
7938
8477
  dispatch: {
7939
8478
  ...existing.dispatch,
7940
8479
  trigger: keepNew ? trigger : existing.dispatch.trigger,
7941
8480
  priority,
7942
- onboarding: existing.dispatch.onboarding || trigger.kind === "onboarding"
8481
+ onboarding: existing.dispatch.onboarding || trigger.kind === "onboarding",
8482
+ ...crons.length === 0 ? {} : { crons }
7943
8483
  },
7944
8484
  readyAt: Math.min(existing.readyAt, readyAt),
7945
8485
  ...existing.stageWakes !== void 0 && trigger.kind === "stage" ? {
@@ -7948,7 +8488,8 @@ var Scheduler = class {
7948
8488
  } : {},
7949
8489
  ...newestMessage === void 0 ? {} : { newestMessage },
7950
8490
  onlyMessages: existing.onlyMessages === true && message !== void 0,
7951
- ...existing.steeredThrough === void 0 ? {} : { steeredThrough: existing.steeredThrough }
8491
+ ...existing.steeredThrough === void 0 ? {} : { steeredThrough: existing.steeredThrough },
8492
+ onlyCrons: existing.onlyCrons === true && cron !== void 0
7952
8493
  });
7953
8494
  }
7954
8495
  /** Debounce coalesces chatter: mentions, stage handoffs, and bursts of signals. Everything else runs at once. */
@@ -8058,6 +8599,71 @@ var Scheduler = class {
8058
8599
  }
8059
8600
  }
8060
8601
  }
8602
+ /**
8603
+ * Each active cron whose next time has come wakes its citizen in its conversation, unless the turn
8604
+ * of its previous fire is still queued or running, in which case the time passes. The fire is
8605
+ * recorded on the board, so its next time counts from it and a restart does not fire it again.
8606
+ */
8607
+ async checkCrons(now) {
8608
+ const crons = (await this.board.listCrons()).filter((cron) => cron.ended === void 0 && cron.paused === void 0);
8609
+ if (crons.length === 0) {
8610
+ return;
8611
+ }
8612
+ const { timezone } = await this.board.society();
8613
+ for (const cron of crons) {
8614
+ const due = nextFire(cron, timezone);
8615
+ if (due === null || due.getTime() > now) {
8616
+ continue;
8617
+ }
8618
+ if (this.cronQueuedOrRunning(cron.id)) {
8619
+ await this.board.cronSkipped(cron.id, iso(now));
8620
+ this.log.info({ cron: cron.id, agent: cron.agent }, "cron time passed, its previous turn has not ended");
8621
+ continue;
8622
+ }
8623
+ const agent = await this.tryReadAgent(cron.agent);
8624
+ if (agent === null) {
8625
+ continue;
8626
+ }
8627
+ if (await this.board.cronFired(cron.id, iso(now)) === null) {
8628
+ continue;
8629
+ }
8630
+ this.enqueue(agent.name, cron.scope, {
8631
+ kind: "cron",
8632
+ from: cron.createdBy,
8633
+ fromUser: cron.createdBy === USER_NAME,
8634
+ reason: `cron "${cron.title}" came due`,
8635
+ cronId: cron.id
8636
+ }, now, await this.cronConversation(cron));
8637
+ }
8638
+ }
8639
+ /** The conversation a cron fires in, beside its scope's home or none for the home. */
8640
+ async cronConversation(cron) {
8641
+ if (cron.thread !== void 0) {
8642
+ return { thread: await this.conversationOf(cron.thread, cron.scope) };
8643
+ }
8644
+ return cron.channel === void 0 ? void 0 : { channel: cron.channel };
8645
+ }
8646
+ /** Whether a cron's wake waits in the queue or its turn is running. */
8647
+ cronQueuedOrRunning(id) {
8648
+ return this.cronsInFlight.has(id) || [...this.pending.values()].some((item) => item.dispatch.crons?.includes(id) === true);
8649
+ }
8650
+ /** Takes an ended cron out of the queued wakes, and drops a wake left with nothing else to run. */
8651
+ dropCronWakes(id) {
8652
+ for (const [key, item] of this.pending) {
8653
+ const crons = item.dispatch.crons;
8654
+ if (crons?.includes(id) !== true) {
8655
+ continue;
8656
+ }
8657
+ const rest = crons.filter((each) => each !== id);
8658
+ if (rest.length === 0 && item.onlyCrons === true) {
8659
+ this.pending.delete(key);
8660
+ this.log.info({ agent: item.dispatch.agent, cron: id }, "dropping cron wake, the cron ended");
8661
+ continue;
8662
+ }
8663
+ const { crons: _crons, ...dispatch } = item.dispatch;
8664
+ item.dispatch = rest.length === 0 ? dispatch : { ...dispatch, crons: rest };
8665
+ }
8666
+ }
8061
8667
  /**
8062
8668
  * Every `reflectionMs`, a member whose charter reflects takes a reflection turn in the scope of
8063
8669
  * its latest working turn, unless it has not worked since its last reflection.
@@ -8272,6 +8878,7 @@ var Scheduler = class {
8272
8878
  });
8273
8879
  }
8274
8880
  }
8881
+ const crons = (await this.board.listCrons()).filter((cron) => cron.ended === void 0);
8275
8882
  for (const agent of await this.board.listAgents()) {
8276
8883
  if (agent.status !== "active" || agent.cli === null) {
8277
8884
  continue;
@@ -8296,6 +8903,20 @@ var Scheduler = class {
8296
8903
  role: agent.role
8297
8904
  });
8298
8905
  }
8906
+ for (const cron of crons.filter((each) => each.agent === agent.name)) {
8907
+ if (cron.failures >= CRON_FAILING_RUNS) {
8908
+ signals.push({
8909
+ kind: "cron_failing",
8910
+ key: `cron_failing:${cron.id}`,
8911
+ summary: `${agent.name}'s cron ${cron.id} "${cron.title}" has had ${cron.failures} failed turns in a row`,
8912
+ value: cron.failures,
8913
+ threshold: CRON_FAILING_RUNS,
8914
+ agent: agent.name,
8915
+ role: agent.role,
8916
+ ...cron.scope === SOCIETY_SCOPE ? {} : { project: cron.scope }
8917
+ });
8918
+ }
8919
+ }
8299
8920
  const left = (await this.board.listHomeConflicts(agent.name)).filter((conflict) => now - Date.parse(conflict.since) >= this.timings.homeConflictMs);
8300
8921
  if (left.length > 0) {
8301
8922
  const oldest = Math.max(...left.map((conflict) => now - Date.parse(conflict.since)));
@@ -8463,6 +9084,10 @@ var Scheduler = class {
8463
9084
  channel: dispatch.channel,
8464
9085
  trigger: dispatch.trigger.kind
8465
9086
  }, "dispatching turn");
9087
+ const crons = dispatch.crons ?? [];
9088
+ for (const id of crons) {
9089
+ this.cronsInFlight.add(id);
9090
+ }
8466
9091
  const promise = (async () => {
8467
9092
  try {
8468
9093
  const record = await this.runner.runTurn(dispatch, assignment);
@@ -8477,6 +9102,9 @@ var Scheduler = class {
8477
9102
  } finally {
8478
9103
  this.running.delete(key);
8479
9104
  this.exclusive.delete(key);
9105
+ for (const id of crons) {
9106
+ this.cronsInFlight.delete(id);
9107
+ }
8480
9108
  }
8481
9109
  })();
8482
9110
  this.running.set(key, promise);
@@ -8575,6 +9203,7 @@ var TURN_CONTRACT = [
8575
9203
  "- Work only inside your worktree. In a task's conversation it is a worktree of its own on the task's branch `task/<id>`: commit your work there. After each turn the runner commits what you left on the branch and lets it go for the next holder. Your home worktree is on your own branch, `agent/<name>`, for work tied to no task; nothing lands from it by itself. In a proposal's, a topic's, or a channel's conversation it is a worktree of its own, detached at the project's default branch, which goes when the thread closes or the channel is archived, so work that should last goes on a task's branch. Before each turn the runner fetches the project's remote and brings what holds no work of yours up to the default branch: your own branch when it has no commits of its own, a task's branch nobody has worked on, and a detached worktree with nothing uncommitted; what it leaves where it is, and why, is under Your workspace in the prompt. It never merges or commits for you.",
8576
9204
  "- Posts that would wake this conversation may arrive while you work, delivered into the turn under a heading that says so and formatted like the digest. They are part of this turn: act on them now, after the step in hand, or not at all, as with the digest. The user may also stop a turn; the next turn is told.",
8577
9205
  "- Silence is allowed. If the digest needs no reply, post nothing.",
9206
+ "- To come back to something later, set a cron with create_cron: a note on what to do then, and a five-field cron expression or one time with at. It wakes you in this conversation unless you name another, at most every fifteen minutes, and every fire is a paid turn, so end a cron with remove_cron once it has done its job. Never wait by holding a stage, sleeping, or leaving a process running: a turn's board token ends with the turn, so nothing it leaves behind can act on the board. The society's crons are under society/crons/ in the board projection.",
8578
9207
  "- An @mention wakes the citizen named, and every wake costs a turn. Address someone with @ only when you need them to act; when you merely refer to citizens, write their names plainly. @user wakes nobody, since the user takes no turns; it is what puts a question before the user, so use it only when you need the user to answer or decide.",
8579
9208
  "- A thread is a conversation off a channel that reaches only its participants and whoever is mentioned in it. Every task has one, opened with it under the task's id on the channel of its project it was filed in, general unless it was filed in another: talk about a task there, with post_message and its thread_id, and the notes you give advance_task and update_task are posted there as you. Its participants are the task's creator, its stage holders and assignees, whoever may take the stage that waits, and anyone who posted; it closes when the task ends. Every proposal has one too, in governance, opened with its pitch and closed by its decision. Open other threads with open_thread on a channel with a title, for any other topic; close_thread posts your summary to its channel.",
8580
9209
  "- Route every lesson: about you, your craft, or the user, write it to memory/core.md in your home directory, and keep that file short by moving detail to memory/<topic>.md, your archive; a durable fact about a project's codebase or process goes through `write_knowledge` on that project; something the whole project should know now, post it to the project channel, and anything about one task, in the task's thread.",
@@ -8737,6 +9366,17 @@ function messageLines(messages, threads) {
8737
9366
  }
8738
9367
  return lines;
8739
9368
  }
9369
+ function cronLines(cron, timeZone) {
9370
+ const previous = cron.lastRun === void 0 ? "" : ` The turn of its previous fire ${cron.lastRun.outcome === "stopped" ? "was stopped by the user" : cron.lastRun.outcome} at ${cron.lastRun.at}.`;
9371
+ const next = nextFire(cron, timeZone);
9372
+ const state = isOneTime(cron.schedule) ? " It fired once, as set, and has ended." : cron.ended !== void 0 ? ` ${cron.ended.by} has ended it since it fired (${cron.ended.reason}), so do what it asks only if it still matters.` : next === null ? "" : ` Next it fires at ${next.toISOString()}.`;
9373
+ return [
9374
+ `- "${cron.title}" (cron ${cron.id}), set by ${cron.createdBy} at ${cron.createdAt}, fires ${describeSchedule(cron.schedule, timeZone)}.${previous}${state}`,
9375
+ "",
9376
+ clip(cron.note).split("\n").map((line) => ` ${line}`).join("\n"),
9377
+ ""
9378
+ ];
9379
+ }
8740
9380
  function buildSteerText(messages, threads) {
8741
9381
  return `${[
8742
9382
  `## New in this conversation while you work (${messages.length})`,
@@ -8757,6 +9397,10 @@ function buildTurnPrompt(input) {
8757
9397
  if (dispatch.trigger.taskId !== void 0) {
8758
9398
  lines.push(`Task in question: ${dispatch.trigger.taskId}`);
8759
9399
  }
9400
+ if (input.clock !== void 0) {
9401
+ const { now, timeZone } = input.clock;
9402
+ lines.push(`It is ${now.toISOString()} now, ${localTime(now, timeZone)} in the society's time zone, ${timeZone}.`);
9403
+ }
8760
9404
  const askedIn = dispatch.trigger.channel === void 0 ? null : parseChannelRef(dispatch.trigger.channel).project;
8761
9405
  if (dispatch.project === SOCIETY_SCOPE && askedIn !== null) {
8762
9406
  lines.push(`You were asked in ${dispatch.trigger.channel}, in project ${askedIn}, which you are not a member of, so this turn is outside any project. Answer there with post_message. If the work needs you in ${askedIn}, ask the concierge or @user to add you, or join it with join_project if your charter grants that verb.`);
@@ -8773,6 +9417,9 @@ function buildTurnPrompt(input) {
8773
9417
  if (dispatch.trigger.kind === "proposal_decided") {
8774
9418
  lines.push("A proposal you made was decided; the decision is the last post in the proposal's thread. A decision wakes you in the conversation you filed the proposal from, or at home once that has ended. Carry on with what an approval enables or a rejection asks, and stay silent if nothing follows.");
8775
9419
  }
9420
+ if (dispatch.trigger.kind === "cron") {
9421
+ lines.push("A cron came due: what it asks is under Crons below.");
9422
+ }
8776
9423
  if (dispatch.trigger.kind === "user_post") {
8777
9424
  lines.push("The user posted. Route it: answer where it was posted, in its thread when it came in one, or create what it needs: a task with a plan whose stages name who does them, a thread, or a project, adding citizens to a project first when they are not members. Stay silent when the user already addressed a citizen and nothing else is needed.");
8778
9425
  }
@@ -8799,6 +9446,11 @@ function buildTurnPrompt(input) {
8799
9446
  if (ending !== null) {
8800
9447
  lines.push("", "## This conversation has ended", "", `${capitalized(ending.how)}. You are woken once more because your workspace here still holds work on no branch, listed under Your workspace, and it is removed when this turn ends, whatever it still holds. You may not be the one who ended the conversation, and the work may not be yours to finish, so decide for each piece:`, "- It is yours and it matters: land it through a task. File one, or use one you hold, switch this worktree to its branch task/<id> (git switch -c task/<id> for a task filed in this turn), and commit there.", `- It matters but is someone else's to finish: put it on a task's branch the same way, then hand that task to whoever should own it and ask them in ${ending.askIn} with a mention, since this conversation takes no more posts.`, "- It is disposable: leave it, and it goes with the workspace.");
8801
9448
  }
9449
+ const crons = input.crons ?? [];
9450
+ if (crons.length > 0) {
9451
+ const timeZone = input.clock?.timeZone ?? DEFAULT_TIME_ZONE;
9452
+ lines.push("", "## Crons", "", "Each of these came due for this turn; its note says what to do. Change one with update_cron, or end it with remove_cron, by its id.", "", ...crons.flatMap((cron) => cronLines(cron, timeZone)));
9453
+ }
8802
9454
  lines.push("", WORKSPACE_TOKEN);
8803
9455
  const conflicts = input.conflicts ?? [];
8804
9456
  if (dispatch.trigger.kind === "reflection") {
@@ -8981,7 +9633,8 @@ var TurnHost = class {
8981
9633
  usage: null,
8982
9634
  costUsd: 0,
8983
9635
  toolCalls: 0,
8984
- model: agent.model ?? null
9636
+ model: agent.model ?? null,
9637
+ ...dispatch.crons === void 0 ? {} : { crons: dispatch.crons }
8985
9638
  };
8986
9639
  if (agent.status === "retired") {
8987
9640
  return { failed: await this.fail(base, `${agent.name} is retired`) };
@@ -9068,8 +9721,18 @@ var TurnHost = class {
9068
9721
  ...threadRecord.state === "closed" ? { replyIn: await this.replyIn(threadRecord) } : {}
9069
9722
  };
9070
9723
  const ending = dispatch.trigger.kind === "closing" ? await this.endingOf(dispatch.project, threadRecord, task, channel) : null;
9724
+ const society = await this.board.society();
9725
+ const crons = [];
9726
+ for (const id of dispatch.crons ?? []) {
9727
+ const cron = await this.board.readCron(id).catch(() => null);
9728
+ if (cron !== null) {
9729
+ crons.push(cron);
9730
+ }
9731
+ }
9071
9732
  const prompt = buildTurnPrompt({
9072
9733
  dispatch,
9734
+ clock: { now: this.now(), timeZone: society.timezone },
9735
+ crons,
9073
9736
  messages,
9074
9737
  threads,
9075
9738
  conversation,
@@ -9428,7 +10091,8 @@ var TurnHost = class {
9428
10091
  usage: null,
9429
10092
  costUsd: 0,
9430
10093
  toolCalls: 0,
9431
- model: agent?.model ?? null
10094
+ model: agent?.model ?? null,
10095
+ ...dispatch.crons === void 0 ? {} : { crons: dispatch.crons }
9432
10096
  }, reason);
9433
10097
  }
9434
10098
  /**
@@ -10334,20 +10998,20 @@ async function handleMcpRequest(board2, actor, request, version) {
10334
10998
  import { Hono as Hono3 } from "hono";
10335
10999
  import { compress } from "hono/compress";
10336
11000
  import { streamSSE as streamSSE2 } from "hono/streaming";
10337
- import { z as z15, ZodError } from "zod";
11001
+ import { z as z16, ZodError } from "zod";
10338
11002
 
10339
11003
  // apps/server/dist/github-auth.js
10340
11004
  import { randomBytes as randomBytes2 } from "node:crypto";
10341
11005
  import { Hono } from "hono";
10342
- import { z as z14 } from "zod";
11006
+ import { z as z15 } from "zod";
10343
11007
  var SIGN_IN_TTL_MS = 30 * 24 * 36e5;
10344
11008
  var STATE_TTL_MS = 10 * 6e4;
10345
11009
  var MAX_STATES = 1e3;
10346
- var TokenAnswerSchema = z14.object({ access_token: z14.string().min(1) });
10347
- var GithubUserSchema = z14.object({
10348
- login: z14.string().min(1),
10349
- id: z14.number().int(),
10350
- avatar_url: z14.string().optional()
11010
+ var TokenAnswerSchema = z15.object({ access_token: z15.string().min(1) });
11011
+ var GithubUserSchema = z15.object({
11012
+ login: z15.string().min(1),
11013
+ id: z15.number().int(),
11014
+ avatar_url: z15.string().optional()
10351
11015
  });
10352
11016
  function githubAvatar(url) {
10353
11017
  try {
@@ -10821,17 +11485,18 @@ var ERROR_STATUS = {
10821
11485
  INVALID_TRANSITION: 409,
10822
11486
  INVALID_STATE: 409
10823
11487
  };
10824
- var RetireBodySchema = z15.object({ reason: z15.string().min(1) });
10825
- var RunnerBodySchema = z15.object({ name: NameSchema });
10826
- var EnrollmentApprovalSchema = z15.object({ name: NameSchema });
10827
- var HomeRunnerBodySchema = z15.object({ runner: NameSchema.nullable() });
10828
- var ModelBodySchema = z15.object({ model: ModelNameSchema.nullable() });
10829
- var EffortBodySchema = z15.object({ effort: EffortSchema.nullable() });
10830
- var HistoryLimitSchema = z15.coerce.number().int().min(1).max(500);
10831
- var ChannelBodySchema = z15.object({
11488
+ var RetireBodySchema = z16.object({ reason: z16.string().min(1) });
11489
+ var RunnerBodySchema = z16.object({ name: NameSchema });
11490
+ var EnrollmentApprovalSchema = z16.object({ name: NameSchema });
11491
+ var HomeRunnerBodySchema = z16.object({ runner: NameSchema.nullable() });
11492
+ var ModelBodySchema = z16.object({ model: ModelNameSchema.nullable() });
11493
+ var EffortBodySchema = z16.object({ effort: EffortSchema.nullable() });
11494
+ var HistoryLimitSchema = z16.coerce.number().int().min(1).max(500);
11495
+ var TimezoneBodySchema = z16.object({ timezone: TimeZoneSchema });
11496
+ var ChannelBodySchema = z16.object({
10832
11497
  project: NameSchema.nullable().default(null),
10833
11498
  name: NameSchema,
10834
- purpose: z15.string().min(1)
11499
+ purpose: z16.string().min(1)
10835
11500
  });
10836
11501
  function bearer2(c) {
10837
11502
  const header = c.req.header("authorization") ?? "";
@@ -10880,6 +11545,10 @@ function createApp(deps) {
10880
11545
  });
10881
11546
  api.delete("/sign-in", async (c) => c.json({ signedOut: await board2.signOut(bearer2(c) ?? "") }));
10882
11547
  api.get("/society", async (c) => c.json(await board2.society()));
11548
+ api.put("/society/timezone", async (c) => {
11549
+ const body = TimezoneBodySchema.parse(await c.req.json());
11550
+ return c.json(await board2.setTimezone(c.get("actor"), body.timezone));
11551
+ });
10883
11552
  api.get("/version", (c) => c.json({ version }));
10884
11553
  api.get("/agents", async (c) => c.json((await board2.listAgents()).map(({ tokenHash: _hash, ...agent }) => agent)));
10885
11554
  api.get("/members", async (c) => c.json(await board2.listMembers()));
@@ -10931,6 +11600,7 @@ function createApp(deps) {
10931
11600
  });
10932
11601
  }
10933
11602
  api.get("/proposals", async (c) => c.json(await board2.listProposals()));
11603
+ api.get("/crons", async (c) => c.json(await board2.listCrons()));
10934
11604
  api.get("/proposals/:id", async (c) => c.json(await board2.readProposal(c.req.param("id"))));
10935
11605
  api.get("/signals", async (c) => c.json(await board2.listSignals(Number(c.req.query("limit") ?? "100"))));
10936
11606
  api.get("/metrics", async (c) => {
@@ -10980,7 +11650,7 @@ function createApp(deps) {
10980
11650
  });
10981
11651
  api.put("/roles/:name", async (c) => {
10982
11652
  const charter = RoleCharterSchema.parse({
10983
- ...z15.record(z15.string(), z15.unknown()).parse(await c.req.json()),
11653
+ ...z16.record(z16.string(), z16.unknown()).parse(await c.req.json()),
10984
11654
  name: c.req.param("name")
10985
11655
  });
10986
11656
  return c.json(await board2.setRoleCharter(c.get("actor"), charter));