@kubitnodes/stellaris 0.3.12 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/cli/index.js +1149 -535
  2. package/dist/server/index.js +1426 -593
  3. package/package.json +2 -1
  4. package/web/dist/assets/{BackLink-BlJvN2RA.js → BackLink-giAdtos8.js} +1 -1
  5. package/web/dist/assets/{ChannelForms-CmFxBXNK.js → ChannelForms-b6eKogvP.js} +1 -1
  6. package/web/dist/assets/ChannelView-DXoJ5w3z.js +1 -0
  7. package/web/dist/assets/CitizenView-yOnQtNz4.js +2 -0
  8. package/web/dist/assets/{CitizensView-B8lVGSBD.js → CitizensView-Dbo47F82.js} +1 -1
  9. package/web/dist/assets/{CliIcon-CT5ljQDC.js → CliIcon-DxDvRRcX.js} +1 -1
  10. package/web/dist/assets/CronsView-DYx7dQcr.js +1 -0
  11. package/web/dist/assets/KnowledgeView-BXLj85OH.js +1 -0
  12. package/web/dist/assets/{Markdown-B5zEdYuO.js → Markdown-Dw04_ImA.js} +4 -4
  13. package/web/dist/assets/{MessageItem-DuxSZwn0.js → MessageItem-DnfMDxDB.js} +1 -1
  14. package/web/dist/assets/{MetricsView-D8D-y-sz.js → MetricsView-_h2TWeo8.js} +1 -1
  15. package/web/dist/assets/{NeedsYouView-Buwq8-_l.js → NeedsYouView-BI1-_bYI.js} +1 -1
  16. package/web/dist/assets/{Overview-CrQ8ybpq.js → Overview-BdZiHglw.js} +1 -1
  17. package/web/dist/assets/{Pane-CFoSKOo5.js → Pane-CODhA5zE.js} +1 -1
  18. package/web/dist/assets/ProjectView-CScK4F7F.js +1 -0
  19. package/web/dist/assets/ProposalView-BQ1rflqo.js +1 -0
  20. package/web/dist/assets/{ProposalsView-DjyZpZe0.js → ProposalsView-DSi2u6qO.js} +1 -1
  21. package/web/dist/assets/{RunnersView-2h5NWUuD.js → RunnersView-9E2qUSO5.js} +1 -1
  22. package/web/dist/assets/{SocietyView-BuaSb_3d.js → SocietyView-DEeUo4wb.js} +1 -1
  23. package/web/dist/assets/SubjectThread-DhGB5RPc.js +1 -0
  24. package/web/dist/assets/{TaskFileView-CeEzbSCL.js → TaskFileView-DQC_ZUdY.js} +2 -2
  25. package/web/dist/assets/TaskView-yVpUG83f.js +3 -0
  26. package/web/dist/assets/{TasksView-DCVU5oZn.js → TasksView-CJbJt5dM.js} +1 -1
  27. package/web/dist/assets/ThreadCard-DVAx3gGc.js +1 -0
  28. package/web/dist/assets/{ThreadForms-CTBaAJ57.js → ThreadForms-RnrV7V5X.js} +1 -1
  29. package/web/dist/assets/{ThreadView-zg8KqF3t.js → ThreadView-CMwiVZFq.js} +1 -1
  30. package/web/dist/assets/{citizen-CImM6WB6.js → citizen-aLkZ4owL.js} +1 -1
  31. package/web/dist/assets/{compose-BG_-Tp87.js → compose-D7DOz6kC.js} +1 -1
  32. package/web/dist/assets/format-BORdp4w-.js +55 -0
  33. package/web/dist/assets/{governance-cDTg2_iq.js → governance-WrDQawLN.js} +1 -1
  34. package/web/dist/assets/index-7uInG5D9.js +23 -0
  35. package/web/dist/assets/index-D02Wgo_C.css +2 -0
  36. package/web/dist/assets/{link-CFdt17Sp.js → link-1v5lugnf.js} +1 -1
  37. package/web/dist/assets/{tasks-C10zwkED.js → tasks-C-9dAYQV.js} +1 -1
  38. package/web/dist/assets/useMatch-CyeFfbcR.js +1 -0
  39. package/web/dist/assets/{useStickyScroll-BjqdEt_f.js → useStickyScroll-CE7dyz0Q.js} +1 -1
  40. package/web/dist/index.html +9 -9
  41. package/web/dist/assets/ChannelView-4s--IwKC.js +0 -1
  42. package/web/dist/assets/CitizenView-B5-LVVCG.js +0 -2
  43. package/web/dist/assets/KnowledgeView-DbI8DIZb.js +0 -1
  44. package/web/dist/assets/ProjectView-BBQg-ZxC.js +0 -1
  45. package/web/dist/assets/ProposalView-BjPHVxj4.js +0 -1
  46. package/web/dist/assets/SubjectThread-C1mHvHMI.js +0 -1
  47. package/web/dist/assets/TaskView-yL9hCM7G.js +0 -3
  48. package/web/dist/assets/ThreadCard-CXXf9qfA.js +0 -1
  49. package/web/dist/assets/format-BWr3zKWg.js +0 -55
  50. package/web/dist/assets/index-Cxqhrg2v.js +0 -23
  51. package/web/dist/assets/index-VVnGdAXc.css +0 -2
  52. package/web/dist/assets/useMatch-BpnmzT6s.js +0 -1
@@ -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,60 +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
- linkedAt: IsoDateTimeSchema
363
+ linkedAt: IsoDateTimeSchema,
364
+ /**
365
+ * Once its `ghpr` task was abandoned, how the board's closing of it on GitHub went: `ok` false
366
+ * leaves it open, for the reason `detail` gives.
367
+ */
368
+ closed: z4.object({ at: IsoDateTimeSchema, ok: z4.boolean(), detail: z4.string() }).optional()
222
369
  });
223
- var DefaultBranchSchema = z3.string().trim().min(1).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/, "a branch name is letters, digits, and . _ / -").refine((branch) => !branch.includes("..") && !branch.includes("//") && !branch.endsWith("/") && !branch.endsWith(".") && !branch.endsWith(".lock"), "not a branch name git takes").refine((branch) => !branch.startsWith("task/") && !branch.startsWith("agent/"), "task/ and agent/ hold the board's own branches");
224
- var StageIdSchema = z3.string().regex(/^s[1-9][0-9]*$/, "expected a stage id such as s1");
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");
225
372
  var stageFields = {
226
- name: z3.string().trim().min(1).max(120),
373
+ name: z4.string().trim().min(1).max(120),
227
374
  role: NameSchema.optional(),
228
375
  agent: NameSchema.optional(),
229
- gate: z3.boolean().default(false)
376
+ gate: z4.boolean().default(false)
230
377
  };
231
378
  function oneAssignee(stage) {
232
379
  return stage.role === void 0 || stage.agent === void 0;
233
380
  }
234
381
  var ONE_ASSIGNEE = { message: "a stage names a role or an agent, not both" };
235
- var PlanStageSchema = z3.object(stageFields).refine(oneAssignee, ONE_ASSIGNEE);
236
- var PlanEditStageSchema = z3.object({ id: StageIdSchema.optional(), ...stageFields }).refine(oneAssignee, ONE_ASSIGNEE);
237
- var PlanSchema = z3.array(PlanStageSchema);
238
- var PlanEditSchema = z3.array(PlanEditStageSchema);
239
- var ProjectSchema = z3.object({
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({
240
387
  slug: NameSchema,
241
- name: z3.string().min(1),
242
- repo: z3.string().nullable(),
243
- defaultBranch: z3.string().min(1),
388
+ name: z4.string().min(1),
389
+ repo: z4.string().nullable(),
390
+ defaultBranch: z4.string().min(1),
244
391
  /** The channels that take posts; an archived one moves to `archivedChannels`. */
245
- channels: z3.array(NameSchema),
246
- archivedChannels: z3.array(ArchivedChannelSchema).default([]),
247
- members: z3.array(NameSchema),
248
- approvers: z3.array(NameSchema),
249
- requiredCapabilities: z3.array(z3.string()),
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()),
250
397
  createdAt: IsoDateTimeSchema,
251
398
  onDone: CompletionEffectSchema.default("none"),
252
399
  /** The runner the project lives on, which holds its repository; set on the project's first turn. */
253
400
  runner: NameSchema.optional(),
254
401
  /** Set once the project is archived: nobody belongs to it, and nothing more is posted, filed, or joined there. */
255
- archived: z3.object({ at: IsoDateTimeSchema, by: NameSchema, reason: z3.string().min(1) }).optional()
402
+ archived: z4.object({ at: IsoDateTimeSchema, by: NameSchema, reason: z4.string().min(1) }).optional()
256
403
  });
257
- var TaskStepSchema = z3.object({
258
- action: z3.enum(["advanced", "returned", "abandoned", "landed", "reopened"]),
404
+ var TaskStepSchema = z4.object({
405
+ action: z4.enum(["advanced", "returned", "abandoned", "landed", "reopened"]),
259
406
  stage: StageIdSchema,
260
407
  to: StageIdSchema.nullable()
261
408
  });
262
- var ProposalStepSchema = z3.object({ action: z3.enum(["approved", "rejected"]) });
263
- var MessageStepSchema = z3.union([TaskStepSchema, ProposalStepSchema]);
409
+ var ProposalStepSchema = z4.object({ action: z4.enum(["approved", "rejected"]) });
410
+ var MessageStepSchema = z4.union([TaskStepSchema, ProposalStepSchema]);
264
411
  function isTaskStep(step) {
265
412
  return "stage" in step;
266
413
  }
267
- var MessageFrontmatterSchema = z3.object({
414
+ var MessageFrontmatterSchema = z4.object({
268
415
  id: UlidSchema,
269
416
  author: NameSchema,
270
417
  channel: ChannelRefSchema,
@@ -276,18 +423,18 @@ var MessageFrontmatterSchema = z3.object({
276
423
  /** On a post a task verb made in the task's thread, or a decision in a proposal's: the step it recorded. */
277
424
  step: MessageStepSchema.optional(),
278
425
  ts: IsoDateTimeSchema,
279
- mentions: z3.array(NameSchema)
426
+ mentions: z4.array(NameSchema)
280
427
  });
281
- var TaskStatusSchema = z3.enum(["open", "claimed", "done", "abandoned"]);
282
- var ThreadStateSchema = z3.enum(["open", "closed"]);
283
- var ThreadSubjectSchema = z3.object({
284
- kind: z3.enum(["task", "proposal"]),
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"]),
285
432
  id: UlidSchema
286
433
  });
287
- var ThreadFrontmatterSchema = z3.object({
434
+ var ThreadFrontmatterSchema = z4.object({
288
435
  id: UlidSchema,
289
436
  channel: ChannelRefSchema,
290
- title: z3.string().min(1).max(200),
437
+ title: z4.string().min(1).max(200),
291
438
  subject: ThreadSubjectSchema.optional(),
292
439
  state: ThreadStateSchema,
293
440
  openedBy: NameSchema,
@@ -295,28 +442,28 @@ var ThreadFrontmatterSchema = z3.object({
295
442
  closedBy: NameSchema.optional(),
296
443
  closedAt: IsoDateTimeSchema.optional()
297
444
  });
298
- var StageSchema = z3.object({
445
+ var StageSchema = z4.object({
299
446
  id: StageIdSchema,
300
- name: z3.string().min(1),
447
+ name: z4.string().min(1),
301
448
  role: NameSchema.optional(),
302
449
  agent: NameSchema.optional(),
303
- gate: z3.boolean().default(false),
450
+ gate: z4.boolean().default(false),
304
451
  /** Everyone who has held the stage, oldest first. */
305
- holders: z3.array(NameSchema).default([]),
452
+ holders: z4.array(NameSchema).default([]),
306
453
  completedBy: NameSchema.optional(),
307
454
  completedAt: IsoDateTimeSchema.optional()
308
455
  });
309
- var TaskReturnSchema = z3.object({
456
+ var TaskReturnSchema = z4.object({
310
457
  from: StageIdSchema,
311
458
  by: NameSchema,
312
459
  at: IsoDateTimeSchema
313
460
  });
314
- var TaskFrontmatterSchema = z3.object({
461
+ var TaskFrontmatterSchema = z4.object({
315
462
  id: UlidSchema,
316
463
  project: NameSchema,
317
464
  /** The channel of the project the task is filed in, where its thread hangs. */
318
465
  channel: NameSchema.default("general"),
319
- title: z3.string().min(1),
466
+ title: z4.string().min(1),
320
467
  status: TaskStatusSchema,
321
468
  createdBy: NameSchema,
322
469
  createdAt: IsoDateTimeSchema,
@@ -325,19 +472,19 @@ var TaskFrontmatterSchema = z3.object({
325
472
  claimedBy: NameSchema.optional(),
326
473
  leaseExpiresAt: IsoDateTimeSchema.optional(),
327
474
  parentId: UlidSchema.optional(),
328
- blockedBy: z3.array(UlidSchema),
329
- requiredCapabilities: z3.array(z3.string()),
330
- stages: z3.array(StageSchema).min(1),
475
+ blockedBy: z4.array(UlidSchema),
476
+ requiredCapabilities: z4.array(z4.string()),
477
+ stages: z4.array(StageSchema).min(1),
331
478
  stage: StageIdSchema,
332
479
  /** When the current stage last became current or lost its holder: the waiting clock. */
333
480
  stageSince: IsoDateTimeSchema,
334
481
  /** The highest stage number issued, so a removed stage's id is never reused. */
335
- stageSeq: z3.number().int().positive(),
482
+ stageSeq: z4.number().int().positive(),
336
483
  onDone: CompletionEffectSchema.default("none"),
337
484
  /** The pull request a `ghpr` task lands through. */
338
485
  pullRequest: PullRequestLinkSchema.optional(),
339
486
  /** The last stage is complete and the completion effect is running. */
340
- completing: z3.boolean().default(false),
487
+ completing: z4.boolean().default(false),
341
488
  /** The latest send-back while its rework lasts: cleared once the task reaches that stage again. */
342
489
  returned: TaskReturnSchema.optional()
343
490
  });
@@ -361,79 +508,79 @@ function mayHoldStage(member, task) {
361
508
  }
362
509
  return !(stage.gate && task.stages.slice(0, index).some((s) => s.holders.includes(member.name)));
363
510
  }
364
- var ModelNameSchema = z3.string().trim().min(1).max(120).regex(/^[\w.:/[\]-]+$/, "a model name is one word of letters, digits, and . _ : / - [ ]");
365
- var EffortSchema = z3.string().trim().min(1).max(32).regex(/^[a-z][a-z0-9_-]*$/, "an effort is one lowercase word, such as high");
366
- var EffortOptionSchema = z3.object({
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({
367
514
  id: EffortSchema,
368
- description: z3.string().default("")
515
+ description: z4.string().default("")
369
516
  });
370
- var ModelOptionSchema = z3.object({
517
+ var ModelOptionSchema = z4.object({
371
518
  id: ModelNameSchema,
372
- name: z3.string(),
373
- description: z3.string().default(""),
374
- isDefault: z3.boolean().default(false),
375
- 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([]),
376
523
  defaultEffort: EffortSchema.optional()
377
524
  });
378
- var RunnerModelsSchema = z3.object({
525
+ var RunnerModelsSchema = z4.object({
379
526
  runner: NameSchema,
380
527
  cli: CliKindSchema,
381
- models: z3.array(ModelOptionSchema)
528
+ models: z4.array(ModelOptionSchema)
382
529
  });
383
- var AgentStatusSchema = z3.enum(["active", "retired"]);
384
- var AgentSchema = z3.object({
530
+ var AgentStatusSchema = z4.enum(["active", "retired"]);
531
+ var AgentSchema = z4.object({
385
532
  name: NameSchema,
386
533
  role: NameSchema,
387
534
  cli: CliKindSchema.nullable(),
388
- model: z3.string().optional(),
535
+ model: z4.string().optional(),
389
536
  /** The reasoning effort set for the agent's turns; the model's own default applies otherwise. */
390
- effort: z3.string().optional(),
537
+ effort: z4.string().optional(),
391
538
  /** The runner preferred for the agent's turns where nothing else decides; any runner with its CLI otherwise. */
392
539
  homeRunner: NameSchema.optional(),
393
- memberships: z3.array(NameSchema),
394
- subscriptions: z3.array(ChannelRefSchema),
540
+ memberships: z4.array(NameSchema),
541
+ subscriptions: z4.array(ChannelRefSchema),
395
542
  status: AgentStatusSchema,
396
543
  createdAt: IsoDateTimeSchema,
397
- tokenHash: z3.string().min(1),
544
+ tokenHash: z4.string().min(1),
398
545
  retiredAt: IsoDateTimeSchema.optional(),
399
- retiredReason: z3.string().optional()
546
+ retiredReason: z4.string().optional()
400
547
  });
401
- var MemberSchema = z3.object({
548
+ var MemberSchema = z4.object({
402
549
  name: NameSchema,
403
550
  role: NameSchema,
404
551
  cli: CliKindSchema.nullable(),
405
552
  homeRunner: NameSchema.optional(),
406
553
  status: AgentStatusSchema,
407
- resident: z3.boolean().default(false),
554
+ resident: z4.boolean().default(false),
408
555
  /** The model configured on the record, if any; the CLI's own default applies otherwise. */
409
- model: z3.string().optional(),
556
+ model: z4.string().optional(),
410
557
  /** The model the CLI reported on the citizen's last turn. */
411
- lastModel: z3.string().optional(),
558
+ lastModel: z4.string().optional(),
412
559
  /** The reasoning effort configured on the record, if any; the model's own default applies otherwise. */
413
- effort: z3.string().optional(),
560
+ effort: z4.string().optional(),
414
561
  /** The citizen's own skills, by name. */
415
- skills: z3.array(NameSchema).default([]),
416
- memberships: z3.array(NameSchema),
417
- subscriptions: z3.array(ChannelRefSchema),
418
- claimsHeld: z3.number().int().nonnegative().default(0),
419
- tasksDone: z3.number().int().nonnegative().default(0),
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),
420
567
  lastTurnAt: IsoDateTimeSchema.optional(),
421
- lastTurnOutcome: z3.string().optional(),
568
+ lastTurnOutcome: z4.string().optional(),
422
569
  createdAt: IsoDateTimeSchema,
423
570
  retiredAt: IsoDateTimeSchema.optional()
424
571
  });
425
- var RunnerOsSchema = z3.enum(["linux", "windows", "darwin"]);
426
- var RunnerSchema = z3.object({
572
+ var RunnerOsSchema = z4.enum(["linux", "windows", "darwin"]);
573
+ var RunnerSchema = z4.object({
427
574
  name: NameSchema,
428
575
  os: RunnerOsSchema,
429
- clis: z3.array(CliKindSchema),
430
- capabilities: z3.array(z3.string()),
576
+ clis: z4.array(CliKindSchema),
577
+ capabilities: z4.array(z4.string()),
431
578
  /** The release the runner reported when it last registered; absent until it has. */
432
- version: z3.string().optional(),
433
- status: z3.enum(["connected", "disconnected"]),
579
+ version: z4.string().optional(),
580
+ status: z4.enum(["connected", "disconnected"]),
434
581
  lastSeen: IsoDateTimeSchema.optional()
435
582
  });
436
- var ProposalKindSchema = z3.enum([
583
+ var ProposalKindSchema = z4.enum([
437
584
  "role",
438
585
  "member",
439
586
  "channel",
@@ -442,43 +589,43 @@ var ProposalKindSchema = z3.enum([
442
589
  "skill",
443
590
  "archive"
444
591
  ]);
445
- var ProposalStatusSchema = z3.enum([
592
+ var ProposalStatusSchema = z4.enum([
446
593
  "proposed",
447
594
  "approved",
448
595
  "rejected",
449
596
  "provisioned",
450
597
  "retired"
451
598
  ]);
452
- var MemberProposalSchema = z3.object({
599
+ var MemberProposalSchema = z4.object({
453
600
  name: NameSchema,
454
601
  role: NameSchema,
455
602
  cli: CliKindSchema,
456
- model: z3.string().optional(),
603
+ model: z4.string().optional(),
457
604
  homeRunner: NameSchema.optional(),
458
- memberships: z3.array(NameSchema).default([]),
459
- subscriptions: z3.array(ChannelRefSchema).default([]),
460
- seedInstructions: z3.string().optional()
605
+ memberships: z4.array(NameSchema).default([]),
606
+ subscriptions: z4.array(ChannelRefSchema).default([]),
607
+ seedInstructions: z4.string().optional()
461
608
  });
462
- var ChannelProposalSchema = z3.object({
609
+ var ChannelProposalSchema = z4.object({
463
610
  project: NameSchema.nullable().default(null),
464
611
  name: NameSchema,
465
- purpose: z3.string().min(1)
612
+ purpose: z4.string().min(1)
466
613
  });
467
- var ReallocationProposalSchema = z3.object({
468
- description: z3.string().min(1)
614
+ var ReallocationProposalSchema = z4.object({
615
+ description: z4.string().min(1)
469
616
  });
470
- var RetirementProposalSchema = z3.object({
617
+ var RetirementProposalSchema = z4.object({
471
618
  agent: NameSchema,
472
- reason: z3.string().min(1)
619
+ reason: z4.string().min(1)
473
620
  });
474
- var ArchiveProposalSchema = z3.object({
621
+ var ArchiveProposalSchema = z4.object({
475
622
  project: NameSchema,
476
- reason: z3.string().min(1)
623
+ reason: z4.string().min(1)
477
624
  });
478
- var SkillProposalSchema = z3.object({
625
+ var SkillProposalSchema = z4.object({
479
626
  name: NameSchema,
480
- summary: z3.string().min(1).max(200),
481
- body: z3.string().min(1)
627
+ summary: z4.string().min(1).max(200),
628
+ body: z4.string().min(1)
482
629
  });
483
630
  var ProposalCharterSchemas = {
484
631
  role: RoleCharterSchema,
@@ -550,17 +697,17 @@ function describeCharter(kind, charter) {
550
697
  }
551
698
  return `${kind} ${JSON.stringify(charter)}`;
552
699
  }
553
- var SkillSchema = z3.object({
700
+ var SkillSchema = z4.object({
554
701
  name: NameSchema,
555
- summary: z3.string().default(""),
556
- scope: z3.enum(["own", "society"]),
557
- path: z3.string().min(1)
702
+ summary: z4.string().default(""),
703
+ scope: z4.enum(["own", "society"]),
704
+ path: z4.string().min(1)
558
705
  });
559
- var HomeConflictSchema = z3.object({
706
+ var HomeConflictSchema = z4.object({
560
707
  /** The copy, relative to the home. */
561
- path: z3.string().min(1),
708
+ path: z4.string().min(1),
562
709
  /** The file beside it, whose version the board kept. */
563
- file: z3.string().min(1),
710
+ file: z4.string().min(1),
564
711
  /** When the copy reached the board. */
565
712
  since: IsoDateTimeSchema
566
713
  });
@@ -568,196 +715,238 @@ var CONFLICT_COPY = /^(.+)\.conflict-[^/.]+$/;
568
715
  function conflictCopyOf(copy) {
569
716
  return CONFLICT_COPY.exec(copy)?.[1] ?? null;
570
717
  }
571
- var CommitIdSchema = z3.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/);
572
- var HomeFileChangeSchema = z3.object({
573
- path: z3.string().min(1),
574
- status: z3.enum(["added", "modified", "deleted"]),
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"]),
575
722
  /** Lines added and removed, or null for a binary file. */
576
- added: z3.number().int().nonnegative().nullable(),
577
- removed: z3.number().int().nonnegative().nullable()
723
+ added: z4.number().int().nonnegative().nullable(),
724
+ removed: z4.number().int().nonnegative().nullable()
578
725
  });
579
- var HomeChangeSchema = z3.object({
726
+ var HomeChangeSchema = z4.object({
580
727
  commit: CommitIdSchema,
581
728
  at: IsoDateTimeSchema,
582
- kind: z3.enum(["turn", "board", "merge"]),
583
- author: z3.string(),
729
+ kind: z4.enum(["turn", "board", "merge"]),
730
+ author: z4.string(),
584
731
  /** The turn that made it, from the runner's commit message. */
585
732
  turnId: UlidSchema.optional(),
586
- subject: z3.string(),
587
- files: z3.array(HomeFileChangeSchema)
733
+ subject: z4.string(),
734
+ files: z4.array(HomeFileChangeSchema)
588
735
  });
589
- var HomeHistorySchema = z3.object({
590
- changes: z3.array(HomeChangeSchema),
736
+ var HomeHistorySchema = z4.object({
737
+ changes: z4.array(HomeChangeSchema),
591
738
  /** Whether older commits remain past the ones read. */
592
- more: z3.boolean()
739
+ more: z4.boolean()
593
740
  });
594
741
  var HomeFileDiffSchema = HomeFileChangeSchema.pick({ path: true, status: true }).extend({
595
- patch: z3.string().nullable(),
742
+ patch: z4.string().nullable(),
596
743
  /** The patch was cut at its size limit. */
597
- truncated: z3.boolean()
744
+ truncated: z4.boolean()
598
745
  });
599
- var KnowledgeSchema = z3.object({
746
+ var KnowledgeSchema = z4.object({
600
747
  topic: NameSchema,
601
748
  project: NameSchema.nullable(),
602
749
  updatedBy: NameSchema,
603
750
  updatedAt: IsoDateTimeSchema
604
751
  });
605
- var RemovedKnowledgeSchema = z3.object({
752
+ var RemovedKnowledgeSchema = z4.object({
606
753
  topic: NameSchema,
607
754
  project: NameSchema.nullable(),
608
755
  removedBy: NameSchema,
609
756
  removedAt: IsoDateTimeSchema
610
757
  });
611
- var DashboardSchema = z3.object({
758
+ var DashboardSchema = z4.object({
612
759
  project: NameSchema,
613
- revision: z3.number().int().min(0),
760
+ revision: z4.number().int().min(0),
614
761
  /** Absent until someone updates the dashboard the project began with. */
615
762
  updatedBy: NameSchema.optional(),
616
763
  updatedAt: IsoDateTimeSchema.optional(),
617
- body: z3.string()
764
+ body: z4.string()
765
+ });
766
+ var ProposalOriginSchema = z4.object({
767
+ scope: NameSchema,
768
+ thread: UlidSchema.optional(),
769
+ channel: NameSchema.optional()
618
770
  });
619
- var ProposalFrontmatterSchema = z3.object({
771
+ var ProposalFrontmatterSchema = z4.object({
620
772
  id: UlidSchema,
621
773
  kind: ProposalKindSchema,
622
774
  proposedBy: NameSchema,
623
775
  status: ProposalStatusSchema,
624
776
  createdAt: IsoDateTimeSchema,
777
+ /** Absent for a proposal filed outside a turn, or before proposals recorded it. */
778
+ filedFrom: ProposalOriginSchema.optional(),
625
779
  decidedBy: NameSchema.optional(),
626
780
  decidedAt: IsoDateTimeSchema.optional(),
627
- reason: z3.string().optional(),
628
- charter: z3.record(z3.string(), z3.unknown()),
781
+ reason: z4.string().optional(),
782
+ charter: z4.record(z4.string(), z4.unknown()),
629
783
  /** What approval created, for kinds the board provisions: the agent, channel, role, or retirement. */
630
784
  provisionedAt: IsoDateTimeSchema.optional(),
631
- provision: z3.record(z3.string(), z3.unknown()).optional()
785
+ provision: z4.record(z4.string(), z4.unknown()).optional()
632
786
  });
633
- var DecisionSchema = z3.object({
787
+ var DecisionSchema = z4.object({
634
788
  id: UlidSchema,
635
789
  proposalId: UlidSchema,
636
790
  decidedBy: NameSchema,
637
- outcome: z3.enum(["approved", "rejected"]),
638
- reason: z3.string().optional(),
791
+ outcome: z4.enum(["approved", "rejected"]),
792
+ reason: z4.string().optional(),
639
793
  ts: IsoDateTimeSchema
640
794
  });
641
795
 
642
796
  // packages/shared/dist/verbs.js
643
- 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);
644
801
  var VerbInputs = {
645
- post_message: z4.object({
802
+ post_message: z5.object({
646
803
  /** Required outside a thread; in one it may be left out, and must otherwise be the thread's. */
647
804
  channel: ChannelRefSchema.optional(),
648
- body: z4.string().min(1),
805
+ body: z5.string().min(1),
649
806
  thread_id: UlidSchema.optional()
650
807
  }),
651
- read_inbox: z4.object({
808
+ read_inbox: z5.object({
652
809
  since_cursor: UlidSchema.nullable().optional(),
653
- limit: z4.number().int().positive().max(200).default(50),
654
- advance: z4.boolean().default(true)
810
+ limit: z5.number().int().positive().max(200).default(50),
811
+ advance: z5.boolean().default(true)
655
812
  }),
656
- search: z4.object({
657
- query: z4.string().min(1),
813
+ search: z5.object({
814
+ query: z5.string().min(1),
658
815
  project: NameSchema.optional(),
659
816
  channel: ChannelRefSchema.optional(),
660
- limit: z4.number().int().positive().max(100).default(20)
817
+ limit: z5.number().int().positive().max(100).default(20)
661
818
  }),
662
819
  /** On a task or a proposal, whose id the thread takes, or on a channel with a title. */
663
- open_thread: z4.object({
820
+ open_thread: z5.object({
664
821
  task_id: UlidSchema.optional(),
665
822
  proposal_id: UlidSchema.optional(),
666
823
  channel: ChannelRefSchema.optional(),
667
- title: z4.string().min(1).max(200).optional()
824
+ title: z5.string().min(1).max(200).optional()
668
825
  }),
669
- close_thread: z4.object({ thread_id: UlidSchema, summary: z4.string().min(1) }),
670
- create_task: z4.object({
826
+ close_thread: z5.object({ thread_id: UlidSchema, summary: z5.string().min(1) }),
827
+ create_task: z5.object({
671
828
  project: NameSchema,
672
- title: z4.string().min(1).max(200),
673
- body: z4.string().default(""),
829
+ title: z5.string().min(1).max(200),
830
+ body: z5.string().default(""),
674
831
  parent_id: UlidSchema.optional(),
675
- required_capabilities: z4.array(z4.string()).default([]),
832
+ required_capabilities: z5.array(z5.string()).default([]),
676
833
  /** The plan; without it the task gets one stage, work, that anyone in the project may take. */
677
- stages: z4.array(PlanStageSchema).min(1).optional(),
834
+ stages: z5.array(PlanStageSchema).min(1).optional(),
678
835
  /** A channel of the project the task belongs to, such as a release's; its thread opens there. */
679
836
  channel: NameSchema.default("general")
680
837
  }),
681
- claim_task: z4.object({ task_id: UlidSchema }),
682
- release_task: z4.object({ task_id: UlidSchema }),
683
- 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({
684
841
  task_id: UlidSchema,
685
842
  /** Move the task back to this earlier stage. */
686
843
  stage: StageIdSchema.optional(),
687
- status: z4.enum(["abandoned"]).optional(),
844
+ status: z5.enum(["abandoned"]).optional(),
688
845
  /** Posted to the task's thread as the caller, marked with the step when the task moved. */
689
- note: z4.string().min(1).optional(),
690
- blocked_by: z4.array(UlidSchema).optional(),
846
+ note: z5.string().min(1).optional(),
847
+ blocked_by: z5.array(UlidSchema).optional(),
691
848
  /**
692
849
  * The pull request a `ghpr` task lands through, on its project's repository, with the merge
693
850
  * commit's subject and body; null unlinks it.
694
851
  */
695
- pull_request: z4.object({
852
+ pull_request: z5.object({
696
853
  url: PullRequestUrlSchema,
697
854
  merge_subject: MergeSubjectSchema.optional(),
698
- merge_body: z4.string().max(2e4).optional()
855
+ merge_body: z5.string().max(2e4).optional()
699
856
  }).nullable().optional()
700
857
  }),
701
- get_task: z4.object({ task_id: UlidSchema }),
702
- subscribe: z4.object({ channel: ChannelRefSchema }),
703
- unsubscribe: z4.object({ channel: ChannelRefSchema }),
704
- 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({
705
862
  kind: ProposalKindSchema,
706
- charter: z4.record(z4.string(), z4.unknown()),
707
- rationale: z4.string().default("")
863
+ charter: z5.record(z5.string(), z5.unknown()),
864
+ rationale: z5.string().default("")
708
865
  }),
709
- approve: z4.object({ proposal_id: UlidSchema, reason: z4.string().optional() }),
710
- reject: z4.object({ proposal_id: UlidSchema, reason: z4.string().min(1) }),
711
- create_project: z4.object({
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({
712
869
  slug: NameSchema,
713
- name: z4.string().min(1).optional(),
714
- repo: z4.string().min(1).nullable().default(null),
870
+ name: z5.string().min(1).optional(),
871
+ repo: z5.string().min(1).nullable().default(null),
715
872
  default_branch: DefaultBranchSchema.default("main"),
716
873
  on_done: CompletionEffectSchema.optional()
717
874
  }),
718
- join_project: z4.object({ project: NameSchema, agent: NameSchema.optional() }),
719
- leave_project: z4.object({ project: NameSchema, agent: NameSchema.optional() }),
720
- write_knowledge: z4.object({
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({
721
878
  /** A project you belong to, or null for society knowledge, which the steward and the user curate. */
722
879
  project: NameSchema.nullable().default(null),
723
880
  topic: NameSchema,
724
- body: z4.string().min(1)
881
+ body: z5.string().min(1)
725
882
  }),
726
- remove_knowledge: z4.object({
883
+ remove_knowledge: z5.object({
727
884
  /** As for write_knowledge: a project you belong to, or null for society knowledge. */
728
885
  project: NameSchema.nullable().default(null),
729
886
  topic: NameSchema
730
887
  }),
731
- plan_task: z4.object({
888
+ plan_task: z5.object({
732
889
  task_id: UlidSchema,
733
- stages: z4.array(PlanEditStageSchema),
890
+ stages: z5.array(PlanEditStageSchema),
734
891
  on_done: CompletionEffectSchema.optional()
735
892
  }),
736
893
  /** The note is posted to the task's thread as the caller, marked with the step. */
737
- advance_task: z4.object({ task_id: UlidSchema, note: z4.string().min(1).optional() }),
738
- configure_project: z4.object({
894
+ advance_task: z5.object({ task_id: UlidSchema, note: z5.string().min(1).optional() }),
895
+ configure_project: z5.object({
739
896
  project: NameSchema,
740
897
  on_done: CompletionEffectSchema.optional(),
741
898
  /** A new display name; the slug, which everything refers to the project by, never changes. */
742
- name: z4.string().trim().min(1).max(120).optional(),
899
+ name: z5.string().trim().min(1).max(120).optional(),
743
900
  /** The branch tasks start from and land on from now; tasks in play keep their branches. */
744
901
  default_branch: DefaultBranchSchema.optional()
745
902
  }),
746
- archive_project: z4.object({ project: NameSchema, reason: z4.string().min(1) }),
747
- create_channel: z4.object({
903
+ archive_project: z5.object({ project: NameSchema, reason: z5.string().min(1) }),
904
+ create_channel: z5.object({
748
905
  /** A project, or null for a channel of the society. */
749
906
  project: NameSchema.nullable().default(null),
750
907
  name: NameSchema,
751
- purpose: z4.string().min(1)
908
+ purpose: z5.string().min(1)
752
909
  }),
753
- archive_channel: z4.object({ channel: ChannelRefSchema, reason: z4.string().min(1) }),
754
- update_dashboard: z4.object({
910
+ archive_channel: z5.object({ channel: ChannelRefSchema, reason: z5.string().min(1) }),
911
+ update_dashboard: z5.object({
755
912
  project: NameSchema,
756
913
  /** The whole dashboard as markdown; the board keeps its frontmatter. */
757
- body: z4.string().min(1),
914
+ body: z5.string().min(1),
758
915
  /** The `revision` in the frontmatter of the dashboard you read, 0 when it names none. */
759
- revision: z4.number().int().min(0)
760
- })
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) })
761
950
  };
762
951
  var VERB_DESCRIPTIONS = {
763
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.",
@@ -786,11 +975,14 @@ var VERB_DESCRIPTIONS = {
786
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.",
787
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.",
788
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.",
789
- 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."
790
982
  };
791
983
 
792
984
  // packages/shared/dist/events.js
793
- import { z as z5 } from "zod";
985
+ import { z as z6 } from "zod";
794
986
  var BOARD_EVENT_TYPES = [
795
987
  "society.initialized",
796
988
  "project.added",
@@ -843,30 +1035,38 @@ var BOARD_EVENT_TYPES = [
843
1035
  "turn.failed",
844
1036
  "merge.completed",
845
1037
  "merge.failed",
846
- "workspace.leftovers"
1038
+ "pull_request.closed",
1039
+ "pull_request.close_failed",
1040
+ "workspace.leftovers",
1041
+ "society.configured",
1042
+ "cron.created",
1043
+ "cron.updated",
1044
+ "cron.fired",
1045
+ "cron.skipped",
1046
+ "cron.ended"
847
1047
  ];
848
- var BoardEventTypeSchema = z5.enum(BOARD_EVENT_TYPES);
849
- var BoardEventSchema = z5.object({
1048
+ var BoardEventTypeSchema = z6.enum(BOARD_EVENT_TYPES);
1049
+ var BoardEventSchema = z6.object({
850
1050
  id: UlidSchema,
851
1051
  ts: IsoDateTimeSchema,
852
1052
  type: BoardEventTypeSchema,
853
1053
  actor: NameSchema,
854
- payload: z5.record(z5.string(), z5.unknown())
1054
+ payload: z6.record(z6.string(), z6.unknown())
855
1055
  });
856
- var UsageSchema = z5.object({
857
- inputTokens: z5.number().int().nonnegative(),
858
- outputTokens: z5.number().int().nonnegative(),
859
- cacheReadTokens: z5.number().int().nonnegative().default(0),
860
- 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)
861
1061
  });
862
- var TurnStatusSchema = z5.object({
863
- summary: z5.string().min(1),
864
- claimsHeld: z5.array(UlidSchema).default([]),
865
- blockedOn: z5.array(z5.string()).default([]),
866
- needsUserDecision: z5.boolean().default(false),
867
- 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)
868
1068
  });
869
- var TurnExitReasonSchema = z5.enum([
1069
+ var TurnExitReasonSchema = z6.enum([
870
1070
  "completed",
871
1071
  "timeout",
872
1072
  "error",
@@ -874,50 +1074,50 @@ var TurnExitReasonSchema = z5.enum([
874
1074
  "blocked",
875
1075
  "stopped"
876
1076
  ]);
877
- var AgentEventSchema = z5.discriminatedUnion("type", [
878
- z5.object({
879
- type: z5.literal("turn_started"),
1077
+ var AgentEventSchema = z6.discriminatedUnion("type", [
1078
+ z6.object({
1079
+ type: z6.literal("turn_started"),
880
1080
  agent: NameSchema,
881
- session: z5.string(),
1081
+ session: z6.string(),
882
1082
  runner: NameSchema,
883
1083
  /** The model the CLI reports running, when it reports one. */
884
- model: z5.string().optional()
1084
+ model: z6.string().optional()
885
1085
  }),
886
1086
  /**
887
1087
  * The CLI took a steer into the running turn. Adapters name the steer; the server adds the
888
1088
  * messages it delivered and the text the CLI was given.
889
1089
  */
890
- z5.object({
891
- type: z5.literal("steered"),
892
- steer: z5.string().min(1),
893
- messages: z5.array(UlidSchema).optional(),
894
- 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()
895
1095
  }),
896
- z5.object({ type: z5.literal("text"), delta: z5.string() }),
897
- z5.object({ type: z5.literal("tool_call"), name: z5.string(), input: z5.unknown() }),
898
- z5.object({
899
- type: z5.literal("tool_result"),
900
- name: z5.string(),
901
- 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(),
902
1102
  /** What the tool returned, cut to `OUTPUT_LIMIT` characters by `capOutput`. */
903
- output: z5.string().optional()
1103
+ output: z6.string().optional()
904
1104
  }),
905
- z5.object({ type: z5.literal("approval_requested"), kind: z5.string(), detail: z5.unknown() }),
906
- z5.object({
907
- 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"),
908
1108
  usage: UsageSchema,
909
- costUsd: z5.number().nonnegative(),
1109
+ costUsd: z6.number().nonnegative(),
910
1110
  status: TurnStatusSchema.nullable(),
911
1111
  exitReason: TurnExitReasonSchema
912
1112
  }),
913
- z5.object({ type: z5.literal("error"), message: z5.string() })
1113
+ z6.object({ type: z6.literal("error"), message: z6.string() })
914
1114
  ]);
915
- var TranscriptEntrySchema = z5.object({
1115
+ var TranscriptEntrySchema = z6.object({
916
1116
  ts: IsoDateTimeSchema,
917
1117
  event: AgentEventSchema
918
1118
  });
919
- var LiveTurnEventSchema = z5.object({
920
- seq: z5.number().int().positive(),
1119
+ var LiveTurnEventSchema = z6.object({
1120
+ seq: z6.number().int().positive(),
921
1121
  ts: IsoDateTimeSchema,
922
1122
  agent: NameSchema,
923
1123
  project: NameSchema,
@@ -929,10 +1129,10 @@ var LiveTurnEventSchema = z5.object({
929
1129
  });
930
1130
 
931
1131
  // packages/shared/dist/turns.js
932
- import { z as z7 } from "zod";
1132
+ import { z as z8 } from "zod";
933
1133
 
934
1134
  // packages/shared/dist/runner.js
935
- import { z as z6 } from "zod";
1135
+ import { z as z7 } from "zod";
936
1136
  var RUNNER_PROTOCOL = 1;
937
1137
  var PATH_TOKENS = {
938
1138
  home: "{{stellaris:home}}",
@@ -943,67 +1143,67 @@ var SERVER_TOKEN = "{{stellaris:server}}";
943
1143
  var WORKSPACE_TOKEN = "{{stellaris:workspace}}";
944
1144
  var HOME_SCRATCH = "scratch";
945
1145
  var HOME_FILE_LIMIT_BYTES = 8 * 1024 * 1024;
946
- var RunnerHelloSchema = z6.object({
947
- protocol: z6.number().int().positive(),
948
- version: z6.string().min(1),
1146
+ var RunnerHelloSchema = z7.object({
1147
+ protocol: z7.number().int().positive(),
1148
+ version: z7.string().min(1),
949
1149
  os: RunnerOsSchema,
950
- clis: z6.array(CliKindSchema),
1150
+ clis: z7.array(CliKindSchema),
951
1151
  /** The CLIs whose adapter can keep a session warm between turns. */
952
- residentClis: z6.array(CliKindSchema).default([]),
1152
+ residentClis: z7.array(CliKindSchema).default([]),
953
1153
  /** The CLIs whose adapter can take input into a running turn; the server steers only these. */
954
- steerableClis: z6.array(CliKindSchema).default([]),
1154
+ steerableClis: z7.array(CliKindSchema).default([]),
955
1155
  /** The CLIs whose adapter can stop a running turn; the server stops only these. */
956
- stoppableClis: z6.array(CliKindSchema).default([]),
1156
+ stoppableClis: z7.array(CliKindSchema).default([]),
957
1157
  /** Whether the runner answers file reads on its projects' branches; one from before never does. */
958
- branchReads: z6.boolean().default(false),
1158
+ branchReads: z7.boolean().default(false),
959
1159
  /** Whether the runner lists what a branch changed; one from before never does. */
960
- branchChanges: z6.boolean().default(false),
961
- capabilities: z6.array(z6.string()).default([]),
1160
+ branchChanges: z7.boolean().default(false),
1161
+ capabilities: z7.array(z7.string()).default([]),
962
1162
  /** Turns the runner runs at once, or null for no limit. */
963
- slots: z6.number().int().positive().nullable(),
1163
+ slots: z7.number().int().positive().nullable(),
964
1164
  /** Turns the runner is still running, when it registers again; the server fails the rest of its turns. */
965
- turns: z6.array(UlidSchema).default([])
1165
+ turns: z7.array(UlidSchema).default([])
966
1166
  });
967
- var RunnerWelcomeSchema = z6.object({ name: NameSchema, version: z6.string() });
968
- var RunnerEnrollRequestSchema = z6.object({
969
- protocol: z6.number().int().positive(),
970
- version: z6.string().min(1).max(64),
971
- 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),
972
1172
  os: RunnerOsSchema,
973
- clis: z6.array(CliKindSchema).max(8),
974
- 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([])
975
1175
  });
976
- var RunnerEnrollmentSchema = z6.object({
977
- deviceCode: z6.string().min(1),
978
- userCode: z6.string().min(1),
1176
+ var RunnerEnrollmentSchema = z7.object({
1177
+ deviceCode: z7.string().min(1),
1178
+ userCode: z7.string().min(1),
979
1179
  expiresAt: IsoDateTimeSchema,
980
- intervalMs: z6.number().int().positive()
1180
+ intervalMs: z7.number().int().positive()
981
1181
  });
982
- var RunnerEnrollPollSchema = z6.object({ deviceCode: z6.string().min(1) });
983
- var RunnerEnrollStatusSchema = z6.discriminatedUnion("status", [
984
- z6.object({ status: z6.literal("pending") }),
985
- z6.object({ status: z6.literal("approved"), name: NameSchema, token: z6.string().min(1) }),
986
- 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") })
987
1187
  ]);
988
- var PendingEnrollmentSchema = z6.object({
989
- userCode: z6.string().min(1),
990
- hostname: z6.string(),
1188
+ var PendingEnrollmentSchema = z7.object({
1189
+ userCode: z7.string().min(1),
1190
+ hostname: z7.string(),
991
1191
  os: RunnerOsSchema,
992
- clis: z6.array(CliKindSchema),
993
- capabilities: z6.array(z6.string()),
994
- version: z6.string(),
1192
+ clis: z7.array(CliKindSchema),
1193
+ capabilities: z7.array(z7.string()),
1194
+ version: z7.string(),
995
1195
  requestedAt: IsoDateTimeSchema,
996
1196
  expiresAt: IsoDateTimeSchema
997
1197
  });
998
- var ProjectRepoSchema = z6.object({
1198
+ var ProjectRepoSchema = z7.object({
999
1199
  slug: NameSchema,
1000
- origin: z6.string().nullable(),
1001
- defaultBranch: z6.string().min(1),
1200
+ origin: z7.string().nullable(),
1201
+ defaultBranch: z7.string().min(1),
1002
1202
  /**
1003
1203
  * The board lands finished tasks on the default branch, itself or through their pull requests,
1004
1204
  * so the runner lets nothing else move it.
1005
1205
  */
1006
- boardLands: z6.boolean()
1206
+ boardLands: z7.boolean()
1007
1207
  });
1008
1208
  function projectRepo(project) {
1009
1209
  return {
@@ -1013,29 +1213,29 @@ function projectRepo(project) {
1013
1213
  boardLands: project.onDone !== "none"
1014
1214
  };
1015
1215
  }
1016
- var TurnWorkspaceSchema = z6.discriminatedUnion("kind", [
1017
- z6.object({
1018
- kind: z6.literal("home"),
1216
+ var TurnWorkspaceSchema = z7.discriminatedUnion("kind", [
1217
+ z7.object({
1218
+ kind: z7.literal("home"),
1019
1219
  thread: UlidSchema.optional(),
1020
1220
  channel: NameSchema.optional()
1021
1221
  }),
1022
- z6.object({
1023
- kind: z6.literal("project"),
1222
+ z7.object({
1223
+ kind: z7.literal("project"),
1024
1224
  repo: ProjectRepoSchema,
1025
- branches: z6.array(z6.string().min(1)),
1225
+ branches: z7.array(z7.string().min(1)),
1026
1226
  thread: UlidSchema.optional(),
1027
1227
  channel: NameSchema.optional()
1028
1228
  }),
1029
- z6.object({ kind: z6.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
1229
+ z7.object({ kind: z7.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
1030
1230
  ]);
1031
- var TurnJobSchema = z6.object({
1231
+ var TurnJobSchema = z7.object({
1032
1232
  turnId: UlidSchema,
1033
1233
  agent: NameSchema,
1034
1234
  role: NameSchema,
1035
1235
  cli: CliKindSchema,
1036
- model: z6.string().optional(),
1236
+ model: z7.string().optional(),
1037
1237
  /** The reasoning effort set on the agent; absent for the model's own default. */
1038
- effort: z6.string().optional(),
1238
+ effort: z7.string().optional(),
1039
1239
  /** A project slug, or the society scope. */
1040
1240
  scope: NameSchema,
1041
1241
  /** The thread whose conversation the turn is in; absent for the home conversation. */
@@ -1046,112 +1246,117 @@ var TurnJobSchema = z6.object({
1046
1246
  * The session to resume, or null to start one: the CLI's adapter picks a fresh session's id, and
1047
1247
  * the outcome reports it.
1048
1248
  */
1049
- session: z6.string().min(1).nullable(),
1249
+ session: z7.string().min(1).nullable(),
1050
1250
  /** The running total the session reported after its last turn, which a resumed session starts from. */
1051
- costSoFarUsd: z6.number().nonnegative(),
1251
+ costSoFarUsd: z7.number().nonnegative(),
1052
1252
  /** The rendered charter, norms, memory core, and skills index, with `PATH_TOKENS` for places. */
1053
- instructions: z6.string(),
1054
- prompt: z6.string(),
1055
- mcp: z6.object({ url: z6.string().min(1), token: z6.string().min(1) }),
1056
- limits: z6.object({
1057
- timeoutMs: z6.number().int().positive().nullable(),
1058
- 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()
1059
1259
  }),
1060
1260
  /** Present when the runner keeps the conversation's session warm between turns, under `key`. */
1061
- 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(),
1062
1262
  workspace: TurnWorkspaceSchema
1063
1263
  });
1064
- var TurnWorkSchema = z6.object({ branch: z6.string().min(1), head: z6.string().min(1) });
1065
- var TurnEventsSchema = z6.object({ entries: z6.array(TranscriptEntrySchema) });
1066
- 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({
1067
1267
  exitReason: TurnExitReasonSchema,
1068
1268
  status: TurnStatusSchema.nullable(),
1069
- error: z6.string().nullable(),
1269
+ error: z7.string().nullable(),
1070
1270
  usage: UsageSchema,
1071
- costUsd: z6.number().nonnegative(),
1271
+ costUsd: z7.number().nonnegative(),
1072
1272
  /** The session's running total after this turn, for CLIs that report one. */
1073
- sessionCostUsd: z6.number().nonnegative().optional(),
1273
+ sessionCostUsd: z7.number().nonnegative().optional(),
1074
1274
  /** The session the CLI used, which differs from the job's for CLIs that assign their own ids. */
1075
- session: z6.string().min(1),
1275
+ session: z7.string().min(1),
1076
1276
  /** The model the CLI reported, when it reported one. */
1077
- model: z6.string().nullable(),
1277
+ model: z7.string().nullable(),
1078
1278
  work: TurnWorkSchema.nullable(),
1079
1279
  /**
1080
1280
  * Whether the turn's own workspace, a thread's, a channel's, or a task's, holds work on no
1081
1281
  * branch after the hand-back: uncommitted changes, or commits no branch contains.
1082
1282
  */
1083
- leftovers: z6.boolean().default(false)
1283
+ leftovers: z7.boolean().default(false)
1084
1284
  });
1085
- var TurnAckSchema = z6.object({ dropWorkspace: z6.boolean() });
1086
- var LandRequestSchema = z6.object({
1285
+ var TurnAckSchema = z7.object({ dropWorkspace: z7.boolean() });
1286
+ var LandRequestSchema = z7.object({
1087
1287
  repo: ProjectRepoSchema,
1088
- branch: z6.string().min(1),
1288
+ branch: z7.string().min(1),
1089
1289
  /**
1090
1290
  * For a `ghpr` task, the pull request to merge in place of the branch, which must hold the
1091
1291
  * branch's files, and the merge commit's subject and body, the pull request's title and an
1092
1292
  * empty body when left out.
1093
1293
  */
1094
- pullRequest: z6.object({
1095
- url: z6.string().min(1),
1096
- subject: z6.string().min(1).optional(),
1097
- 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()
1098
1298
  }).optional()
1099
1299
  });
1100
- var MergeOutcomeSchema = z6.object({ ok: z6.boolean(), detail: z6.string() });
1101
- var ModelListSchema = z6.array(ModelOptionSchema);
1300
+ var CloseRequestSchema = z7.object({
1301
+ repo: ProjectRepoSchema,
1302
+ url: z7.string().min(1),
1303
+ comment: z7.string().min(1)
1304
+ });
1305
+ var MergeOutcomeSchema = z7.object({ ok: z7.boolean(), detail: z7.string() });
1306
+ var ModelListSchema = z7.array(ModelOptionSchema);
1102
1307
  var BRANCH_FILE_LIMIT_BYTES = 8 * 1024 * 1024;
1103
- 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" });
1104
- var BranchNameSchema = z6.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/);
1105
- 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({
1106
1311
  repo: ProjectRepoSchema,
1107
1312
  branch: BranchNameSchema,
1108
1313
  path: BranchPathSchema
1109
1314
  });
1110
- var BranchCommitSchema = z6.object({
1111
- id: z6.string().min(1),
1315
+ var BranchCommitSchema = z7.object({
1316
+ id: z7.string().min(1),
1112
1317
  at: IsoDateTimeSchema,
1113
- author: z6.string()
1318
+ author: z7.string()
1114
1319
  });
1115
- var BranchFileSchema = z6.discriminatedUnion("kind", [
1116
- z6.object({
1117
- kind: z6.literal("file"),
1320
+ var BranchFileSchema = z7.discriminatedUnion("kind", [
1321
+ z7.object({
1322
+ kind: z7.literal("file"),
1118
1323
  path: BranchPathSchema,
1119
1324
  commit: BranchCommitSchema,
1120
- size: z6.number().int().nonnegative(),
1121
- content: z6.string().nullable()
1325
+ size: z7.number().int().nonnegative(),
1326
+ content: z7.string().nullable()
1122
1327
  }),
1123
- z6.object({
1124
- kind: z6.literal("dir"),
1328
+ z7.object({
1329
+ kind: z7.literal("dir"),
1125
1330
  path: BranchPathSchema,
1126
1331
  commit: BranchCommitSchema,
1127
- entries: z6.array(z6.object({
1128
- name: z6.string().min(1),
1129
- kind: z6.enum(["file", "dir"]),
1130
- 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()
1131
1336
  }))
1132
1337
  })
1133
1338
  ]);
1134
- var BranchChangesReadSchema = z6.object({
1339
+ var BranchChangesReadSchema = z7.object({
1135
1340
  repo: ProjectRepoSchema,
1136
1341
  branch: BranchNameSchema
1137
1342
  });
1138
- var BranchFileChangeSchema = z6.object({
1139
- path: z6.string().min(1),
1140
- status: z6.enum(["added", "modified", "deleted"]),
1343
+ var BranchFileChangeSchema = z7.object({
1344
+ path: z7.string().min(1),
1345
+ status: z7.enum(["added", "modified", "deleted"]),
1141
1346
  /** Lines added and removed, or null for a binary file. */
1142
- added: z6.number().int().nonnegative().nullable(),
1143
- removed: z6.number().int().nonnegative().nullable(),
1347
+ added: z7.number().int().nonnegative().nullable(),
1348
+ removed: z7.number().int().nonnegative().nullable(),
1144
1349
  /** Absent when the file changed only through a merge of another branch. */
1145
- lastChange: z6.object({ author: z6.string(), at: IsoDateTimeSchema }).optional()
1350
+ lastChange: z7.object({ author: z7.string(), at: IsoDateTimeSchema }).optional()
1146
1351
  });
1147
- var BranchChangesSchema = z6.object({
1352
+ var BranchChangesSchema = z7.object({
1148
1353
  head: BranchCommitSchema,
1149
- files: z6.array(BranchFileChangeSchema),
1150
- total: z6.number().int().nonnegative()
1354
+ files: z7.array(BranchFileChangeSchema),
1355
+ total: z7.number().int().nonnegative()
1151
1356
  });
1152
- var WorkspaceConversationSchema = z6.discriminatedUnion("kind", [
1153
- z6.object({ kind: z6.literal("thread"), id: UlidSchema }),
1154
- 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 })
1155
1360
  ]);
1156
1361
  function workspaceConversationOf(turn) {
1157
1362
  if (turn.thread !== void 0) {
@@ -1159,53 +1364,55 @@ function workspaceConversationOf(turn) {
1159
1364
  }
1160
1365
  return turn.channel === void 0 ? null : { kind: "channel", scope: turn.scope, name: turn.channel };
1161
1366
  }
1162
- var HeldWorkspaceSchema = z6.object({
1367
+ var HeldWorkspaceSchema = z7.object({
1163
1368
  agent: NameSchema,
1164
1369
  scope: NameSchema,
1165
1370
  conversation: WorkspaceConversationSchema
1166
1371
  });
1167
- var HeldWorkspacesSchema = z6.object({ workspaces: z6.array(HeldWorkspaceSchema) });
1168
- var SweepResultSchema = z6.object({ kept: z6.array(HeldWorkspaceSchema) });
1169
- var TurnSteerSchema = z6.object({ id: z6.uuid(), text: z6.string().min(1) });
1170
- var RunnerMessageSchema = z6.discriminatedUnion("type", [
1171
- z6.object({ type: z6.literal("turn"), job: TurnJobSchema }),
1172
- z6.object({ type: z6.literal("land"), request: z6.string().min(1), land: LandRequestSchema }),
1173
- z6.object({ type: z6.literal("models"), request: z6.string().min(1), cli: CliKindSchema }),
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 }),
1378
+ /** Answered with a `MergeOutcome`. */
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 }),
1174
1381
  /** Answered with a `BranchFile`, or null when the branch or the path does not exist. */
1175
- 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 }),
1176
1383
  /** Answered with `BranchChanges`, or null when the repository or the branch does not exist. */
1177
- z6.object({
1178
- type: z6.literal("changes"),
1179
- request: z6.string().min(1),
1384
+ z7.object({
1385
+ type: z7.literal("changes"),
1386
+ request: z7.string().min(1),
1180
1387
  read: BranchChangesReadSchema
1181
1388
  }),
1182
1389
  /** Answered with whether the CLI accepted it; that it took it comes as a `steered` event. */
1183
- z6.object({
1184
- type: z6.literal("steer"),
1185
- request: z6.string().min(1),
1390
+ z7.object({
1391
+ type: z7.literal("steer"),
1392
+ request: z7.string().min(1),
1186
1393
  turnId: UlidSchema,
1187
1394
  steer: TurnSteerSchema
1188
1395
  }),
1189
1396
  /** Answered with whether the turn was there to stop; it ends `stopped`. */
1190
- 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 }),
1191
1398
  /**
1192
1399
  * A conversation ended: the runner removes every citizen's workspace for it but those a turn
1193
1400
  * still uses, and answers with a `SweepResult` naming those it kept for holding work on no branch.
1194
1401
  */
1195
- z6.object({
1196
- type: z6.literal("sweep"),
1197
- request: z6.string().min(1),
1402
+ z7.object({
1403
+ type: z7.literal("sweep"),
1404
+ request: z7.string().min(1),
1198
1405
  conversation: WorkspaceConversationSchema
1199
1406
  })
1200
1407
  ]);
1201
- var RunnerAnswerSchema = z6.discriminatedUnion("ok", [
1202
- z6.object({ ok: z6.literal(true), value: z6.unknown() }),
1203
- 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() })
1204
1411
  ]);
1205
- var WarmSessionsSchema = z6.object({ warm: z6.array(z6.string()) });
1206
- var FileManifestSchema = z6.object({ files: z6.record(z6.string(), z6.string()) });
1207
- var FileReadSchema = z6.object({ paths: z6.array(z6.string()) });
1208
- 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()) });
1209
1416
  function isSafeRelativePath(file) {
1210
1417
  if (file.length === 0 || file.startsWith("/") || file.includes("\\") || file.includes("\0")) {
1211
1418
  return false;
@@ -1214,7 +1421,7 @@ function isSafeRelativePath(file) {
1214
1421
  }
1215
1422
 
1216
1423
  // packages/shared/dist/turns.js
1217
- var TriggerKindSchema = z7.enum([
1424
+ var TriggerKindSchema = z8.enum([
1218
1425
  "mention",
1219
1426
  "stage",
1220
1427
  "task_done",
@@ -1225,27 +1432,32 @@ var TriggerKindSchema = z7.enum([
1225
1432
  "manual",
1226
1433
  "ops_event",
1227
1434
  "user_post",
1228
- "closing"
1435
+ "closing",
1436
+ "cron"
1229
1437
  ]);
1230
- var TriggerSchema = z7.object({
1438
+ var TriggerSchema = z8.object({
1231
1439
  kind: TriggerKindSchema,
1232
1440
  from: NameSchema.optional(),
1233
- fromUser: z7.boolean().default(false),
1234
- reason: z7.string().default(""),
1441
+ fromUser: z8.boolean().default(false),
1442
+ reason: z8.string().default(""),
1235
1443
  taskId: UlidSchema.optional(),
1236
1444
  messageId: UlidSchema.optional(),
1237
1445
  /** The channel the waking message was posted in. */
1238
- channel: ChannelRefSchema.optional()
1446
+ channel: ChannelRefSchema.optional(),
1447
+ /** The cron that came due, for a `cron` wake. */
1448
+ cronId: UlidSchema.optional()
1239
1449
  });
1240
- var TurnThreadSchema = z7.object({ id: UlidSchema, task: z7.boolean() });
1241
- var TurnDispatchSchema = z7.object({
1450
+ var TurnThreadSchema = z8.object({ id: UlidSchema, task: z8.boolean() });
1451
+ var TurnDispatchSchema = z8.object({
1242
1452
  agent: NameSchema,
1243
1453
  project: NameSchema,
1244
1454
  thread: TurnThreadSchema.optional(),
1245
1455
  channel: NameSchema.optional(),
1246
1456
  trigger: TriggerSchema,
1247
- priority: z7.number().int().min(0).max(2),
1248
- 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()
1249
1461
  });
1250
1462
  function conversationPart(conversation) {
1251
1463
  return conversation.thread ?? (conversation.channel === void 0 ? void 0 : `#${conversation.channel}`);
@@ -1269,7 +1481,7 @@ function channelConversation(scope, channel) {
1269
1481
  const name = at < 0 ? channel : channel.slice(at + 1);
1270
1482
  return place === scope && name !== "general" ? name : void 0;
1271
1483
  }
1272
- var TurnRecordSchema = z7.object({
1484
+ var TurnRecordSchema = z8.object({
1273
1485
  /** Assigned by the board when the turn begins; the turn's transcript is filed under it. */
1274
1486
  id: UlidSchema.optional(),
1275
1487
  agent: NameSchema,
@@ -1280,29 +1492,31 @@ var TurnRecordSchema = z7.object({
1280
1492
  channel: NameSchema.optional(),
1281
1493
  runner: NameSchema,
1282
1494
  cli: CliKindSchema.nullable(),
1283
- session: z7.string().nullable(),
1495
+ session: z8.string().nullable(),
1284
1496
  trigger: TriggerSchema,
1285
1497
  startedAt: IsoDateTimeSchema,
1286
1498
  endedAt: IsoDateTimeSchema.nullable(),
1287
1499
  exitReason: TurnExitReasonSchema.nullable(),
1288
1500
  status: TurnStatusSchema.nullable(),
1289
- error: z7.string().nullable(),
1501
+ error: z8.string().nullable(),
1290
1502
  usage: UsageSchema.nullable(),
1291
- costUsd: z7.number().nonnegative().default(0),
1503
+ costUsd: z8.number().nonnegative().default(0),
1292
1504
  /**
1293
1505
  * The running total the CLI reported for the session after this turn, when it reports one. A
1294
1506
  * resumed session starts from it, so the next turn's own cost is measured from here.
1295
1507
  */
1296
- sessionCostUsd: z7.number().nonnegative().optional(),
1297
- toolCalls: z7.number().int().nonnegative().default(0),
1508
+ sessionCostUsd: z8.number().nonnegative().optional(),
1509
+ toolCalls: z8.number().int().nonnegative().default(0),
1298
1510
  /** The model the CLI reported for this turn, else the model configured for the agent, else null. */
1299
- model: z7.string().nullable().default(null),
1511
+ model: z8.string().nullable().default(null),
1300
1512
  /** The task branch the turn left work on, with its commit, as the runner reported it. */
1301
1513
  work: TurnWorkSchema.optional(),
1302
1514
  /** Who stopped the turn, for one that ended `stopped`. */
1303
- 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()
1304
1518
  });
1305
- var RunningTurnSchema = z7.object({
1519
+ var RunningTurnSchema = z8.object({
1306
1520
  turnId: UlidSchema,
1307
1521
  agent: NameSchema,
1308
1522
  scope: NameSchema,
@@ -1310,49 +1524,49 @@ var RunningTurnSchema = z7.object({
1310
1524
  channel: NameSchema.optional(),
1311
1525
  cli: CliKindSchema,
1312
1526
  askedIn: ChannelRefSchema.optional(),
1313
- steerable: z7.boolean(),
1314
- stoppable: z7.boolean()
1527
+ steerable: z8.boolean(),
1528
+ stoppable: z8.boolean()
1315
1529
  });
1316
- var SessionsFileSchema = z7.object({
1317
- claude: z7.string().optional(),
1318
- codex: z7.string().optional(),
1530
+ var SessionsFileSchema = z8.object({
1531
+ claude: z8.string().optional(),
1532
+ codex: z8.string().optional(),
1319
1533
  runner: NameSchema.optional()
1320
1534
  });
1321
- var WakeRequestSchema = z7.object({
1535
+ var WakeRequestSchema = z8.object({
1322
1536
  agent: NameSchema,
1323
1537
  project: NameSchema,
1324
- reason: z7.string().default("manual wake"),
1538
+ reason: z8.string().default("manual wake"),
1325
1539
  /** A manual wake, or a reflection turn requested ahead of the cadence. */
1326
- kind: z7.enum(["manual", "reflection"]).default("manual")
1540
+ kind: z8.enum(["manual", "reflection"]).default("manual")
1327
1541
  });
1328
- var TurnHistoryEntrySchema = z7.object({
1542
+ var TurnHistoryEntrySchema = z8.object({
1329
1543
  /** The id of the event that ended the turn. */
1330
1544
  id: UlidSchema,
1331
1545
  /** The turn's own id, when it has a transcript to open; turns before transcripts have none. */
1332
1546
  turnId: UlidSchema.optional(),
1333
1547
  ts: IsoDateTimeSchema,
1334
- outcome: z7.enum(["completed", "failed"]),
1548
+ outcome: z8.enum(["completed", "failed"]),
1335
1549
  /** A project slug, or the society scope. */
1336
1550
  project: NameSchema,
1337
1551
  /** The thread whose conversation the turn was in; absent for the home conversation. */
1338
1552
  thread: UlidSchema.optional(),
1339
1553
  /** The channel, never general, whose conversation the turn was in. */
1340
1554
  channel: NameSchema.optional(),
1341
- trigger: z7.string(),
1342
- exitReason: z7.string().nullable(),
1343
- costUsd: z7.number(),
1555
+ trigger: z8.string(),
1556
+ exitReason: z8.string().nullable(),
1557
+ costUsd: z8.number(),
1344
1558
  /** The tokens the turn used; absent for turns logged before turns recorded them. */
1345
1559
  usage: UsageSchema.optional(),
1346
- model: z7.string().nullable(),
1347
- summary: z7.string().nullable(),
1348
- error: z7.string().nullable(),
1560
+ model: z8.string().nullable(),
1561
+ summary: z8.string().nullable(),
1562
+ error: z8.string().nullable(),
1349
1563
  /** When the log recorded the turn's start; absent when the start is missing from the log. */
1350
1564
  startedAt: IsoDateTimeSchema.optional(),
1351
- toolCalls: z7.number().int().nonnegative().optional()
1565
+ toolCalls: z8.number().int().nonnegative().optional()
1352
1566
  });
1353
1567
 
1354
1568
  // packages/shared/dist/ops.js
1355
- import { z as z8 } from "zod";
1569
+ import { z as z9 } from "zod";
1356
1570
  var OPS_SIGNAL_KINDS = [
1357
1571
  "waiting_stage",
1358
1572
  "backlog",
@@ -1365,9 +1579,10 @@ var OPS_SIGNAL_KINDS = [
1365
1579
  "scaled",
1366
1580
  "runner",
1367
1581
  "home_conflict",
1368
- "stuck_workspace"
1582
+ "stuck_workspace",
1583
+ "cron_failing"
1369
1584
  ];
1370
- var OpsSignalKindSchema = z8.enum(OPS_SIGNAL_KINDS);
1585
+ var OpsSignalKindSchema = z9.enum(OPS_SIGNAL_KINDS);
1371
1586
  var WAKING_SIGNAL_KINDS = [
1372
1587
  "backlog",
1373
1588
  "role_gap",
@@ -1377,15 +1592,15 @@ var WAKING_SIGNAL_KINDS = [
1377
1592
  "blocked_capability",
1378
1593
  "scaled"
1379
1594
  ];
1380
- var OpsSignalSchema = z8.object({
1595
+ var OpsSignalSchema = z9.object({
1381
1596
  kind: OpsSignalKindSchema,
1382
1597
  /** Identifies the condition, for example `backlog:demo:engineer`, so a persisting one is not logged again every pass. */
1383
- key: z8.string().min(1),
1598
+ key: z9.string().min(1),
1384
1599
  /** One line a reader can act on. */
1385
- summary: z8.string().min(1),
1600
+ summary: z9.string().min(1),
1386
1601
  /** The counter or timer value behind the signal. */
1387
- value: z8.number(),
1388
- threshold: z8.number().optional(),
1602
+ value: z9.number(),
1603
+ threshold: z9.number().optional(),
1389
1604
  project: NameSchema.optional(),
1390
1605
  agent: NameSchema.optional(),
1391
1606
  role: NameSchema.optional(),
@@ -1393,8 +1608,8 @@ var OpsSignalSchema = z8.object({
1393
1608
  });
1394
1609
 
1395
1610
  // packages/shared/dist/metrics.js
1396
- import { z as z9 } from "zod";
1397
- var MetricsWindowSchema = z9.enum(["24h", "7d", "all"]);
1611
+ import { z as z10 } from "zod";
1612
+ var MetricsWindowSchema = z10.enum(["24h", "7d", "all"]);
1398
1613
  var METRICS_WINDOW_MS = {
1399
1614
  "24h": 24 * 36e5,
1400
1615
  "7d": 7 * 24 * 36e5,
@@ -1405,9 +1620,9 @@ function isBoardAction(tool) {
1405
1620
  const verb = /^mcp__board__(.+)$/.exec(tool)?.[1];
1406
1621
  return verb !== void 0 && !READ_VERBS.some((read2) => read2 === verb);
1407
1622
  }
1408
- var CountSchema = z9.number().int().nonnegative();
1409
- var MsSchema = z9.number().nonnegative();
1410
- var MetricsSchema = z9.object({
1623
+ var CountSchema = z10.number().int().nonnegative();
1624
+ var MsSchema = z10.number().nonnegative();
1625
+ var MetricsSchema = z10.object({
1411
1626
  window: MetricsWindowSchema,
1412
1627
  since: IsoDateTimeSchema.nullable(),
1413
1628
  until: IsoDateTimeSchema,
@@ -1415,12 +1630,12 @@ var MetricsSchema = z9.object({
1415
1630
  * Finished turns that changed nothing on the board. Reflections are left out, since they write
1416
1631
  * memory, not the board; turns without a transcript are counted apart, their actions unknown.
1417
1632
  */
1418
- idle: z9.object({
1633
+ idle: z10.object({
1419
1634
  turns: CountSchema,
1420
1635
  idle: CountSchema,
1421
1636
  unknown: CountSchema,
1422
- byTrigger: z9.array(z9.object({ trigger: z9.string(), turns: CountSchema, idle: CountSchema })),
1423
- byAgent: z9.array(z9.object({
1637
+ byTrigger: z10.array(z10.object({ trigger: z10.string(), turns: CountSchema, idle: CountSchema })),
1638
+ byAgent: z10.array(z10.object({
1424
1639
  agent: NameSchema,
1425
1640
  role: NameSchema.nullable(),
1426
1641
  turns: CountSchema,
@@ -1428,33 +1643,33 @@ var MetricsSchema = z9.object({
1428
1643
  }))
1429
1644
  }),
1430
1645
  /** Work sent back to an earlier stage: over the tasks finished in the window, and by where. */
1431
- sentBack: z9.object({
1646
+ sentBack: z10.object({
1432
1647
  finished: CountSchema,
1433
1648
  sendBacks: CountSchema,
1434
1649
  tasksSentBack: CountSchema,
1435
- byStage: z9.array(z9.object({ project: NameSchema, stage: z9.string(), count: CountSchema })),
1436
- 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 }))
1437
1652
  }),
1438
1653
  /** Posts in the threads of the tasks finished in the window, the board's own notices left out. */
1439
- messages: z9.object({
1654
+ messages: z10.object({
1440
1655
  finished: CountSchema,
1441
1656
  messages: CountSchema,
1442
- byProject: z9.array(z9.object({ project: NameSchema, finished: CountSchema, messages: CountSchema })),
1443
- busiest: z9.array(z9.object({
1657
+ byProject: z10.array(z10.object({ project: NameSchema, finished: CountSchema, messages: CountSchema })),
1658
+ busiest: z10.array(z10.object({
1444
1659
  taskId: UlidSchema,
1445
- title: z9.string(),
1660
+ title: z10.string(),
1446
1661
  project: NameSchema,
1447
1662
  messages: CountSchema
1448
1663
  }))
1449
1664
  }),
1450
1665
  /** Time from a mention to the start of the turn it woke, in the conversation it was made in. */
1451
- latency: z9.object({
1666
+ latency: z10.object({
1452
1667
  mentions: CountSchema,
1453
1668
  /** Mentions no turn has followed yet. */
1454
1669
  unanswered: CountSchema,
1455
1670
  medianMs: MsSchema.nullable(),
1456
1671
  slowestMs: MsSchema.nullable(),
1457
- byAgent: z9.array(z9.object({
1672
+ byAgent: z10.array(z10.object({
1458
1673
  agent: NameSchema,
1459
1674
  mentions: CountSchema,
1460
1675
  medianMs: MsSchema,
@@ -1462,61 +1677,61 @@ var MetricsSchema = z9.object({
1462
1677
  }))
1463
1678
  }),
1464
1679
  /** The user's decisions: proposals decided, questions answered, and stages passed or sent back. */
1465
- decisions: z9.object({
1680
+ decisions: z10.object({
1466
1681
  total: CountSchema,
1467
- byDay: z9.array(z9.object({
1468
- day: z9.string(),
1682
+ byDay: z10.array(z10.object({
1683
+ day: z10.string(),
1469
1684
  proposals: CountSchema,
1470
1685
  answers: CountSchema,
1471
1686
  stages: CountSchema
1472
1687
  }))
1473
1688
  }),
1474
1689
  /** Tasks that needed a capability no connected runner offered, and whether they still do. */
1475
- blocked: z9.array(z9.object({
1690
+ blocked: z10.array(z10.object({
1476
1691
  taskId: UlidSchema,
1477
- title: z9.string().nullable(),
1692
+ title: z10.string().nullable(),
1478
1693
  project: NameSchema.nullable(),
1479
- summary: z9.string(),
1694
+ summary: z10.string(),
1480
1695
  firstAt: IsoDateTimeSchema,
1481
1696
  lastAt: IsoDateTimeSchema,
1482
- holds: z9.boolean()
1697
+ holds: z10.boolean()
1483
1698
  }))
1484
1699
  });
1485
1700
 
1486
1701
  // packages/shared/dist/config.js
1487
- import { z as z10 } from "zod";
1488
- var LogLevelSchema = z10.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
1489
- var GithubSignInSchema = z10.object({
1490
- clientId: z10.string().min(1),
1491
- 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),
1492
1707
  /** Logins, lowercased; GitHub logins are case-insensitive. */
1493
- users: z10.array(z10.string().min(1)).min(1)
1708
+ users: z11.array(z11.string().min(1)).min(1)
1494
1709
  });
1495
- var ServerConfigSchema = z10.object({
1496
- dataDir: z10.string().min(1),
1497
- host: z10.string().min(1),
1498
- 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),
1499
1714
  /**
1500
1715
  * An address every runner's agents reach this server at, for example https://board.example, or
1501
1716
  * null for each runner to use the address it reaches the server at itself.
1502
1717
  */
1503
- publicUrl: z10.string().min(1).nullable(),
1718
+ publicUrl: z11.string().min(1).nullable(),
1504
1719
  logLevel: LogLevelSchema,
1505
1720
  /** Turns running at once across every runner, or null for no limit beyond each runner's own. */
1506
- concurrency: z10.number().int().positive().nullable(),
1721
+ concurrency: z11.number().int().positive().nullable(),
1507
1722
  /** How long one turn may run before it is stopped, or null for no limit. */
1508
- turnTimeoutMs: z10.number().int().positive().nullable(),
1723
+ turnTimeoutMs: z11.number().int().positive().nullable(),
1509
1724
  /** Rounds of tool calls one Claude turn may take, or null for no limit. */
1510
- toolRounds: z10.number().int().positive().nullable(),
1725
+ toolRounds: z11.number().int().positive().nullable(),
1511
1726
  /** How long a resident session stays warm after its last turn. */
1512
- residentIdleMs: z10.number().int().positive(),
1727
+ residentIdleMs: z11.number().int().positive(),
1513
1728
  /** Signing in to the board with GitHub, or null when only a token lets anyone in. */
1514
1729
  github: GithubSignInSchema.nullable(),
1515
1730
  /**
1516
1731
  * Reverse proxies in front of the server whose X-Forwarded-For entries name the client, for the
1517
1732
  * rate limits on what anyone may ask without a token: 0 when clients connect directly.
1518
1733
  */
1519
- trustedProxies: z10.number().int().min(0).max(16)
1734
+ trustedProxies: z11.number().int().min(0).max(16)
1520
1735
  });
1521
1736
  function limit(value, fallback) {
1522
1737
  if (value === void 0 || value.trim() === "") {
@@ -1554,25 +1769,25 @@ function loadServerConfig(env) {
1554
1769
  trustedProxies: Number(env["STELLARIS_TRUSTED_PROXIES"] ?? "0")
1555
1770
  });
1556
1771
  }
1557
- var RunnerConfigSchema = z10.object({
1772
+ var RunnerConfigSchema = z11.object({
1558
1773
  /** The board server, for example http://127.0.0.1:4700. */
1559
- serverUrl: z10.string().url(),
1774
+ serverUrl: z11.string().url(),
1560
1775
  /**
1561
1776
  * The runner's token, from `runner add`, or null for the one it saved when it was enrolled, or
1562
1777
  * to enroll it.
1563
1778
  */
1564
- token: z10.string().min(1).nullable(),
1779
+ token: z11.string().min(1).nullable(),
1565
1780
  /** The runner's own data directory: the board mirror, agent homes, repositories, worktrees. */
1566
- dataDir: z10.string().min(1),
1781
+ dataDir: z11.string().min(1),
1567
1782
  logLevel: LogLevelSchema,
1568
1783
  /** Turns this machine runs at once, or null for no limit. */
1569
- slots: z10.number().int().positive().nullable(),
1570
- clis: z10.array(CliKindSchema).min(1),
1571
- 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())
1572
1787
  });
1573
1788
 
1574
1789
  // packages/shared/dist/version.js
1575
- var STELLARIS_VERSION = "0.3.12";
1790
+ var STELLARIS_VERSION = "0.3.14";
1576
1791
 
1577
1792
  // packages/board-core/dist/errors.js
1578
1793
  var BoardError = class extends Error {
@@ -2120,32 +2335,32 @@ function filesOf(lines, status) {
2120
2335
  }
2121
2336
 
2122
2337
  // packages/board-core/dist/metrics.js
2123
- import { z as z11 } from "zod";
2124
- var TurnPayload = z11.object({
2125
- turnId: z11.string().optional(),
2126
- project: z11.string(),
2127
- thread: z11.string().optional(),
2128
- channel: z11.string().optional(),
2129
- 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()
2130
2345
  });
2131
- var TaskPayload = z11.object({
2132
- taskId: z11.string(),
2133
- project: z11.string(),
2134
- from: z11.string().optional()
2346
+ var TaskPayload = z12.object({
2347
+ taskId: z12.string(),
2348
+ project: z12.string(),
2349
+ from: z12.string().optional()
2135
2350
  });
2136
- var MessagePayload = z11.object({
2137
- id: z11.string().optional(),
2138
- channel: z11.string(),
2139
- thread: z11.string().nullable().optional(),
2140
- 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([])
2141
2356
  });
2142
- var SteeredPayload = z11.object({ messages: z11.array(z11.string()) });
2143
- var SignalPayload = z11.object({
2144
- kind: z11.string(),
2145
- key: z11.string(),
2146
- summary: z11.string(),
2147
- taskId: z11.string().optional(),
2148
- 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()
2149
2364
  });
2150
2365
  function median(values) {
2151
2366
  const sorted = values.toSorted((a, b) => a - b);
@@ -2514,6 +2729,13 @@ var BoardPaths = class {
2514
2729
  members() {
2515
2730
  return path5.join(this.society, "members");
2516
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
+ }
2517
2739
  member(name) {
2518
2740
  return path5.join(this.members(), `${name}.md`);
2519
2741
  }
@@ -2660,41 +2882,41 @@ function namesList(names) {
2660
2882
  return names.length <= 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}`;
2661
2883
  }
2662
2884
  var SKIPPED_IN_HOME = /* @__PURE__ */ new Set([".git", "turns", ".claude", ".codex"]);
2663
- var RunnerTokensSchema = z12.object({
2664
- 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) }))
2665
2887
  });
2666
- var SignInsSchema = z12.object({
2667
- signIns: z12.array(z12.object({
2668
- tokenHash: z12.string().min(1),
2669
- 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),
2670
2892
  /** The account's picture at the identity provider; sign-ins from before it have none. */
2671
- avatarUrl: z12.url().optional(),
2893
+ avatarUrl: z13.url().optional(),
2672
2894
  createdAt: IsoDateTimeSchema,
2673
2895
  expiresAt: IsoDateTimeSchema
2674
2896
  })).default([])
2675
2897
  });
2676
- var TurnHistoryPayloadSchema = z12.object({
2898
+ var TurnHistoryPayloadSchema = z13.object({
2677
2899
  project: NameSchema,
2678
2900
  thread: UlidSchema.optional(),
2679
2901
  channel: NameSchema.optional(),
2680
- trigger: z12.string().default("manual"),
2681
- exitReason: z12.string().nullable().default(null),
2682
- costUsd: z12.number().default(0),
2902
+ trigger: z13.string().default("manual"),
2903
+ exitReason: z13.string().nullable().default(null),
2904
+ costUsd: z13.number().default(0),
2683
2905
  usage: UsageSchema.optional(),
2684
- model: z12.string().nullable().default(null),
2685
- summary: z12.string().nullable().default(null),
2686
- error: z12.string().nullable().default(null),
2687
- 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(),
2688
2910
  turnId: UlidSchema.optional()
2689
2911
  });
2690
- var TurnStartPayloadSchema = z12.object({ project: NameSchema, turnId: UlidSchema.optional() });
2691
- var CursorsSchema = z12.object({
2692
- digest: z12.string().nullable(),
2693
- scopes: z12.record(z12.string(), z12.string()).default({}),
2694
- threadsFrom: z12.record(z12.string(), z12.string().nullable()).default({}),
2695
- 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({}),
2696
2918
  /** How far a work role has read news, which goes to whichever of its general conversations reads first. */
2697
- news: z12.string().optional()
2919
+ news: z13.string().optional()
2698
2920
  });
2699
2921
  var NO_CURSORS = { digest: null, scopes: {}, threadsFrom: {}, channelsFrom: {} };
2700
2922
  function conversationKey(scope, part) {
@@ -2733,7 +2955,7 @@ function byId(a, b) {
2733
2955
  function turnTime(record) {
2734
2956
  return record.endedAt ?? record.startedAt;
2735
2957
  }
2736
- var PausedSchema = z12.object({ paused: z12.boolean() });
2958
+ var PausedSchema = z13.object({ paused: z13.boolean() });
2737
2959
  var DEFAULT_LEASE_MS = 30 * 60 * 1e3;
2738
2960
  var MENTION_PATTERN = /(^|[^\w@])@([a-z0-9][a-z0-9-]{0,31})(?![\w-])/g;
2739
2961
  function withoutMentions(text) {
@@ -2745,7 +2967,7 @@ var OPS_WAKE_TRIGGER = "ops_event";
2745
2967
  var FRONT_DESK_TRIGGER = "user_post";
2746
2968
  var OPS_CHANNELS = ["governance"];
2747
2969
  var FRONT_DESK_CHANNELS = [ASK_CHANNEL];
2748
- var AlignmentsSchema = z12.object({ applied: z12.array(z12.string()).default([]) });
2970
+ var AlignmentsSchema = z13.object({ applied: z13.array(z13.string()).default([]) });
2749
2971
  var FOLLOW_DECISIONS = "ops-readers-follow-decisions";
2750
2972
  var THREAD_RECORDS = "threads-as-records";
2751
2973
  var DIGEST_CURSORS = "digest-replaces-inbox";
@@ -2757,6 +2979,8 @@ var ASKS_CHANNEL_OPENED = "asks-channel-opened";
2757
2979
  var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
2758
2980
  var CHANNEL_CONVERSATIONS = "channel-conversations";
2759
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"];
2760
2984
  var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
2761
2985
  var DASHBOARD_LINE = "The project's members keep this page current with update_dashboard.";
2762
2986
  var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
@@ -2958,7 +3182,8 @@ var Board = class _Board {
2958
3182
  ASKS_CHANNEL_OPENED,
2959
3183
  KNOWLEDGE_REMOVAL,
2960
3184
  CHANNEL_CONVERSATIONS,
2961
- DASHBOARD_UPDATES
3185
+ DASHBOARD_UPDATES,
3186
+ CRON_VERBS
2962
3187
  ].filter((name) => !applied.includes(name));
2963
3188
  if (pending.length === 0) {
2964
3189
  return;
@@ -2997,6 +3222,9 @@ var Board = class _Board {
2997
3222
  await this.grantBesideKnowledge("update_dashboard");
2998
3223
  await this.rewordDashboards();
2999
3224
  }
3225
+ if (pending.includes(CRON_VERBS)) {
3226
+ await this.grantToEveryRole(CRON_VERB_NAMES);
3227
+ }
3000
3228
  await writeJson(file, { applied: [...applied, ...pending] });
3001
3229
  }
3002
3230
  /**
@@ -3010,6 +3238,15 @@ var Board = class _Board {
3010
3238
  }
3011
3239
  }
3012
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
+ }
3013
3250
  /** A dashboard still saying agents may edit its file says how it changes now. */
3014
3251
  async rewordDashboards() {
3015
3252
  for (const slug of await listDirs(this.paths.projects())) {
@@ -3154,7 +3391,7 @@ var Board = class _Board {
3154
3391
  }
3155
3392
  }
3156
3393
  for (const file of await listFiles(this.paths.tasks(slug))) {
3157
- 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()));
3158
3395
  const legacy = raw.data["thread"];
3159
3396
  if (legacy === void 0) {
3160
3397
  continue;
@@ -3232,7 +3469,7 @@ var Board = class _Board {
3232
3469
  * turns, so it follows nothing now.
3233
3470
  */
3234
3471
  async renameInboxCursors() {
3235
- const Legacy = z12.object({ inbox: z12.string().nullable() });
3472
+ const Legacy = z13.object({ inbox: z13.string().nullable() });
3236
3473
  for (const agent of await this.listAgents()) {
3237
3474
  const file = this.paths.agentCursors(agent.name);
3238
3475
  if (await exists(file)) {
@@ -3278,7 +3515,7 @@ ${charter.purpose}
3278
3515
  */
3279
3516
  async alignSeedRole(seed) {
3280
3517
  const file = this.paths.role(seed.name);
3281
- const raw = await readMarkdown(file, z12.record(z12.string(), z12.unknown()));
3518
+ const raw = await readMarkdown(file, z13.record(z13.string(), z13.unknown()));
3282
3519
  const current = RoleCharterSchema.parse(raw.data);
3283
3520
  const verbsAdded = seed.verbs.filter((verb) => !current.verbs.includes(verb));
3284
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));
@@ -3591,14 +3828,14 @@ ${charter.purpose}
3591
3828
  /** The society's norms, the one knowledge topic every turn loads. Empty until the steward writes it. */
3592
3829
  async readSocietyNorms() {
3593
3830
  const file = this.paths.societyKnowledgeFile("norms");
3594
- 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 : "";
3595
3832
  }
3596
3833
  /** Knowledge topics of a project, or of the society when the project is null. */
3597
3834
  async listKnowledge(project) {
3598
3835
  const dir = project === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project);
3599
3836
  const topics = [];
3600
3837
  for (const file of await listFiles(dir)) {
3601
- 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()));
3602
3839
  const parsed = KnowledgeSchema.safeParse({
3603
3840
  ...doc.data,
3604
3841
  project,
@@ -3998,6 +4235,21 @@ ${DASHBOARD_LINE}
3998
4235
  await this.events.append("paused.changed", actor.name, { paused });
3999
4236
  });
4000
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
+ }
4001
4253
  /** Releases every claim whose lease has expired. Called by the scheduler on its tick. */
4002
4254
  async expireLeases() {
4003
4255
  return this.mutex.run(async () => {
@@ -4420,6 +4672,241 @@ ${current.body}`);
4420
4672
  return updated;
4421
4673
  });
4422
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
+ }
4423
4910
  async openThread(actor, input) {
4424
4911
  const args = VerbInputs.open_thread.parse(input);
4425
4912
  await this.authorize(actor, "open_thread");
@@ -4636,6 +5123,7 @@ ${args.summary}`);
4636
5123
  threadId: thread.id,
4637
5124
  channel: thread.channel
4638
5125
  });
5126
+ await this.endCronsUnlocked(actor.name, "its thread closed", (cron) => cron.thread === thread.id);
4639
5127
  return summary;
4640
5128
  });
4641
5129
  }
@@ -5137,6 +5625,13 @@ ${args.summary}`);
5137
5625
  proposedBy: actor.name,
5138
5626
  status: "proposed",
5139
5627
  createdAt: this.now().toISOString(),
5628
+ ...actor.scope === void 0 ? {} : {
5629
+ filedFrom: {
5630
+ scope: actor.scope,
5631
+ ...actor.thread === void 0 ? {} : { thread: actor.thread },
5632
+ ...actor.channel === void 0 ? {} : { channel: actor.channel }
5633
+ }
5634
+ },
5140
5635
  charter: parsed.data
5141
5636
  }),
5142
5637
  body: args.rationale
@@ -5308,6 +5803,7 @@ ${rationale}`}`);
5308
5803
  members: project.members.filter((member) => member !== target)
5309
5804
  }));
5310
5805
  await this.refreshMember(target);
5806
+ await this.endCronsUnlocked(actor.name, `${target} left ${args.project}`, (cron) => cron.agent === target && cron.scope === args.project);
5311
5807
  await this.events.append("agent.left", actor.name, {
5312
5808
  name: target,
5313
5809
  project: args.project,
@@ -5376,6 +5872,12 @@ ${rationale}`}`);
5376
5872
  return this.archiveChannel(actor, VerbInputs.archive_channel.parse(input));
5377
5873
  case "update_dashboard":
5378
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));
5379
5881
  default:
5380
5882
  throw new BoardError("VALIDATION", `unknown verb ${String(verb)}`);
5381
5883
  }
@@ -5422,7 +5924,7 @@ ${rationale}`}`);
5422
5924
  }
5423
5925
  async readAgentRoleBody(agent) {
5424
5926
  const file = this.paths.agentRole(agent);
5425
- 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 : "";
5426
5928
  }
5427
5929
  /** The steps of one finished turn, as the runner handed them over when it ended. */
5428
5930
  /**
@@ -5600,6 +6102,9 @@ ${rationale}`}`);
5600
6102
  }
5601
6103
  await this.writeTurnRecord(parsed);
5602
6104
  await this.refreshMember(parsed.agent);
6105
+ for (const id of parsed.crons ?? []) {
6106
+ await this.recordCronRun(id, parsed, failed);
6107
+ }
5603
6108
  if (parsed.exitReason === "stopped") {
5604
6109
  await this.events.append("turn.stopped", parsed.stoppedBy ?? USER_NAME, {
5605
6110
  ...parsed.id === void 0 ? {} : { turnId: parsed.id },
@@ -5637,6 +6142,29 @@ ${rationale}`}`);
5637
6142
  });
5638
6143
  });
5639
6144
  }
6145
+ /**
6146
+ * Records how closing an abandoned `ghpr` task's pull request on GitHub went, once: the task's
6147
+ * link keeps it, so a restart does not try again.
6148
+ */
6149
+ async recordPullRequestClosed(actor, input) {
6150
+ return this.mutex.run(async () => {
6151
+ const location = await this.findTask(input.taskId);
6152
+ const current = location.task;
6153
+ const pull = current.pullRequest;
6154
+ if (pull === void 0 || pull.closed !== void 0) {
6155
+ return current;
6156
+ }
6157
+ const task = await this.writeTask(location.project, {
6158
+ ...current,
6159
+ pullRequest: {
6160
+ ...pull,
6161
+ closed: { at: this.now().toISOString(), ok: input.ok, detail: input.detail }
6162
+ }
6163
+ });
6164
+ await this.events.append(input.ok ? "pull_request.closed" : "pull_request.close_failed", actor.name, { project: location.project, taskId: task.id, url: pull.url, detail: input.detail });
6165
+ return task;
6166
+ });
6167
+ }
5640
6168
  /**
5641
6169
  * Records that a citizen's workspace in a conversation that has ended holds work on no branch, so
5642
6170
  * the scheduler gives the citizen a closing turn there to decide about it. False, recording
@@ -5658,6 +6186,45 @@ ${rationale}`}`);
5658
6186
  return true;
5659
6187
  });
5660
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
+ }
5661
6228
  /**
5662
6229
  * Whether a conversation with a workspace of its own has ended: a thread closed, a task ended
5663
6230
  * (for one from before tasks had threads), or a channel archived.
@@ -5757,7 +6324,7 @@ ${rationale}`}`);
5757
6324
  return { project: slug, revision: 0, body: "" };
5758
6325
  }
5759
6326
  const { data, body } = await readLooseMarkdown(file);
5760
- const revision = z12.number().int().min(0).safeParse(data["revision"]);
6327
+ const revision = z13.number().int().min(0).safeParse(data["revision"]);
5761
6328
  const updatedBy = NameSchema.safeParse(data["updatedBy"]);
5762
6329
  const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
5763
6330
  return {
@@ -6048,7 +6615,7 @@ ${seed}
6048
6615
  continue;
6049
6616
  }
6050
6617
  const file = this.paths.agentRole(agent.name);
6051
- 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 : "";
6052
6619
  const at = body.indexOf(`
6053
6620
  ${SEED_INSTRUCTIONS_HEADING}
6054
6621
  `);
@@ -6492,6 +7059,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
6492
7059
  archivedChannels: [...current.archivedChannels, archived]
6493
7060
  }));
6494
7061
  }
7062
+ await this.endCronsUnlocked(by, `${ref} was archived`, (cron) => cron.scope === scope && cron.channel === channel || cron.thread !== void 0 && threadsClosed.includes(cron.thread));
6495
7063
  await this.events.append("channel.archived", by, {
6496
7064
  channel: ref,
6497
7065
  reason,
@@ -6596,6 +7164,7 @@ ${charter.purpose}
6596
7164
  });
6597
7165
  threadsClosed.push(doc.data.id);
6598
7166
  }
7167
+ await this.endCronsUnlocked(by, `project ${slug} was archived`, (cron) => cron.scope === slug || cron.thread !== void 0 && threadsClosed.includes(cron.thread));
6599
7168
  const project = await this.updateProject(slug, (p) => ({
6600
7169
  ...p,
6601
7170
  members: [],
@@ -6660,6 +7229,7 @@ ${charter.purpose}
6660
7229
  }
6661
7230
  this.tokenIndex.delete(current.tokenHash);
6662
7231
  await this.refreshMember(name);
7232
+ await this.endCronsUnlocked(by, `${name} was retired`, (cron) => cron.agent === name);
6663
7233
  await this.events.append("agent.retired", by, {
6664
7234
  name,
6665
7235
  role: agent.role,
@@ -6869,7 +7439,8 @@ ${charter.purpose}
6869
7439
  proposalId,
6870
7440
  outcome,
6871
7441
  kind: proposal.kind,
6872
- proposedBy: proposal.proposedBy
7442
+ proposedBy: proposal.proposedBy,
7443
+ ...proposal.filedFrom === void 0 ? {} : { filedFrom: proposal.filedFrom }
6873
7444
  });
6874
7445
  if (provision !== void 0) {
6875
7446
  await this.events.append("proposal.provisioned", actor.name, {
@@ -6970,6 +7541,7 @@ ${charter.purpose}
6970
7541
  channel: found.thread.channel,
6971
7542
  ended
6972
7543
  });
7544
+ await this.endCronsUnlocked(by, `its thread closed: its ${found.thread.subject?.kind ?? "subject"} was ${ended}`, (cron) => cron.thread === subjectId);
6973
7545
  }
6974
7546
  async readMessagesIn(dir, since) {
6975
7547
  const messages = [];
@@ -7033,6 +7605,8 @@ function decideWake(input) {
7033
7605
  return { wake: true, reason: "user post", priority: 2 };
7034
7606
  case "closing":
7035
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 };
7036
7610
  case "heartbeat":
7037
7611
  break;
7038
7612
  }
@@ -7047,7 +7621,7 @@ function decideWake(input) {
7047
7621
  }
7048
7622
 
7049
7623
  // packages/scheduler/dist/scheduler.js
7050
- import { z as z13 } from "zod";
7624
+ import { z as z14 } from "zod";
7051
7625
  function besideFields(beside) {
7052
7626
  if (beside === void 0) {
7053
7627
  return {};
@@ -7074,21 +7648,21 @@ var DEFAULT_TIMINGS = Object.freeze({
7074
7648
  scaleCooldownMs: 36e5,
7075
7649
  reflectionMs: 24 * 36e5
7076
7650
  });
7077
- var SchedulerTimingsSchema = z13.object({
7078
- pollMs: z13.number().positive(),
7079
- debounceMs: z13.number().nonnegative(),
7080
- userDebounceMs: z13.number().nonnegative(),
7081
- heartbeatMs: z13.number().positive(),
7082
- waitingStageMs: z13.number().positive(),
7083
- leaseSweepMs: z13.number().positive(),
7084
- opsIntervalMs: z13.number().positive(),
7085
- signalRepeatMs: z13.number().positive(),
7086
- costReportMs: z13.number().positive(),
7087
- staleThreadMs: z13.number().positive(),
7088
- idleMemberMs: z13.number().positive(),
7089
- homeConflictMs: z13.number().positive(),
7090
- scaleCooldownMs: z13.number().positive(),
7091
- 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()
7092
7666
  }).partial();
7093
7667
  function parseTimings(input) {
7094
7668
  const parsed = SchedulerTimingsSchema.parse(input);
@@ -7100,31 +7674,32 @@ function parseTimings(input) {
7100
7674
  }
7101
7675
  return overrides;
7102
7676
  }
7103
- var PendingSchema = z13.object({
7677
+ var PendingSchema = z14.object({
7104
7678
  dispatch: TurnDispatchSchema,
7105
- readyAt: z13.number(),
7106
- stageWakes: z13.array(TriggerSchema).optional(),
7107
- holder: z13.boolean().optional(),
7679
+ readyAt: z14.number(),
7680
+ stageWakes: z14.array(TriggerSchema).optional(),
7681
+ holder: z14.boolean().optional(),
7108
7682
  newestMessage: UlidSchema.optional(),
7109
- onlyMessages: z13.boolean().optional(),
7110
- steeredThrough: UlidSchema.optional()
7683
+ onlyMessages: z14.boolean().optional(),
7684
+ steeredThrough: UlidSchema.optional(),
7685
+ onlyCrons: z14.boolean().optional()
7111
7686
  });
7112
- var StateSchema = z13.object({
7113
- cursor: z13.string().nullable(),
7114
- 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()),
7115
7690
  /** Waiting stages already signalled, as `task:stage`, so each is logged once while it waits. */
7116
- waitingSeen: z13.record(z13.string(), z13.string()).default({}),
7691
+ waitingSeen: z14.record(z14.string(), z14.string()).default({}),
7117
7692
  /** Queued dispatches survive a restart; the cursor has already moved past the events that made them. */
7118
- pending: z13.record(z13.string(), PendingSchema).default({}),
7119
- opsReported: z13.record(z13.string(), z13.string()).default({}),
7120
- releaseCounts: z13.record(z13.string(), z13.number().int().nonnegative()).default({}),
7121
- lastOps: z13.string().nullable().default(null),
7122
- lastScaled: z13.record(z13.string(), z13.string()).default({}),
7123
- lastCostReport: z13.string().nullable().default(null),
7124
- costSinceReport: z13.number().nonnegative().default(0),
7125
- 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),
7126
7701
  /** When each member last reflected, or was first seen; the cadence counts from here. */
7127
- lastReflection: z13.record(z13.string(), z13.string()).default({})
7702
+ lastReflection: z14.record(z14.string(), z14.string()).default({})
7128
7703
  });
7129
7704
  function messageOf(trigger) {
7130
7705
  return trigger.kind === "mention" || trigger.kind === "user_post" ? trigger.messageId : void 0;
@@ -7150,6 +7725,9 @@ function stringArray(value) {
7150
7725
  function stringOf(value) {
7151
7726
  return typeof value === "string" ? value : null;
7152
7727
  }
7728
+ function awaitsClosing(task) {
7729
+ return task.status === "abandoned" && task.onDone === "ghpr" && task.pullRequest !== void 0 && task.pullRequest.closed === void 0;
7730
+ }
7153
7731
  function inPlay(task) {
7154
7732
  return (task.status === "open" || task.status === "claimed") && !task.completing;
7155
7733
  }
@@ -7182,6 +7760,9 @@ var Scheduler = class {
7182
7760
  steering = /* @__PURE__ */ new Map();
7183
7761
  /** Tasks whose completion effect waits for the turn that finished their last stage to end. */
7184
7762
  completions = /* @__PURE__ */ new Map();
7763
+ /** Abandoned `ghpr` tasks whose pull request waits to be closed, by task id, with its project. */
7764
+ closings = /* @__PURE__ */ new Map();
7765
+ /** Completions and closings under way. */
7185
7766
  completing = /* @__PURE__ */ new Set();
7186
7767
  /** Running turns that must run alone among their citizen's, by session key: see `heldBack`. */
7187
7768
  exclusive = /* @__PURE__ */ new Map();
@@ -7189,6 +7770,8 @@ var Scheduler = class {
7189
7770
  landing = /* @__PURE__ */ new Set();
7190
7771
  /** Sweeps of ended conversations' workspaces under way, which `drain` and `stop` wait for. */
7191
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();
7192
7775
  state = StateSchema.parse({
7193
7776
  cursor: null,
7194
7777
  lastHeartbeat: {}
@@ -7268,6 +7851,7 @@ var Scheduler = class {
7268
7851
  this.state.cursor = event.id;
7269
7852
  }
7270
7853
  if (!paused) {
7854
+ await this.checkCrons(now);
7271
7855
  await this.checkHeartbeats(now);
7272
7856
  await this.checkWaitingStages(now);
7273
7857
  await this.checkReflections(now);
@@ -7281,6 +7865,7 @@ var Scheduler = class {
7281
7865
  this.lastSweep = now;
7282
7866
  }
7283
7867
  this.runCompletions();
7868
+ this.runClosings();
7284
7869
  if (!paused) {
7285
7870
  await this.dispatchReady(now);
7286
7871
  }
@@ -7308,6 +7893,9 @@ var Scheduler = class {
7308
7893
  if (task.completing) {
7309
7894
  this.completions.set(task.id, { project: project.slug, actor: USER_NAME });
7310
7895
  }
7896
+ if (awaitsClosing(task)) {
7897
+ this.closings.set(task.id, project.slug);
7898
+ }
7311
7899
  }
7312
7900
  }
7313
7901
  this.loaded = true;
@@ -7416,6 +8004,7 @@ var Scheduler = class {
7416
8004
  const taskId = stringOf(payload["taskId"]);
7417
8005
  if (taskId !== null && payload["to"] === "abandoned") {
7418
8006
  delete this.state.releaseCounts[taskId];
8007
+ await this.queueClosing(taskId);
7419
8008
  }
7420
8009
  if (taskId !== null) {
7421
8010
  await this.pruneStageWakes(taskId);
@@ -7520,6 +8109,13 @@ var Scheduler = class {
7520
8109
  }
7521
8110
  return;
7522
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
+ }
7523
8119
  case "proposal.decided": {
7524
8120
  const proposalId = stringOf(payload["proposalId"]);
7525
8121
  const proposer = stringOf(payload["proposedBy"]);
@@ -7755,13 +8351,16 @@ var Scheduler = class {
7755
8351
  reason: "a conversation you took part in has ended, and its workspace holds work on no branch"
7756
8352
  }, now, beside);
7757
8353
  }
7758
- /** A decided proposal wakes the citizen that made it, to carry on with what it enables. */
8354
+ /**
8355
+ * A decided proposal wakes the citizen that made it, to carry on with what it enables, in the
8356
+ * conversation it was filed from, where that work waits on the decision.
8357
+ */
7759
8358
  async wakeProposer(proposalId, proposer, event, now) {
7760
8359
  const agent = await this.tryReadAgent(proposer);
7761
8360
  if (agent === null) {
7762
8361
  return;
7763
8362
  }
7764
- const scope = this.scopeForProject(agent, null);
8363
+ const { scope, beside } = await this.filedFrom(agent, event.payload["filedFrom"]);
7765
8364
  const kind = stringOf(event.payload["kind"]) ?? "";
7766
8365
  const outcome = stringOf(event.payload["outcome"]) ?? "decided";
7767
8366
  this.enqueue(agent.name, scope, {
@@ -7769,7 +8368,27 @@ var Scheduler = class {
7769
8368
  from: event.actor,
7770
8369
  fromUser: event.actor === USER_NAME,
7771
8370
  reason: `your ${kind} proposal ${proposalId} was ${outcome} by ${event.actor}`
7772
- }, now);
8371
+ }, now, beside);
8372
+ }
8373
+ /**
8374
+ * The conversation a proposal was filed from while it is still open; its scope's home once it
8375
+ * has ended; and the home outside projects when the citizen has left that scope, or the proposal
8376
+ * names none, filed outside a turn or before proposals recorded it.
8377
+ */
8378
+ async filedFrom(agent, recorded) {
8379
+ const origin = ProposalOriginSchema.safeParse(recorded);
8380
+ if (!origin.success || origin.data.scope !== SOCIETY_SCOPE && !agent.memberships.includes(origin.data.scope)) {
8381
+ return { scope: this.scopeForProject(agent, null) };
8382
+ }
8383
+ const scope = origin.data.scope;
8384
+ const conversation = workspaceConversationOf(origin.data);
8385
+ if (conversation === null || await this.board.conversationEnded(conversation)) {
8386
+ return { scope };
8387
+ }
8388
+ return {
8389
+ scope,
8390
+ beside: conversation.kind === "thread" ? { thread: await this.conversationOf(conversation.id, scope) } : { channel: conversation.name }
8391
+ };
7773
8392
  }
7774
8393
  /**
7775
8394
  * A thread's conversation for a turn in `scope`, and whether it is a task's in that scope, whose
@@ -7828,6 +8447,7 @@ var Scheduler = class {
7828
8447
  const readyAt = now + debounce;
7829
8448
  const existing = this.pending.get(key);
7830
8449
  const message = messageOf(trigger);
8450
+ const cron = trigger.kind === "cron" ? trigger.cronId : void 0;
7831
8451
  if (existing === void 0) {
7832
8452
  this.pending.set(key, {
7833
8453
  dispatch: {
@@ -7836,24 +8456,30 @@ var Scheduler = class {
7836
8456
  ...besideFields(beside),
7837
8457
  trigger,
7838
8458
  priority: decision.priority,
7839
- onboarding: trigger.kind === "onboarding"
8459
+ onboarding: trigger.kind === "onboarding",
8460
+ ...cron === void 0 ? {} : { crons: [cron] }
7840
8461
  },
7841
8462
  readyAt,
7842
8463
  ...trigger.kind === "stage" ? { stageWakes: [trigger] } : {},
7843
8464
  ...message === void 0 ? {} : { newestMessage: message },
7844
- onlyMessages: message !== void 0
8465
+ onlyMessages: message !== void 0,
8466
+ onlyCrons: cron !== void 0
7845
8467
  });
7846
8468
  return;
7847
8469
  }
7848
8470
  const newestMessage = later(existing.newestMessage, message);
7849
8471
  const keepNew = decision.priority > existing.dispatch.priority;
7850
8472
  const priority = keepNew ? decision.priority : existing.dispatch.priority;
8473
+ const crons = [
8474
+ .../* @__PURE__ */ new Set([...existing.dispatch.crons ?? [], ...cron === void 0 ? [] : [cron]])
8475
+ ];
7851
8476
  this.pending.set(key, {
7852
8477
  dispatch: {
7853
8478
  ...existing.dispatch,
7854
8479
  trigger: keepNew ? trigger : existing.dispatch.trigger,
7855
8480
  priority,
7856
- onboarding: existing.dispatch.onboarding || trigger.kind === "onboarding"
8481
+ onboarding: existing.dispatch.onboarding || trigger.kind === "onboarding",
8482
+ ...crons.length === 0 ? {} : { crons }
7857
8483
  },
7858
8484
  readyAt: Math.min(existing.readyAt, readyAt),
7859
8485
  ...existing.stageWakes !== void 0 && trigger.kind === "stage" ? {
@@ -7862,7 +8488,8 @@ var Scheduler = class {
7862
8488
  } : {},
7863
8489
  ...newestMessage === void 0 ? {} : { newestMessage },
7864
8490
  onlyMessages: existing.onlyMessages === true && message !== void 0,
7865
- ...existing.steeredThrough === void 0 ? {} : { steeredThrough: existing.steeredThrough }
8491
+ ...existing.steeredThrough === void 0 ? {} : { steeredThrough: existing.steeredThrough },
8492
+ onlyCrons: existing.onlyCrons === true && cron !== void 0
7866
8493
  });
7867
8494
  }
7868
8495
  /** Debounce coalesces chatter: mentions, stage handoffs, and bursts of signals. Everything else runs at once. */
@@ -7972,6 +8599,71 @@ var Scheduler = class {
7972
8599
  }
7973
8600
  }
7974
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
+ }
7975
8667
  /**
7976
8668
  * Every `reflectionMs`, a member whose charter reflects takes a reflection turn in the scope of
7977
8669
  * its latest working turn, unless it has not worked since its last reflection.
@@ -8186,6 +8878,7 @@ var Scheduler = class {
8186
8878
  });
8187
8879
  }
8188
8880
  }
8881
+ const crons = (await this.board.listCrons()).filter((cron) => cron.ended === void 0);
8189
8882
  for (const agent of await this.board.listAgents()) {
8190
8883
  if (agent.status !== "active" || agent.cli === null) {
8191
8884
  continue;
@@ -8210,6 +8903,20 @@ var Scheduler = class {
8210
8903
  role: agent.role
8211
8904
  });
8212
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
+ }
8213
8920
  const left = (await this.board.listHomeConflicts(agent.name)).filter((conflict) => now - Date.parse(conflict.since) >= this.timings.homeConflictMs);
8214
8921
  if (left.length > 0) {
8215
8922
  const oldest = Math.max(...left.map((conflict) => now - Date.parse(conflict.since)));
@@ -8377,6 +9084,10 @@ var Scheduler = class {
8377
9084
  channel: dispatch.channel,
8378
9085
  trigger: dispatch.trigger.kind
8379
9086
  }, "dispatching turn");
9087
+ const crons = dispatch.crons ?? [];
9088
+ for (const id of crons) {
9089
+ this.cronsInFlight.add(id);
9090
+ }
8380
9091
  const promise = (async () => {
8381
9092
  try {
8382
9093
  const record = await this.runner.runTurn(dispatch, assignment);
@@ -8391,6 +9102,9 @@ var Scheduler = class {
8391
9102
  } finally {
8392
9103
  this.running.delete(key);
8393
9104
  this.exclusive.delete(key);
9105
+ for (const id of crons) {
9106
+ this.cronsInFlight.delete(id);
9107
+ }
8394
9108
  }
8395
9109
  })();
8396
9110
  this.running.set(key, promise);
@@ -8419,6 +9133,40 @@ var Scheduler = class {
8419
9133
  }
8420
9134
  return others.some((other) => this.exclusive.get(other) === "reflection");
8421
9135
  }
9136
+ /** Queues closing an abandoned task's pull request, when it is a `ghpr` task with one open. */
9137
+ async queueClosing(taskId) {
9138
+ if (this.runner.closePullRequest === void 0) {
9139
+ return;
9140
+ }
9141
+ try {
9142
+ const { task } = await this.board.findTask(taskId);
9143
+ if (awaitsClosing(task)) {
9144
+ this.closings.set(taskId, task.project);
9145
+ }
9146
+ } catch {
9147
+ }
9148
+ }
9149
+ /** Starts closing each queued pull request; one whose runner is away waits in the queue. */
9150
+ runClosings() {
9151
+ const close = this.runner.closePullRequest?.bind(this.runner);
9152
+ if (close === void 0) {
9153
+ return;
9154
+ }
9155
+ for (const [taskId, project] of this.closings) {
9156
+ this.closings.delete(taskId);
9157
+ const promise = close(project, taskId).then((outcome) => {
9158
+ if (outcome === "deferred" && !this.closings.has(taskId)) {
9159
+ this.closings.set(taskId, project);
9160
+ }
9161
+ return void 0;
9162
+ }).catch((error) => {
9163
+ this.log.error({ project, taskId, error: String(error) }, "closing a pull request failed");
9164
+ }).finally(() => {
9165
+ this.completing.delete(promise);
9166
+ });
9167
+ this.completing.add(promise);
9168
+ }
9169
+ }
8422
9170
  /** Starts each queued completion once the turn that finished the task's last stage has ended. */
8423
9171
  runCompletions() {
8424
9172
  for (const [taskId, item] of this.completions) {
@@ -8455,6 +9203,7 @@ var TURN_CONTRACT = [
8455
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.",
8456
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.",
8457
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.",
8458
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.",
8459
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.",
8460
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.",
@@ -8617,6 +9366,17 @@ function messageLines(messages, threads) {
8617
9366
  }
8618
9367
  return lines;
8619
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
+ }
8620
9380
  function buildSteerText(messages, threads) {
8621
9381
  return `${[
8622
9382
  `## New in this conversation while you work (${messages.length})`,
@@ -8637,6 +9397,10 @@ function buildTurnPrompt(input) {
8637
9397
  if (dispatch.trigger.taskId !== void 0) {
8638
9398
  lines.push(`Task in question: ${dispatch.trigger.taskId}`);
8639
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
+ }
8640
9404
  const askedIn = dispatch.trigger.channel === void 0 ? null : parseChannelRef(dispatch.trigger.channel).project;
8641
9405
  if (dispatch.project === SOCIETY_SCOPE && askedIn !== null) {
8642
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.`);
@@ -8651,7 +9415,10 @@ function buildTurnPrompt(input) {
8651
9415
  lines.push("A task you created is done. Tell whoever asked for it, if anyone did.");
8652
9416
  }
8653
9417
  if (dispatch.trigger.kind === "proposal_decided") {
8654
- lines.push("A proposal you made was decided; the decision is the last post in its thread. Carry on with what an approval enables or a rejection asks, and stay silent if nothing follows.");
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.");
9419
+ }
9420
+ if (dispatch.trigger.kind === "cron") {
9421
+ lines.push("A cron came due: what it asks is under Crons below.");
8655
9422
  }
8656
9423
  if (dispatch.trigger.kind === "user_post") {
8657
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.");
@@ -8679,6 +9446,11 @@ function buildTurnPrompt(input) {
8679
9446
  if (ending !== null) {
8680
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.");
8681
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
+ }
8682
9454
  lines.push("", WORKSPACE_TOKEN);
8683
9455
  const conflicts = input.conflicts ?? [];
8684
9456
  if (dispatch.trigger.kind === "reflection") {
@@ -8737,7 +9509,7 @@ function buildTurnPrompt(input) {
8737
9509
  }
8738
9510
  if (project !== void 0 && project !== null && landing === "ghpr") {
8739
9511
  const base = project.defaultBranch;
8740
- lines.push("", `## Project ${project.slug}`, "", `A finished task lands by the board merging its pull request on GitHub into ${base}. Open the pull request yourself, from a branch named for what it does that holds the same files as task/<id>, as \`git push origin task/<id>:<branch>\` gives, and link it to the task with update_task pull_request before its last stage completes, with the merge commit's merge_subject and merge_body, which you may change by linking again until it lands. Never merge the pull request or ${base} yourself, nor push other work to ${base}: the board merges once the task's stages are done, and the runner refuses the pushes.`);
9512
+ lines.push("", `## Project ${project.slug}`, "", `A finished task lands by the board merging its pull request on GitHub into ${base}. Open the pull request yourself, from a branch named for what it does that holds the same files as task/<id>, as \`git push origin task/<id>:<branch>\` gives, and link it to the task with update_task pull_request before its last stage completes, with the merge commit's merge_subject and merge_body, which you may change by linking again until it lands. Never merge the pull request or ${base} yourself, nor push other work to ${base}: the board merges once the task's stages are done, and the runner refuses the pushes. Abandoning the task closes its linked pull request and deletes its branch.`);
8741
9513
  }
8742
9514
  if (conflicts.length > 0) {
8743
9515
  lines.push("", "## Edits to reconcile in your home", "", "Two of your turns changed these files at once on different runners. The file in place kept the other turn's version; yours is the copy named here, beside it. Merge what still holds into the file, then delete the copy.", "", ...conflicts.map((conflict) => `- ${conflict.path}, beside ${conflict.file}, since ${conflict.since}`));
@@ -8861,7 +9633,8 @@ var TurnHost = class {
8861
9633
  usage: null,
8862
9634
  costUsd: 0,
8863
9635
  toolCalls: 0,
8864
- model: agent.model ?? null
9636
+ model: agent.model ?? null,
9637
+ ...dispatch.crons === void 0 ? {} : { crons: dispatch.crons }
8865
9638
  };
8866
9639
  if (agent.status === "retired") {
8867
9640
  return { failed: await this.fail(base, `${agent.name} is retired`) };
@@ -8948,8 +9721,18 @@ var TurnHost = class {
8948
9721
  ...threadRecord.state === "closed" ? { replyIn: await this.replyIn(threadRecord) } : {}
8949
9722
  };
8950
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
+ }
8951
9732
  const prompt = buildTurnPrompt({
8952
9733
  dispatch,
9734
+ clock: { now: this.now(), timeZone: society.timezone },
9735
+ crons,
8953
9736
  messages,
8954
9737
  threads,
8955
9738
  conversation,
@@ -9308,7 +10091,8 @@ var TurnHost = class {
9308
10091
  usage: null,
9309
10092
  costUsd: 0,
9310
10093
  toolCalls: 0,
9311
- model: agent?.model ?? null
10094
+ model: agent?.model ?? null,
10095
+ ...dispatch.crons === void 0 ? {} : { crons: dispatch.crons }
9312
10096
  }, reason);
9313
10097
  }
9314
10098
  /**
@@ -9895,6 +10679,49 @@ var RunnerHub = class {
9895
10679
  });
9896
10680
  return "done";
9897
10681
  }
10682
+ /**
10683
+ * Closes the pull request of a `ghpr` task abandoned with one linked, on the runner its project
10684
+ * lives on, whose `gh` login reaches GitHub, and records how it went on the task; `deferred`
10685
+ * while that runner is away.
10686
+ */
10687
+ async closePullRequest(project, taskId) {
10688
+ const { task } = await this.board.findTask(taskId);
10689
+ const pull = task.pullRequest;
10690
+ if (task.status !== "abandoned" || task.onDone !== "ghpr" || pull === void 0 || pull.closed !== void 0) {
10691
+ return "done";
10692
+ }
10693
+ const record = await this.board.readProject(project);
10694
+ let outcome = {
10695
+ ok: false,
10696
+ detail: `${project} lives on no runner yet, so none could close ${pull.url}`
10697
+ };
10698
+ if (record.runner !== void 0) {
10699
+ const seat = this.seats.get(record.runner);
10700
+ if (seat === void 0 || seat.send === null) {
10701
+ return "deferred";
10702
+ }
10703
+ try {
10704
+ outcome = MergeOutcomeSchema.parse(await this.request(seat, (request) => ({
10705
+ type: "close",
10706
+ request,
10707
+ close: {
10708
+ repo: projectRepo(record),
10709
+ url: pull.url,
10710
+ comment: `Closed by Stellaris: the task this pull request was opened for, "${task.title}", was abandoned.`
10711
+ }
10712
+ })));
10713
+ } catch (error) {
10714
+ this.log.warn({ project, taskId, error: String(error) }, "closing a pull request deferred");
10715
+ return "deferred";
10716
+ }
10717
+ }
10718
+ await this.board.recordPullRequestClosed(SYSTEM_ACTOR, {
10719
+ taskId,
10720
+ ok: outcome.ok,
10721
+ detail: outcome.detail
10722
+ });
10723
+ return "done";
10724
+ }
9898
10725
  /**
9899
10726
  * One path on a branch of a project's repository, read on the runner the project lives on.
9900
10727
  * NOT_FOUND when the project has no runner yet or the branch or the path does not exist, and
@@ -10171,20 +10998,20 @@ async function handleMcpRequest(board2, actor, request, version) {
10171
10998
  import { Hono as Hono3 } from "hono";
10172
10999
  import { compress } from "hono/compress";
10173
11000
  import { streamSSE as streamSSE2 } from "hono/streaming";
10174
- import { z as z15, ZodError } from "zod";
11001
+ import { z as z16, ZodError } from "zod";
10175
11002
 
10176
11003
  // apps/server/dist/github-auth.js
10177
11004
  import { randomBytes as randomBytes2 } from "node:crypto";
10178
11005
  import { Hono } from "hono";
10179
- import { z as z14 } from "zod";
11006
+ import { z as z15 } from "zod";
10180
11007
  var SIGN_IN_TTL_MS = 30 * 24 * 36e5;
10181
11008
  var STATE_TTL_MS = 10 * 6e4;
10182
11009
  var MAX_STATES = 1e3;
10183
- var TokenAnswerSchema = z14.object({ access_token: z14.string().min(1) });
10184
- var GithubUserSchema = z14.object({
10185
- login: z14.string().min(1),
10186
- id: z14.number().int(),
10187
- 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()
10188
11015
  });
10189
11016
  function githubAvatar(url) {
10190
11017
  try {
@@ -10658,17 +11485,18 @@ var ERROR_STATUS = {
10658
11485
  INVALID_TRANSITION: 409,
10659
11486
  INVALID_STATE: 409
10660
11487
  };
10661
- var RetireBodySchema = z15.object({ reason: z15.string().min(1) });
10662
- var RunnerBodySchema = z15.object({ name: NameSchema });
10663
- var EnrollmentApprovalSchema = z15.object({ name: NameSchema });
10664
- var HomeRunnerBodySchema = z15.object({ runner: NameSchema.nullable() });
10665
- var ModelBodySchema = z15.object({ model: ModelNameSchema.nullable() });
10666
- var EffortBodySchema = z15.object({ effort: EffortSchema.nullable() });
10667
- var HistoryLimitSchema = z15.coerce.number().int().min(1).max(500);
10668
- 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({
10669
11497
  project: NameSchema.nullable().default(null),
10670
11498
  name: NameSchema,
10671
- purpose: z15.string().min(1)
11499
+ purpose: z16.string().min(1)
10672
11500
  });
10673
11501
  function bearer2(c) {
10674
11502
  const header = c.req.header("authorization") ?? "";
@@ -10717,6 +11545,10 @@ function createApp(deps) {
10717
11545
  });
10718
11546
  api.delete("/sign-in", async (c) => c.json({ signedOut: await board2.signOut(bearer2(c) ?? "") }));
10719
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
+ });
10720
11552
  api.get("/version", (c) => c.json({ version }));
10721
11553
  api.get("/agents", async (c) => c.json((await board2.listAgents()).map(({ tokenHash: _hash, ...agent }) => agent)));
10722
11554
  api.get("/members", async (c) => c.json(await board2.listMembers()));
@@ -10768,6 +11600,7 @@ function createApp(deps) {
10768
11600
  });
10769
11601
  }
10770
11602
  api.get("/proposals", async (c) => c.json(await board2.listProposals()));
11603
+ api.get("/crons", async (c) => c.json(await board2.listCrons()));
10771
11604
  api.get("/proposals/:id", async (c) => c.json(await board2.readProposal(c.req.param("id"))));
10772
11605
  api.get("/signals", async (c) => c.json(await board2.listSignals(Number(c.req.query("limit") ?? "100"))));
10773
11606
  api.get("/metrics", async (c) => {
@@ -10817,7 +11650,7 @@ function createApp(deps) {
10817
11650
  });
10818
11651
  api.put("/roles/:name", async (c) => {
10819
11652
  const charter = RoleCharterSchema.parse({
10820
- ...z15.record(z15.string(), z15.unknown()).parse(await c.req.json()),
11653
+ ...z16.record(z16.string(), z16.unknown()).parse(await c.req.json()),
10821
11654
  name: c.req.param("name")
10822
11655
  });
10823
11656
  return c.json(await board2.setRoleCharter(c.get("actor"), charter));