@kubitnodes/stellaris 0.3.20 → 0.3.22

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 (75) hide show
  1. package/dist/cli/index.js +1029 -637
  2. package/dist/server/index.js +1313 -729
  3. package/package.json +3 -1
  4. package/web/dist/assets/{BackLink-BjfX2KZl.js → BackLink-CD-iuy-h.js} +1 -1
  5. package/web/dist/assets/Button-Yoe21NQ2.js +1 -0
  6. package/web/dist/assets/ChannelForms-55otlg-m.js +1 -0
  7. package/web/dist/assets/ChannelView-DJvLzg0N.js +2 -0
  8. package/web/dist/assets/CitizenView-m1K2aTvK.js +2 -0
  9. package/web/dist/assets/CitizensView-BeTgmrKE.js +1 -0
  10. package/web/dist/assets/{CliIcon-vSuwLv2K.js → CliIcon-DbMYJdlU.js} +1 -1
  11. package/web/dist/assets/CronsView-B4kXk5Ld.js +1 -0
  12. package/web/dist/assets/KnowledgeView-aSGxDDKy.js +1 -0
  13. package/web/dist/assets/Markdown-c9bCCLHg.js +30 -0
  14. package/web/dist/assets/{MessageItem-D7MUCA_T.js → MessageItem-DKJKMqs-.js} +1 -1
  15. package/web/dist/assets/MetricsView-D2TTry5O.js +1 -0
  16. package/web/dist/assets/NeedsYouView-C3celRei.js +1 -0
  17. package/web/dist/assets/Overview-BveU1yJj.js +1 -0
  18. package/web/dist/assets/{Pane-_D7n5uKH.js → Pane-CLjdfBMM.js} +1 -1
  19. package/web/dist/assets/{Picker-DUiiQMFN.js → Picker-CXOe5Xdu.js} +1 -1
  20. package/web/dist/assets/ProjectView-oa4Z46ED.js +1 -0
  21. package/web/dist/assets/ProposalView-B4T-EoX9.js +2 -0
  22. package/web/dist/assets/ProposalsView-BZHivSEm.js +1 -0
  23. package/web/dist/assets/Quota-BC7dpnwd.js +1 -0
  24. package/web/dist/assets/RunnersView-BrJF-aLU.js +1 -0
  25. package/web/dist/assets/SocietyView-C1yosPuf.js +1 -0
  26. package/web/dist/assets/StickyList-7tAlUDCN.js +1 -0
  27. package/web/dist/assets/SubjectThread-CCq6ZAnR.js +1 -0
  28. package/web/dist/assets/TaskFileView-BEXQJQQ6.js +4 -0
  29. package/web/dist/assets/TaskView-Cfhb7NuO.js +3 -0
  30. package/web/dist/assets/TasksView-3XljXcoi.js +1 -0
  31. package/web/dist/assets/ThreadForms-D2y5dXSC.js +1 -0
  32. package/web/dist/assets/ThreadItem-DeIhhmyu.js +1 -0
  33. package/web/dist/assets/ThreadView-CbUnt9WL.js +1 -0
  34. package/web/dist/assets/citizen--bqux6i_.js +1 -0
  35. package/web/dist/assets/compose-DNc3_c5x.js +1 -0
  36. package/web/dist/assets/format-B6fjC0_a.js +55 -0
  37. package/web/dist/assets/governance-RBQ4Lam0.js +1 -0
  38. package/web/dist/assets/index-CyCty7a2.js +23 -0
  39. package/web/dist/assets/index-GRqxKN35.css +2 -0
  40. package/web/dist/assets/mentions-B2Qchd_6.js +3 -0
  41. package/web/dist/assets/{tasks-B6I9DJ8O.js → tasks-DCUj5w_d.js} +1 -1
  42. package/web/dist/assets/useMatch-CfbBQirn.js +1 -0
  43. package/web/dist/index.html +9 -10
  44. package/web/dist/assets/ChannelForms-BYrcUldk.js +0 -1
  45. package/web/dist/assets/ChannelView-GJ_DusLQ.js +0 -2
  46. package/web/dist/assets/CitizenView-Cx7IYSRr.js +0 -2
  47. package/web/dist/assets/CitizensView-IKH7_yNQ.js +0 -1
  48. package/web/dist/assets/CronsView-DJbi8Qhx.js +0 -1
  49. package/web/dist/assets/KnowledgeView-CoYOG9tI.js +0 -1
  50. package/web/dist/assets/Markdown-YC-fbHiE.js +0 -32
  51. package/web/dist/assets/MetricsView-CE94VW0I.js +0 -1
  52. package/web/dist/assets/NeedsYouView-Otnm3lcG.js +0 -1
  53. package/web/dist/assets/Overview-B8f6rLb2.js +0 -1
  54. package/web/dist/assets/ProjectView-D_ACOmEd.js +0 -1
  55. package/web/dist/assets/ProposalView-4d0U3nEb.js +0 -2
  56. package/web/dist/assets/ProposalsView-BovF6CMD.js +0 -1
  57. package/web/dist/assets/RunnersView-B2_uCVHC.js +0 -1
  58. package/web/dist/assets/SocietyView-Bf0iX8AY.js +0 -1
  59. package/web/dist/assets/SubjectThread-B_Vmgs3d.js +0 -1
  60. package/web/dist/assets/TaskFileView-BzO0gjE6.js +0 -4
  61. package/web/dist/assets/TaskView-DB_Nji99.js +0 -3
  62. package/web/dist/assets/TasksView-Bz36YYWq.js +0 -1
  63. package/web/dist/assets/ThreadForms-1mbMZsbv.js +0 -1
  64. package/web/dist/assets/ThreadItem-QJSA92C4.js +0 -1
  65. package/web/dist/assets/ThreadView-BECLqt5B.js +0 -1
  66. package/web/dist/assets/citizen-DVFr5T6R.js +0 -1
  67. package/web/dist/assets/compose-_6feqP1t.js +0 -1
  68. package/web/dist/assets/format-DxXtbdNM.js +0 -55
  69. package/web/dist/assets/governance-C9xH4QTm.js +0 -1
  70. package/web/dist/assets/index-BEVCPm4C.js +0 -23
  71. package/web/dist/assets/index-BkOq4PSU.css +0 -2
  72. package/web/dist/assets/link-CTjFMNoY.js +0 -1
  73. package/web/dist/assets/mentions-BCdV0bMK.js +0 -1
  74. package/web/dist/assets/useMatch-B3RzAe8y.js +0 -1
  75. package/web/dist/assets/useStickyScroll-Ca7sWbGH.js +0 -1
@@ -9,7 +9,7 @@ import { serve } from "@hono/node-server";
9
9
  import { chmod as chmod2, cp, mkdir as mkdir4, readdir as readdir3, readFile as readFile4, rename as rename3, rm as rm2, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
10
10
  import path6 from "node:path";
11
11
  import { decodeTime, monotonicFactory } from "ulid";
12
- import { z as z13 } from "zod";
12
+ import { z as z15 } from "zod";
13
13
 
14
14
  // packages/shared/dist/ids.js
15
15
  import { z } from "zod";
@@ -146,7 +146,7 @@ var SEED_ROLES = [
146
146
  },
147
147
  {
148
148
  name: "steward",
149
- purpose: "Watches the operations signals and the task board for capacity, skill, and capability gaps, and turns signals into proposals: a member when a project's plans need a role nobody fills or a backlog persists, a role when a project's work needs one no existing role covers, a retirement when a member has been idle for a long time. Opens a channel with create_channel when a topic or a workstream needs one, and archives it once its work is done. Owns how work is shaped over time: answers stages waiting too long, stages assigned to roles nobody fills, and work sent back again and again by replanning with plan_task, adjusting gates, or proposing the role a plan needs; sets each project's completion effect with configure_project; turns recurring plan shapes into society skills and planning norms into the society's norms. Prefers scaling an existing role over inventing a new one. Never approves its own proposals; hiring, roles, and retirements are decided by the user, and channels, reallocations, and skills the steward may decide. Curates society knowledge and skills.",
149
+ purpose: "Watches the operations signals and the task board for capacity, skill, and capability gaps, and turns signals into proposals: a member when a project's plans need a role nobody fills or a backlog persists, a role when a project's work needs one no existing role covers, a retirement when a member has been idle for a long time. Opens a channel with create_channel when a topic or a workstream needs one, and archives it once its work is done. Owns how work is shaped over time: answers stages waiting too long, stages assigned to roles nobody fills, and work sent back again and again by replanning with plan_task, adjusting gates, or proposing the role a plan needs; sets each project's completion effect with configure_project; turns recurring plan shapes into society skills and planning norms into the society's norms. Prefers scaling an existing role over inventing a new one, by a reallocation of its replicas, backlog threshold, wakes, or verbs. Never approves its own proposals; hiring, roles, retirements, and reallocations that change a role's verbs or replicas are decided by the user, and channels, skills, and other reallocations the steward may decide. Curates society knowledge and skills.",
150
150
  verbs: [...MEMBER_VERBS, ...GOVERNANCE_VERBS, ...PLANNING_VERBS, ...CURATING_VERBS],
151
151
  wakeTriggers: ["ops_event", "heartbeat"],
152
152
  maxReplicas: 1,
@@ -169,7 +169,7 @@ var SEED_ROLES = [
169
169
  ];
170
170
 
171
171
  // packages/shared/dist/board.js
172
- import { z as z4 } from "zod";
172
+ import { z as z5 } from "zod";
173
173
 
174
174
  // packages/shared/dist/crons.js
175
175
  import { Cron as Pattern } from "croner";
@@ -303,6 +303,115 @@ function localTime(date, timeZone) {
303
303
  return `${part("weekday")} ${part("year")}-${part("month")}-${part("day")} ${part("hour")}:${part("minute")}`;
304
304
  }
305
305
 
306
+ // packages/shared/dist/quota.js
307
+ import { z as z4 } from "zod";
308
+ var QUOTA_WARNING_PERCENT = 80;
309
+ var QuotaWindowSchema = z4.object({
310
+ /**
311
+ * The CLI's own name for the window: Claude's `five_hour`, `seven_day`, `seven_day_opus`, and
312
+ * `model:<label>` for a model's weekly window; Codex's `primary` and `secondary`.
313
+ */
314
+ key: z4.string().min(1).max(80),
315
+ /** What a scoped window counts, as the CLI labels it, such as a model's name. */
316
+ label: z4.string().min(1).max(80).optional(),
317
+ /** The window's length in minutes, when known. */
318
+ minutes: z4.number().int().positive().nullable().default(null),
319
+ /** How much of the window is used, in percent, or null when the CLI did not say. */
320
+ usedPercent: z4.number().nonnegative().nullable().default(null),
321
+ /** When the window starts afresh, when known. */
322
+ resetsAt: IsoDateTimeSchema.nullable().default(null),
323
+ /** When the CLI last reported this window. */
324
+ observedAt: IsoDateTimeSchema
325
+ });
326
+ var QuotaStatusSchema = z4.enum(["allowed", "warning", "limited", "none"]);
327
+ var QuotaCreditsSchema = z4.object({
328
+ hasCredits: z4.boolean(),
329
+ unlimited: z4.boolean(),
330
+ balance: z4.string().max(40).nullable()
331
+ });
332
+ var QuotaExtraUsageSchema = z4.object({
333
+ enabled: z4.boolean(),
334
+ monthlyLimit: z4.number().nonnegative().nullable(),
335
+ usedCredits: z4.number().nonnegative().nullable(),
336
+ currency: z4.string().min(1).max(8).nullable()
337
+ });
338
+ var CliQuotaSchema = z4.object({
339
+ /** When the CLI last reported anything here. */
340
+ observedAt: IsoDateTimeSchema,
341
+ /** Whether it came from a turn's stream or from asking the CLI. */
342
+ source: z4.enum(["turn", "read"]),
343
+ status: QuotaStatusSchema,
344
+ /** While `limited`: when work can go on, when known, and the window that stopped it. */
345
+ limitedUntil: IsoDateTimeSchema.nullable().default(null),
346
+ limitedBy: z4.string().min(1).max(80).nullable().default(null),
347
+ /** The plan's name as the CLI gives it, such as `max` or `pro`. */
348
+ plan: z4.string().min(1).max(40).nullable().default(null),
349
+ windows: z4.array(QuotaWindowSchema).max(24).default([]),
350
+ credits: QuotaCreditsSchema.nullable().default(null),
351
+ extraUsage: QuotaExtraUsageSchema.nullable().default(null)
352
+ });
353
+ function mergeQuota(previous, next) {
354
+ if (previous === void 0) {
355
+ return next;
356
+ }
357
+ const windows = new Map(previous.windows.map((window) => [window.key, window]));
358
+ for (const window of next.windows) {
359
+ windows.set(window.key, window);
360
+ }
361
+ return {
362
+ ...next,
363
+ plan: next.plan ?? previous.plan,
364
+ credits: next.credits ?? previous.credits,
365
+ extraUsage: next.extraUsage ?? previous.extraUsage,
366
+ windows: [...windows.values()]
367
+ };
368
+ }
369
+ function windowCurrent(window, now) {
370
+ return window.resetsAt === null || Date.parse(window.resetsAt) > now.getTime();
371
+ }
372
+ function quotaLevel(quota, now) {
373
+ if (quota.status === "none") {
374
+ return "none";
375
+ }
376
+ if (quota.status === "limited" && (quota.limitedUntil === null || Date.parse(quota.limitedUntil) > now.getTime())) {
377
+ return "limited";
378
+ }
379
+ const high = quota.windows.some((window) => windowCurrent(window, now) && window.usedPercent !== null && window.usedPercent >= QUOTA_WARNING_PERCENT);
380
+ return quota.status === "warning" || high ? "warning" : "ok";
381
+ }
382
+ var WINDOW_NAMES = {
383
+ five_hour: "5h",
384
+ seven_day: "7d",
385
+ seven_day_opus: "7d Opus",
386
+ seven_day_sonnet: "7d Sonnet",
387
+ seven_day_oauth_apps: "7d apps",
388
+ seven_day_overage_included: "7d extra",
389
+ overage: "extra usage"
390
+ };
391
+ function windowName(window) {
392
+ if (window.label !== void 0) {
393
+ return window.minutes === 10080 ? `7d ${window.label}` : window.label;
394
+ }
395
+ const named = WINDOW_NAMES[window.key];
396
+ if (named !== void 0) {
397
+ return named;
398
+ }
399
+ if (window.minutes !== null) {
400
+ return window.minutes % 1440 === 0 ? `${window.minutes / 1440}d` : window.minutes % 60 === 0 ? `${window.minutes / 60}h` : `${window.minutes}m`;
401
+ }
402
+ return window.key;
403
+ }
404
+ function readings(quota) {
405
+ const windows = quota.windows.map((window) => `${window.key}=${window.usedPercent === null ? "?" : Math.round(window.usedPercent)}@${window.resetsAt === null ? "?" : Math.round(Date.parse(window.resetsAt) / 6e4)}`).toSorted();
406
+ return windows.join(",");
407
+ }
408
+ function quotaChanged(previous, next) {
409
+ if (previous === void 0) {
410
+ return true;
411
+ }
412
+ return previous.status !== next.status || previous.limitedUntil !== next.limitedUntil || previous.plan !== next.plan || previous.credits?.balance !== next.credits?.balance || previous.extraUsage?.usedCredits !== next.extraUsage?.usedCredits || readings(previous) !== readings(next);
413
+ }
414
+
306
415
  // packages/shared/dist/board.js
307
416
  var ASK_CHANNEL = "asks";
308
417
  var SOCIETY_CHANNELS = ["general", "governance", ASK_CHANNEL];
@@ -315,24 +424,24 @@ var SOCIETY_SCOPE = "society";
315
424
  function wakeScope(member, project) {
316
425
  return project !== null && member.memberships.includes(project) ? project : SOCIETY_SCOPE;
317
426
  }
318
- var CliKindSchema = z4.enum(["claude", "codex"]);
319
- var ArchivedChannelSchema = z4.object({
427
+ var CliKindSchema = z5.enum(["claude", "codex"]);
428
+ var ArchivedChannelSchema = z5.object({
320
429
  name: NameSchema,
321
430
  at: IsoDateTimeSchema,
322
431
  by: NameSchema,
323
- reason: z4.string().min(1)
432
+ reason: z5.string().min(1)
324
433
  });
325
- var SocietySchema = z4.object({
326
- name: z4.string().min(1),
327
- version: z4.literal(1),
434
+ var SocietySchema = z5.object({
435
+ name: z5.string().min(1),
436
+ version: z5.literal(1),
328
437
  createdAt: IsoDateTimeSchema,
329
438
  /** The channels that take posts; an archived one moves to `archivedChannels`. */
330
- channels: z4.array(NameSchema),
331
- archivedChannels: z4.array(ArchivedChannelSchema).default([]),
439
+ channels: z5.array(NameSchema),
440
+ archivedChannels: z5.array(ArchivedChannelSchema).default([]),
332
441
  /** The time zone crons are read in when they name none, and every prompt tells the time in; the user's to set. */
333
442
  timezone: TimeZoneSchema.default(DEFAULT_TIME_ZONE)
334
443
  });
335
- var CompletionEffectSchema = z4.enum(["none", "merge", "ghpr"]);
444
+ var CompletionEffectSchema = z5.enum(["none", "merge", "ghpr"]);
336
445
  var GITHUB_REMOTE = [
337
446
  /^https:\/\/github\.com\/([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?$/,
338
447
  /^git@github\.com:([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?$/,
@@ -348,7 +457,7 @@ function githubRepository(remote) {
348
457
  return null;
349
458
  }
350
459
  var PULL_REQUEST_URL = /^https:\/\/github\.com\/([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+)\/pull\/([1-9]\d*)\/?$/;
351
- var PullRequestUrlSchema = z4.string().trim().regex(PULL_REQUEST_URL, "a pull request's address, https://github.com/<owner>/<repo>/pull/<number>");
460
+ var PullRequestUrlSchema = z5.string().trim().regex(PULL_REQUEST_URL, "a pull request's address, https://github.com/<owner>/<repo>/pull/<number>");
352
461
  function parsePullRequestUrl(url) {
353
462
  const match = PULL_REQUEST_URL.exec(url.trim());
354
463
  return match?.[1] === void 0 || match[2] === void 0 || match[3] === void 0 ? null : { owner: match[1], name: match[2], number: Number(match[3]) };
@@ -356,72 +465,72 @@ function parsePullRequestUrl(url) {
356
465
  function sameRepository(a, b) {
357
466
  return a.owner.toLowerCase() === b.owner.toLowerCase() && a.name.toLowerCase() === b.name.toLowerCase();
358
467
  }
359
- var MergeSubjectSchema = z4.string().trim().min(1).max(300).regex(/^[^\n]+$/, "a merge commit's subject is one line");
360
- var PullRequestLinkSchema = z4.object({
468
+ var MergeSubjectSchema = z5.string().trim().min(1).max(300).regex(/^[^\n]+$/, "a merge commit's subject is one line");
469
+ var PullRequestLinkSchema = z5.object({
361
470
  url: PullRequestUrlSchema,
362
- number: z4.number().int().positive(),
471
+ number: z5.number().int().positive(),
363
472
  subject: MergeSubjectSchema.optional(),
364
- body: z4.string().max(2e4).optional(),
473
+ body: z5.string().max(2e4).optional(),
365
474
  linkedBy: NameSchema,
366
475
  linkedAt: IsoDateTimeSchema,
367
476
  /**
368
477
  * Once its `ghpr` task was abandoned, how the board's closing of it on GitHub went: `ok` false
369
478
  * leaves it open, for the reason `detail` gives.
370
479
  */
371
- closed: z4.object({ at: IsoDateTimeSchema, ok: z4.boolean(), detail: z4.string() }).optional()
480
+ closed: z5.object({ at: IsoDateTimeSchema, ok: z5.boolean(), detail: z5.string() }).optional()
372
481
  });
373
- 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");
374
- var StageIdSchema = z4.string().regex(/^s[1-9][0-9]*$/, "expected a stage id such as s1");
482
+ var DefaultBranchSchema = z5.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");
483
+ var StageIdSchema = z5.string().regex(/^s[1-9][0-9]*$/, "expected a stage id such as s1");
375
484
  var stageFields = {
376
- name: z4.string().trim().min(1).max(120),
485
+ name: z5.string().trim().min(1).max(120),
377
486
  role: NameSchema.optional(),
378
487
  agent: NameSchema.optional(),
379
- gate: z4.boolean().default(false)
488
+ gate: z5.boolean().default(false)
380
489
  };
381
490
  function oneAssignee(stage) {
382
491
  return stage.role === void 0 || stage.agent === void 0;
383
492
  }
384
493
  var ONE_ASSIGNEE = { message: "a stage names a role or an agent, not both" };
385
- var PlanStageSchema = z4.object(stageFields).refine(oneAssignee, ONE_ASSIGNEE);
386
- var PlanEditStageSchema = z4.object({ id: StageIdSchema.optional(), ...stageFields }).refine(oneAssignee, ONE_ASSIGNEE);
387
- var PlanSchema = z4.array(PlanStageSchema);
388
- var PlanEditSchema = z4.array(PlanEditStageSchema);
389
- var ProjectSchema = z4.object({
494
+ var PlanStageSchema = z5.object(stageFields).refine(oneAssignee, ONE_ASSIGNEE);
495
+ var PlanEditStageSchema = z5.object({ id: StageIdSchema.optional(), ...stageFields }).refine(oneAssignee, ONE_ASSIGNEE);
496
+ var PlanSchema = z5.array(PlanStageSchema);
497
+ var PlanEditSchema = z5.array(PlanEditStageSchema);
498
+ var ProjectSchema = z5.object({
390
499
  slug: NameSchema,
391
- name: z4.string().min(1),
392
- repo: z4.string().nullable(),
393
- defaultBranch: z4.string().min(1),
500
+ name: z5.string().min(1),
501
+ repo: z5.string().nullable(),
502
+ defaultBranch: z5.string().min(1),
394
503
  /** The channels that take posts; an archived one moves to `archivedChannels`. */
395
- channels: z4.array(NameSchema),
396
- archivedChannels: z4.array(ArchivedChannelSchema).default([]),
397
- members: z4.array(NameSchema),
398
- approvers: z4.array(NameSchema),
399
- requiredCapabilities: z4.array(z4.string()),
504
+ channels: z5.array(NameSchema),
505
+ archivedChannels: z5.array(ArchivedChannelSchema).default([]),
506
+ members: z5.array(NameSchema),
507
+ approvers: z5.array(NameSchema),
508
+ requiredCapabilities: z5.array(z5.string()),
400
509
  createdAt: IsoDateTimeSchema,
401
510
  onDone: CompletionEffectSchema.default("none"),
402
511
  /** The runner the project lives on, which holds its repository; set on the project's first turn. */
403
512
  runner: NameSchema.optional(),
404
513
  /** Set once the project is archived: nobody belongs to it, and nothing more is posted, filed, or joined there. */
405
- archived: z4.object({ at: IsoDateTimeSchema, by: NameSchema, reason: z4.string().min(1) }).optional()
514
+ archived: z5.object({ at: IsoDateTimeSchema, by: NameSchema, reason: z5.string().min(1) }).optional()
406
515
  });
407
- var TaskStepSchema = z4.object({
408
- action: z4.enum(["advanced", "returned", "abandoned", "landed", "reopened"]),
516
+ var TaskStepSchema = z5.object({
517
+ action: z5.enum(["advanced", "returned", "abandoned", "landed", "reopened"]),
409
518
  stage: StageIdSchema,
410
519
  to: StageIdSchema.nullable()
411
520
  });
412
- var ProposalStepSchema = z4.object({ action: z4.enum(["approved", "rejected"]) });
413
- var MessageStepSchema = z4.union([TaskStepSchema, ProposalStepSchema]);
521
+ var ProposalStepSchema = z5.object({ action: z5.enum(["approved", "rejected"]) });
522
+ var MessageStepSchema = z5.union([TaskStepSchema, ProposalStepSchema]);
414
523
  function isTaskStep(step) {
415
524
  return "stage" in step;
416
525
  }
417
- var KnowledgeNoticeSchema = z4.object({
418
- kind: z4.literal("knowledge"),
419
- action: z4.enum(["written", "updated", "removed"]),
526
+ var KnowledgeNoticeSchema = z5.object({
527
+ kind: z5.literal("knowledge"),
528
+ action: z5.enum(["written", "updated", "removed"]),
420
529
  topic: NameSchema,
421
530
  project: NameSchema.nullable()
422
531
  });
423
- var MessageNoticeSchema = z4.discriminatedUnion("kind", [KnowledgeNoticeSchema]);
424
- var MessageFrontmatterSchema = z4.object({
532
+ var MessageNoticeSchema = z5.discriminatedUnion("kind", [KnowledgeNoticeSchema]);
533
+ var MessageFrontmatterSchema = z5.object({
425
534
  id: UlidSchema,
426
535
  author: NameSchema,
427
536
  channel: ChannelRefSchema,
@@ -435,7 +544,7 @@ var MessageFrontmatterSchema = z4.object({
435
544
  /** On a post a verb made about its work, such as a knowledge topic's note: what it records. */
436
545
  notice: MessageNoticeSchema.optional(),
437
546
  ts: IsoDateTimeSchema,
438
- mentions: z4.array(NameSchema)
547
+ mentions: z5.array(NameSchema)
439
548
  });
440
549
  var MessageRefSchema = MessageFrontmatterSchema.pick({
441
550
  id: true,
@@ -445,16 +554,16 @@ var MessageRefSchema = MessageFrontmatterSchema.pick({
445
554
  ts: true
446
555
  });
447
556
  var MESSAGE_LOOKUP_LIMIT = 50;
448
- var TaskStatusSchema = z4.enum(["open", "claimed", "done", "abandoned"]);
449
- var ThreadStateSchema = z4.enum(["open", "closed"]);
450
- var ThreadSubjectSchema = z4.object({
451
- kind: z4.enum(["task", "proposal"]),
557
+ var TaskStatusSchema = z5.enum(["open", "claimed", "done", "abandoned"]);
558
+ var ThreadStateSchema = z5.enum(["open", "closed"]);
559
+ var ThreadSubjectSchema = z5.object({
560
+ kind: z5.enum(["task", "proposal"]),
452
561
  id: UlidSchema
453
562
  });
454
- var ThreadFrontmatterSchema = z4.object({
563
+ var ThreadFrontmatterSchema = z5.object({
455
564
  id: UlidSchema,
456
565
  channel: ChannelRefSchema,
457
- title: z4.string().min(1).max(200),
566
+ title: z5.string().min(1).max(200),
458
567
  subject: ThreadSubjectSchema.optional(),
459
568
  state: ThreadStateSchema,
460
569
  openedBy: NameSchema,
@@ -462,28 +571,28 @@ var ThreadFrontmatterSchema = z4.object({
462
571
  closedBy: NameSchema.optional(),
463
572
  closedAt: IsoDateTimeSchema.optional()
464
573
  });
465
- var StageSchema = z4.object({
574
+ var StageSchema = z5.object({
466
575
  id: StageIdSchema,
467
- name: z4.string().min(1),
576
+ name: z5.string().min(1),
468
577
  role: NameSchema.optional(),
469
578
  agent: NameSchema.optional(),
470
- gate: z4.boolean().default(false),
579
+ gate: z5.boolean().default(false),
471
580
  /** Everyone who has held the stage, oldest first. */
472
- holders: z4.array(NameSchema).default([]),
581
+ holders: z5.array(NameSchema).default([]),
473
582
  completedBy: NameSchema.optional(),
474
583
  completedAt: IsoDateTimeSchema.optional()
475
584
  });
476
- var TaskReturnSchema = z4.object({
585
+ var TaskReturnSchema = z5.object({
477
586
  from: StageIdSchema,
478
587
  by: NameSchema,
479
588
  at: IsoDateTimeSchema
480
589
  });
481
- var TaskFrontmatterSchema = z4.object({
590
+ var TaskFrontmatterSchema = z5.object({
482
591
  id: UlidSchema,
483
592
  project: NameSchema,
484
593
  /** The channel of the project the task is filed in, where its thread hangs. */
485
594
  channel: NameSchema.default("general"),
486
- title: z4.string().min(1),
595
+ title: z5.string().min(1),
487
596
  status: TaskStatusSchema,
488
597
  createdBy: NameSchema,
489
598
  createdAt: IsoDateTimeSchema,
@@ -492,19 +601,19 @@ var TaskFrontmatterSchema = z4.object({
492
601
  claimedBy: NameSchema.optional(),
493
602
  leaseExpiresAt: IsoDateTimeSchema.optional(),
494
603
  parentId: UlidSchema.optional(),
495
- blockedBy: z4.array(UlidSchema),
496
- requiredCapabilities: z4.array(z4.string()),
497
- stages: z4.array(StageSchema).min(1),
604
+ blockedBy: z5.array(UlidSchema),
605
+ requiredCapabilities: z5.array(z5.string()),
606
+ stages: z5.array(StageSchema).min(1),
498
607
  stage: StageIdSchema,
499
608
  /** When the current stage last became current or lost its holder: the waiting clock. */
500
609
  stageSince: IsoDateTimeSchema,
501
610
  /** The highest stage number issued, so a removed stage's id is never reused. */
502
- stageSeq: z4.number().int().positive(),
611
+ stageSeq: z5.number().int().positive(),
503
612
  onDone: CompletionEffectSchema.default("none"),
504
613
  /** The pull request a `ghpr` task lands through. */
505
614
  pullRequest: PullRequestLinkSchema.optional(),
506
615
  /** The last stage is complete and the completion effect is running. */
507
- completing: z4.boolean().default(false),
616
+ completing: z5.boolean().default(false),
508
617
  /** The latest send-back while its rework lasts: cleared once the task reaches that stage again. */
509
618
  returned: TaskReturnSchema.optional()
510
619
  });
@@ -528,79 +637,90 @@ function mayHoldStage(member, task) {
528
637
  }
529
638
  return !(stage.gate && task.stages.slice(0, index).some((s) => s.holders.includes(member.name)));
530
639
  }
531
- var ModelNameSchema = z4.string().trim().min(1).max(120).regex(/^[\w.:/[\]-]+$/, "a model name is one word of letters, digits, and . _ : / - [ ]");
532
- var EffortSchema = z4.string().trim().min(1).max(32).regex(/^[a-z][a-z0-9_-]*$/, "an effort is one lowercase word, such as high");
533
- var EffortOptionSchema = z4.object({
640
+ var ModelNameSchema = z5.string().trim().min(1).max(120).regex(/^[\w.:/[\]-]+$/, "a model name is one word of letters, digits, and . _ : / - [ ]");
641
+ var EffortSchema = z5.string().trim().min(1).max(32).regex(/^[a-z][a-z0-9_-]*$/, "an effort is one lowercase word, such as high");
642
+ var EffortOptionSchema = z5.object({
534
643
  id: EffortSchema,
535
- description: z4.string().default("")
644
+ description: z5.string().default("")
536
645
  });
537
- var ModelOptionSchema = z4.object({
646
+ var ModelOptionSchema = z5.object({
538
647
  id: ModelNameSchema,
539
- name: z4.string(),
540
- description: z4.string().default(""),
541
- isDefault: z4.boolean().default(false),
542
- efforts: z4.array(EffortOptionSchema).default([]),
648
+ name: z5.string(),
649
+ description: z5.string().default(""),
650
+ isDefault: z5.boolean().default(false),
651
+ efforts: z5.array(EffortOptionSchema).default([]),
543
652
  defaultEffort: EffortSchema.optional()
544
653
  });
545
- var RunnerModelsSchema = z4.object({
654
+ var RunnerModelsSchema = z5.object({
546
655
  runner: NameSchema,
547
656
  cli: CliKindSchema,
548
- models: z4.array(ModelOptionSchema)
657
+ models: z5.array(ModelOptionSchema)
549
658
  });
550
- var AgentStatusSchema = z4.enum(["active", "retired"]);
551
- var AgentSchema = z4.object({
659
+ var AgentStatusSchema = z5.enum(["active", "retired"]);
660
+ var AgentSchema = z5.object({
552
661
  name: NameSchema,
553
662
  role: NameSchema,
554
663
  cli: CliKindSchema.nullable(),
555
- model: z4.string().optional(),
664
+ model: z5.string().optional(),
556
665
  /** The reasoning effort set for the agent's turns; the model's own default applies otherwise. */
557
- effort: z4.string().optional(),
666
+ effort: z5.string().optional(),
558
667
  /** The runner preferred for the agent's turns where nothing else decides; any runner with its CLI otherwise. */
559
668
  homeRunner: NameSchema.optional(),
560
- memberships: z4.array(NameSchema),
561
- subscriptions: z4.array(ChannelRefSchema),
669
+ memberships: z5.array(NameSchema),
670
+ subscriptions: z5.array(ChannelRefSchema),
562
671
  status: AgentStatusSchema,
563
672
  createdAt: IsoDateTimeSchema,
564
- tokenHash: z4.string().min(1),
673
+ tokenHash: z5.string().min(1),
565
674
  retiredAt: IsoDateTimeSchema.optional(),
566
- retiredReason: z4.string().optional()
675
+ retiredReason: z5.string().optional()
567
676
  });
568
- var MemberSchema = z4.object({
677
+ var MemberSchema = z5.object({
569
678
  name: NameSchema,
570
679
  role: NameSchema,
571
680
  cli: CliKindSchema.nullable(),
572
681
  homeRunner: NameSchema.optional(),
573
682
  status: AgentStatusSchema,
574
- resident: z4.boolean().default(false),
683
+ resident: z5.boolean().default(false),
575
684
  /** The model configured on the record, if any; the CLI's own default applies otherwise. */
576
- model: z4.string().optional(),
685
+ model: z5.string().optional(),
577
686
  /** The model the CLI reported on the citizen's last turn. */
578
- lastModel: z4.string().optional(),
687
+ lastModel: z5.string().optional(),
579
688
  /** The reasoning effort configured on the record, if any; the model's own default applies otherwise. */
580
- effort: z4.string().optional(),
689
+ effort: z5.string().optional(),
581
690
  /** The citizen's own skills, by name. */
582
- skills: z4.array(NameSchema).default([]),
583
- memberships: z4.array(NameSchema),
584
- subscriptions: z4.array(ChannelRefSchema),
585
- claimsHeld: z4.number().int().nonnegative().default(0),
586
- tasksDone: z4.number().int().nonnegative().default(0),
691
+ skills: z5.array(NameSchema).default([]),
692
+ memberships: z5.array(NameSchema),
693
+ subscriptions: z5.array(ChannelRefSchema),
694
+ claimsHeld: z5.number().int().nonnegative().default(0),
695
+ tasksDone: z5.number().int().nonnegative().default(0),
587
696
  lastTurnAt: IsoDateTimeSchema.optional(),
588
- lastTurnOutcome: z4.string().optional(),
697
+ lastTurnOutcome: z5.string().optional(),
589
698
  createdAt: IsoDateTimeSchema,
590
699
  retiredAt: IsoDateTimeSchema.optional()
591
700
  });
592
- var RunnerOsSchema = z4.enum(["linux", "windows", "darwin"]);
593
- var RunnerSchema = z4.object({
701
+ var RunnerOsSchema = z5.enum(["linux", "windows", "darwin"]);
702
+ var GitIdentityPartSchema = z5.enum(["name", "email"]);
703
+ var RunnerSchema = z5.object({
594
704
  name: NameSchema,
595
705
  os: RunnerOsSchema,
596
- clis: z4.array(CliKindSchema),
597
- capabilities: z4.array(z4.string()),
706
+ clis: z5.array(CliKindSchema),
707
+ capabilities: z5.array(z5.string()),
598
708
  /** The release the runner reported when it last registered; absent until it has. */
599
- version: z4.string().optional(),
600
- status: z4.enum(["connected", "disconnected"]),
601
- lastSeen: IsoDateTimeSchema.optional()
709
+ version: z5.string().optional(),
710
+ /**
711
+ * What the runner's account lacked of a git identity when it last registered, empty when it had
712
+ * both; absent while the runner has not said, as one from before runners checked never does.
713
+ */
714
+ gitIdentityMissing: z5.array(GitIdentityPartSchema).optional(),
715
+ status: z5.enum(["connected", "disconnected"]),
716
+ lastSeen: IsoDateTimeSchema.optional(),
717
+ /**
718
+ * Each CLI's plan quota as its login on this runner last reported it, from a turn's stream or a
719
+ * read; absent until one has.
720
+ */
721
+ quotas: z5.partialRecord(CliKindSchema, CliQuotaSchema).optional()
602
722
  });
603
- var ProposalKindSchema = z4.enum([
723
+ var ProposalKindSchema = z5.enum([
604
724
  "role",
605
725
  "member",
606
726
  "channel",
@@ -609,43 +729,88 @@ var ProposalKindSchema = z4.enum([
609
729
  "skill",
610
730
  "archive"
611
731
  ]);
612
- var ProposalStatusSchema = z4.enum([
732
+ var ProposalStatusSchema = z5.enum([
613
733
  "proposed",
614
734
  "approved",
615
735
  "rejected",
616
736
  "provisioned",
617
737
  "retired"
618
738
  ]);
619
- var MemberProposalSchema = z4.object({
739
+ var MemberProposalSchema = z5.object({
620
740
  name: NameSchema,
621
741
  role: NameSchema,
622
742
  cli: CliKindSchema,
623
- model: z4.string().optional(),
743
+ model: z5.string().optional(),
624
744
  homeRunner: NameSchema.optional(),
625
- memberships: z4.array(NameSchema).default([]),
626
- subscriptions: z4.array(ChannelRefSchema).default([]),
627
- seedInstructions: z4.string().optional()
745
+ memberships: z5.array(NameSchema).default([]),
746
+ subscriptions: z5.array(ChannelRefSchema).default([]),
747
+ seedInstructions: z5.string().optional()
628
748
  });
629
- var ChannelProposalSchema = z4.object({
749
+ var ChannelProposalSchema = z5.object({
630
750
  project: NameSchema.nullable().default(null),
631
751
  name: NameSchema,
632
- purpose: z4.string().min(1)
752
+ purpose: z5.string().min(1)
633
753
  });
634
- var ReallocationProposalSchema = z4.object({
635
- description: z4.string().min(1)
754
+ var RoleChangesSchema = z5.strictObject({
755
+ maxReplicas: z5.number().int().min(1).optional(),
756
+ backlogThreshold: z5.number().positive().optional(),
757
+ wakeTriggers: z5.array(CharterTriggerSchema).optional(),
758
+ verbs: z5.array(VerbNameSchema).optional()
759
+ }).refine((changes) => Object.values(changes).some((value) => value !== void 0), {
760
+ message: "a reallocation changes at least one of maxReplicas, backlogThreshold, wakeTriggers, verbs"
761
+ });
762
+ var ReallocationProposalSchema = z5.object({
763
+ role: NameSchema,
764
+ changes: RoleChangesSchema,
765
+ description: z5.string().min(1)
636
766
  });
637
- var RetirementProposalSchema = z4.object({
767
+ var LegacyReallocationSchema = z5.object({ description: z5.string().min(1) });
768
+ function applyRoleChanges(role, changes) {
769
+ return {
770
+ ...role,
771
+ ...changes.maxReplicas === void 0 ? {} : { maxReplicas: changes.maxReplicas },
772
+ ...changes.backlogThreshold === void 0 ? {} : { backlogThreshold: changes.backlogThreshold },
773
+ ...changes.wakeTriggers === void 0 ? {} : { wakeTriggers: [...changes.wakeTriggers] },
774
+ ...changes.verbs === void 0 ? {} : { verbs: [...changes.verbs] }
775
+ };
776
+ }
777
+ function listedOrNone(values) {
778
+ return values.length === 0 ? "none" : values.join(", ");
779
+ }
780
+ function roleChangeLines(role, changes) {
781
+ const lines = [];
782
+ if (changes.maxReplicas !== void 0 && changes.maxReplicas !== role.maxReplicas) {
783
+ lines.push(`maxReplicas ${role.maxReplicas} \u2192 ${changes.maxReplicas}`);
784
+ }
785
+ if (changes.backlogThreshold !== void 0 && changes.backlogThreshold !== role.backlogThreshold) {
786
+ lines.push(`backlogThreshold ${role.backlogThreshold} \u2192 ${changes.backlogThreshold}`);
787
+ }
788
+ const wakes = changes.wakeTriggers;
789
+ if (wakes !== void 0 && (wakes.length !== role.wakeTriggers.length || wakes.some((trigger) => !role.wakeTriggers.includes(trigger)))) {
790
+ lines.push(`wakeTriggers ${listedOrNone(role.wakeTriggers)} \u2192 ${listedOrNone(wakes)}`);
791
+ }
792
+ if (changes.verbs !== void 0) {
793
+ const next = changes.verbs;
794
+ const added = next.filter((verb) => !role.verbs.includes(verb)).map((verb) => `+${verb}`);
795
+ const removed = role.verbs.filter((verb) => !next.includes(verb)).map((verb) => `\u2212${verb}`);
796
+ if (added.length + removed.length > 0) {
797
+ lines.push(`verbs ${[...added, ...removed].join(" ")}`);
798
+ }
799
+ }
800
+ return lines;
801
+ }
802
+ var RetirementProposalSchema = z5.object({
638
803
  agent: NameSchema,
639
- reason: z4.string().min(1)
804
+ reason: z5.string().min(1)
640
805
  });
641
- var ArchiveProposalSchema = z4.object({
806
+ var ArchiveProposalSchema = z5.object({
642
807
  project: NameSchema,
643
- reason: z4.string().min(1)
808
+ reason: z5.string().min(1)
644
809
  });
645
- var SkillProposalSchema = z4.object({
810
+ var SkillProposalSchema = z5.object({
646
811
  name: NameSchema,
647
- summary: z4.string().min(1).max(200),
648
- body: z4.string().min(1).optional()
812
+ summary: z5.string().min(1).max(200),
813
+ body: z5.string().min(1).optional()
649
814
  });
650
815
  var ProposalCharterSchemas = {
651
816
  role: RoleCharterSchema,
@@ -665,6 +830,32 @@ var ROLE_KIND_APPROVERS = {
665
830
  reallocation: [USER_ROLE, "steward"],
666
831
  skill: [USER_ROLE, "steward"]
667
832
  };
833
+ var USER_DECIDED_ROLE_CHANGES = ["verbs", "maxReplicas"];
834
+ function proposalApprovers(kind, charter) {
835
+ if (kind === "reallocation") {
836
+ const parsed = ReallocationProposalSchema.safeParse(charter);
837
+ if (parsed.success && USER_DECIDED_ROLE_CHANGES.some((field) => parsed.data.changes[field] !== void 0)) {
838
+ return [USER_ROLE];
839
+ }
840
+ }
841
+ return ROLE_KIND_APPROVERS[kind];
842
+ }
843
+ function describeRoleChanges(changes) {
844
+ const parts = [];
845
+ if (changes.maxReplicas !== void 0) {
846
+ parts.push(`up to ${changes.maxReplicas} per project`);
847
+ }
848
+ if (changes.backlogThreshold !== void 0) {
849
+ parts.push(`backlog threshold ${changes.backlogThreshold}`);
850
+ }
851
+ if (changes.wakeTriggers !== void 0) {
852
+ parts.push(changes.wakeTriggers.length === 0 ? "no ambient wakes" : `wakes on ${changes.wakeTriggers.join(", ")}`);
853
+ }
854
+ if (changes.verbs !== void 0) {
855
+ parts.push(`${changes.verbs.length} verbs`);
856
+ }
857
+ return parts.join(", ");
858
+ }
668
859
  function describeCharter(kind, charter) {
669
860
  switch (kind) {
670
861
  case "member": {
@@ -696,9 +887,14 @@ function describeCharter(kind, charter) {
696
887
  }
697
888
  case "reallocation": {
698
889
  const parsed = ReallocationProposalSchema.safeParse(charter);
699
- if (!parsed.success)
890
+ if (parsed.success) {
891
+ const { role, changes, description } = parsed.data;
892
+ return `reallocation of role ${role} (${describeRoleChanges(changes)}): ${description}`;
893
+ }
894
+ const legacy = LegacyReallocationSchema.safeParse(charter);
895
+ if (!legacy.success)
700
896
  break;
701
- return `reallocation: ${parsed.data.description}`;
897
+ return `reallocation: ${legacy.data.description}`;
702
898
  }
703
899
  case "skill": {
704
900
  const parsed = SkillProposalSchema.safeParse(charter);
@@ -717,23 +913,23 @@ function describeCharter(kind, charter) {
717
913
  }
718
914
  return `${kind} ${JSON.stringify(charter)}`;
719
915
  }
720
- var SkillSchema = z4.object({
916
+ var SkillSchema = z5.object({
721
917
  name: NameSchema,
722
- summary: z4.string().default(""),
723
- scope: z4.enum(["own", "society"]),
724
- path: z4.string().min(1)
918
+ summary: z5.string().default(""),
919
+ scope: z5.enum(["own", "society"]),
920
+ path: z5.string().min(1)
725
921
  });
726
- var RemovedSkillSchema = z4.object({
922
+ var RemovedSkillSchema = z5.object({
727
923
  name: NameSchema,
728
- reason: z4.string().min(1),
924
+ reason: z5.string().min(1),
729
925
  removedBy: NameSchema,
730
926
  removedAt: IsoDateTimeSchema
731
927
  });
732
- var HomeConflictSchema = z4.object({
928
+ var HomeConflictSchema = z5.object({
733
929
  /** The copy, relative to the home. */
734
- path: z4.string().min(1),
930
+ path: z5.string().min(1),
735
931
  /** The file beside it, whose version the board kept. */
736
- file: z4.string().min(1),
932
+ file: z5.string().min(1),
737
933
  /** When the copy reached the board. */
738
934
  since: IsoDateTimeSchema
739
935
  });
@@ -741,79 +937,79 @@ var CONFLICT_COPY = /^(.+)\.conflict-[^/.]+$/;
741
937
  function conflictCopyOf(copy) {
742
938
  return CONFLICT_COPY.exec(copy)?.[1] ?? null;
743
939
  }
744
- var CommitIdSchema = z4.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/);
745
- var HomeFileChangeSchema = z4.object({
746
- path: z4.string().min(1),
747
- status: z4.enum(["added", "modified", "deleted"]),
940
+ var CommitIdSchema = z5.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/);
941
+ var HomeFileChangeSchema = z5.object({
942
+ path: z5.string().min(1),
943
+ status: z5.enum(["added", "modified", "deleted"]),
748
944
  /** Lines added and removed, or null for a binary file. */
749
- added: z4.number().int().nonnegative().nullable(),
750
- removed: z4.number().int().nonnegative().nullable()
945
+ added: z5.number().int().nonnegative().nullable(),
946
+ removed: z5.number().int().nonnegative().nullable()
751
947
  });
752
- var HomeChangeSchema = z4.object({
948
+ var HomeChangeSchema = z5.object({
753
949
  commit: CommitIdSchema,
754
950
  at: IsoDateTimeSchema,
755
- kind: z4.enum(["turn", "board", "merge"]),
756
- author: z4.string(),
951
+ kind: z5.enum(["turn", "board", "merge"]),
952
+ author: z5.string(),
757
953
  /** The turn that made it, from the runner's commit message. */
758
954
  turnId: UlidSchema.optional(),
759
- subject: z4.string(),
760
- files: z4.array(HomeFileChangeSchema)
955
+ subject: z5.string(),
956
+ files: z5.array(HomeFileChangeSchema)
761
957
  });
762
- var HomeHistorySchema = z4.object({
763
- changes: z4.array(HomeChangeSchema),
958
+ var HomeHistorySchema = z5.object({
959
+ changes: z5.array(HomeChangeSchema),
764
960
  /** Whether older commits remain past the ones read. */
765
- more: z4.boolean()
961
+ more: z5.boolean()
766
962
  });
767
963
  var HomeFileDiffSchema = HomeFileChangeSchema.pick({ path: true, status: true }).extend({
768
- patch: z4.string().nullable(),
964
+ patch: z5.string().nullable(),
769
965
  /** The patch was cut at its size limit. */
770
- truncated: z4.boolean()
966
+ truncated: z5.boolean()
771
967
  });
772
- var KnowledgeSchema = z4.object({
968
+ var KnowledgeSchema = z5.object({
773
969
  topic: NameSchema,
774
970
  project: NameSchema.nullable(),
775
- revision: z4.number().int().min(0),
971
+ revision: z5.number().int().min(0),
776
972
  updatedBy: NameSchema,
777
973
  updatedAt: IsoDateTimeSchema
778
974
  });
779
- var RemovedKnowledgeSchema = z4.object({
975
+ var RemovedKnowledgeSchema = z5.object({
780
976
  topic: NameSchema,
781
977
  project: NameSchema.nullable(),
782
978
  removedBy: NameSchema,
783
979
  removedAt: IsoDateTimeSchema
784
980
  });
785
- var DashboardSchema = z4.object({
981
+ var DashboardSchema = z5.object({
786
982
  project: NameSchema,
787
- revision: z4.number().int().min(0),
983
+ revision: z5.number().int().min(0),
788
984
  /** Absent until someone updates the dashboard the project began with. */
789
985
  updatedBy: NameSchema.optional(),
790
986
  updatedAt: IsoDateTimeSchema.optional(),
791
- body: z4.string()
987
+ body: z5.string()
792
988
  });
793
- var ProposalOriginSchema = z4.object({
989
+ var ProposalOriginSchema = z5.object({
794
990
  scope: NameSchema,
795
991
  thread: UlidSchema.optional(),
796
992
  channel: NameSchema.optional()
797
993
  });
798
994
  var SKILL_FILES_LIMIT = 100;
799
995
  var SKILL_BYTES_LIMIT = 10 * 1024 * 1024;
800
- var SkillFileSchema = z4.object({
801
- path: z4.string().min(1),
802
- size: z4.number().int().nonnegative(),
803
- executable: z4.boolean()
996
+ var SkillFileSchema = z5.object({
997
+ path: z5.string().min(1),
998
+ size: z5.number().int().nonnegative(),
999
+ executable: z5.boolean()
804
1000
  });
805
- var SkillFilesSchema = z4.object({
1001
+ var SkillFilesSchema = z5.object({
806
1002
  /** The commit of the proposer's home the files came from. */
807
1003
  commit: CommitIdSchema,
808
1004
  takenAt: IsoDateTimeSchema,
809
- files: z4.array(SkillFileSchema)
1005
+ files: z5.array(SkillFileSchema)
810
1006
  });
811
1007
  var SkillFileContentSchema = SkillFileSchema.extend({
812
- text: z4.string().nullable(),
1008
+ text: z5.string().nullable(),
813
1009
  /** The text was cut at its size limit. */
814
- truncated: z4.boolean()
1010
+ truncated: z5.boolean()
815
1011
  });
816
- var ProposalFrontmatterSchema = z4.object({
1012
+ var ProposalFrontmatterSchema = z5.object({
817
1013
  id: UlidSchema,
818
1014
  kind: ProposalKindSchema,
819
1015
  proposedBy: NameSchema,
@@ -825,146 +1021,146 @@ var ProposalFrontmatterSchema = z4.object({
825
1021
  skillFiles: SkillFilesSchema.optional(),
826
1022
  decidedBy: NameSchema.optional(),
827
1023
  decidedAt: IsoDateTimeSchema.optional(),
828
- reason: z4.string().optional(),
829
- charter: z4.record(z4.string(), z4.unknown()),
1024
+ reason: z5.string().optional(),
1025
+ charter: z5.record(z5.string(), z5.unknown()),
830
1026
  /** What approval created, for kinds the board provisions: the agent, channel, role, or retirement. */
831
1027
  provisionedAt: IsoDateTimeSchema.optional(),
832
- provision: z4.record(z4.string(), z4.unknown()).optional()
1028
+ provision: z5.record(z5.string(), z5.unknown()).optional()
833
1029
  });
834
- var DecisionSchema = z4.object({
1030
+ var DecisionSchema = z5.object({
835
1031
  id: UlidSchema,
836
1032
  proposalId: UlidSchema,
837
1033
  decidedBy: NameSchema,
838
- outcome: z4.enum(["approved", "rejected"]),
839
- reason: z4.string().optional(),
1034
+ outcome: z5.enum(["approved", "rejected"]),
1035
+ reason: z5.string().optional(),
840
1036
  ts: IsoDateTimeSchema
841
1037
  });
842
1038
 
843
1039
  // packages/shared/dist/verbs.js
844
- import { z as z5 } from "zod";
845
- var MomentSchema = z5.iso.datetime({ offset: true });
846
- var CronTitleSchema = z5.string().trim().min(1).max(120);
847
- var CronNoteSchema = z5.string().trim().min(1).max(4e3);
1040
+ import { z as z6 } from "zod";
1041
+ var MomentSchema = z6.iso.datetime({ offset: true });
1042
+ var CronTitleSchema = z6.string().trim().min(1).max(120);
1043
+ var CronNoteSchema = z6.string().trim().min(1).max(4e3);
848
1044
  var VerbInputs = {
849
- post_message: z5.object({
1045
+ post_message: z6.object({
850
1046
  /** Required outside a thread; in one it may be left out, and must otherwise be the thread's. */
851
1047
  channel: ChannelRefSchema.optional(),
852
- body: z5.string().min(1),
1048
+ body: z6.string().min(1),
853
1049
  thread_id: UlidSchema.optional()
854
1050
  }),
855
- read_inbox: z5.object({
1051
+ read_inbox: z6.object({
856
1052
  since_cursor: UlidSchema.nullable().optional(),
857
- limit: z5.number().int().positive().max(200).default(50),
858
- advance: z5.boolean().default(true)
1053
+ limit: z6.number().int().positive().max(200).default(50),
1054
+ advance: z6.boolean().default(true)
859
1055
  }),
860
- search: z5.object({
861
- query: z5.string().min(1),
1056
+ search: z6.object({
1057
+ query: z6.string().min(1),
862
1058
  project: NameSchema.optional(),
863
1059
  channel: ChannelRefSchema.optional(),
864
- limit: z5.number().int().positive().max(100).default(20)
1060
+ limit: z6.number().int().positive().max(100).default(20)
865
1061
  }),
866
1062
  /** On a task or a proposal, whose id the thread takes, or on a channel with a title. */
867
- open_thread: z5.object({
1063
+ open_thread: z6.object({
868
1064
  task_id: UlidSchema.optional(),
869
1065
  proposal_id: UlidSchema.optional(),
870
1066
  channel: ChannelRefSchema.optional(),
871
- title: z5.string().min(1).max(200).optional()
1067
+ title: z6.string().min(1).max(200).optional()
872
1068
  }),
873
- close_thread: z5.object({ thread_id: UlidSchema, summary: z5.string().min(1) }),
874
- create_task: z5.object({
1069
+ close_thread: z6.object({ thread_id: UlidSchema, summary: z6.string().min(1) }),
1070
+ create_task: z6.object({
875
1071
  project: NameSchema,
876
- title: z5.string().min(1).max(200),
877
- body: z5.string().default(""),
1072
+ title: z6.string().min(1).max(200),
1073
+ body: z6.string().default(""),
878
1074
  parent_id: UlidSchema.optional(),
879
- required_capabilities: z5.array(z5.string()).default([]),
1075
+ required_capabilities: z6.array(z6.string()).default([]),
880
1076
  /** The plan; without it the task gets one stage, work, that anyone in the project may take. */
881
- stages: z5.array(PlanStageSchema).min(1).optional(),
1077
+ stages: z6.array(PlanStageSchema).min(1).optional(),
882
1078
  /** A channel of the project the task belongs to, such as a release's; its thread opens there. */
883
1079
  channel: NameSchema.default("general")
884
1080
  }),
885
- claim_task: z5.object({ task_id: UlidSchema }),
886
- release_task: z5.object({ task_id: UlidSchema }),
887
- update_task: z5.object({
1081
+ claim_task: z6.object({ task_id: UlidSchema }),
1082
+ release_task: z6.object({ task_id: UlidSchema }),
1083
+ update_task: z6.object({
888
1084
  task_id: UlidSchema,
889
1085
  /** Move the task back to this earlier stage. */
890
1086
  stage: StageIdSchema.optional(),
891
- status: z5.enum(["abandoned"]).optional(),
1087
+ status: z6.enum(["abandoned"]).optional(),
892
1088
  /** Posted to the task's thread as the caller, marked with the step when the task moved. */
893
- note: z5.string().min(1).optional(),
894
- blocked_by: z5.array(UlidSchema).optional(),
1089
+ note: z6.string().min(1).optional(),
1090
+ blocked_by: z6.array(UlidSchema).optional(),
895
1091
  /**
896
1092
  * The pull request a `ghpr` task lands through, on its project's repository, with the merge
897
1093
  * commit's subject and body; null unlinks it.
898
1094
  */
899
- pull_request: z5.object({
1095
+ pull_request: z6.object({
900
1096
  url: PullRequestUrlSchema,
901
1097
  merge_subject: MergeSubjectSchema.optional(),
902
- merge_body: z5.string().max(2e4).optional()
1098
+ merge_body: z6.string().max(2e4).optional()
903
1099
  }).nullable().optional()
904
1100
  }),
905
- get_task: z5.object({ task_id: UlidSchema }),
906
- subscribe: z5.object({ channel: ChannelRefSchema }),
907
- unsubscribe: z5.object({ channel: ChannelRefSchema }),
908
- propose: z5.object({
1101
+ get_task: z6.object({ task_id: UlidSchema }),
1102
+ subscribe: z6.object({ channel: ChannelRefSchema }),
1103
+ unsubscribe: z6.object({ channel: ChannelRefSchema }),
1104
+ propose: z6.object({
909
1105
  kind: ProposalKindSchema,
910
- charter: z5.record(z5.string(), z5.unknown()),
911
- rationale: z5.string().default("")
1106
+ charter: z6.record(z6.string(), z6.unknown()),
1107
+ rationale: z6.string().default("")
912
1108
  }),
913
- approve: z5.object({ proposal_id: UlidSchema, reason: z5.string().optional() }),
914
- reject: z5.object({ proposal_id: UlidSchema, reason: z5.string().min(1) }),
915
- create_project: z5.object({
1109
+ approve: z6.object({ proposal_id: UlidSchema, reason: z6.string().optional() }),
1110
+ reject: z6.object({ proposal_id: UlidSchema, reason: z6.string().min(1) }),
1111
+ create_project: z6.object({
916
1112
  slug: NameSchema,
917
- name: z5.string().min(1).optional(),
918
- repo: z5.string().min(1).nullable().default(null),
1113
+ name: z6.string().min(1).optional(),
1114
+ repo: z6.string().min(1).nullable().default(null),
919
1115
  default_branch: DefaultBranchSchema.default("main"),
920
1116
  on_done: CompletionEffectSchema.optional()
921
1117
  }),
922
- join_project: z5.object({ project: NameSchema, agent: NameSchema.optional() }),
923
- leave_project: z5.object({ project: NameSchema, agent: NameSchema.optional() }),
924
- write_knowledge: z5.object({
1118
+ join_project: z6.object({ project: NameSchema, agent: NameSchema.optional() }),
1119
+ leave_project: z6.object({ project: NameSchema, agent: NameSchema.optional() }),
1120
+ write_knowledge: z6.object({
925
1121
  /** A project you belong to, or null for society knowledge, which the steward and the user curate. */
926
1122
  project: NameSchema.nullable().default(null),
927
1123
  topic: NameSchema,
928
- body: z5.string().min(1),
1124
+ body: z6.string().min(1),
929
1125
  /** The `revision` in the frontmatter of the topic you read, 0 when it names none or for a new topic. */
930
- revision: z5.number().int().min(0)
1126
+ revision: z6.number().int().min(0)
931
1127
  }),
932
- remove_knowledge: z5.object({
1128
+ remove_knowledge: z6.object({
933
1129
  /** As for write_knowledge: a project you belong to, or null for society knowledge. */
934
1130
  project: NameSchema.nullable().default(null),
935
1131
  topic: NameSchema
936
1132
  }),
937
- plan_task: z5.object({
1133
+ plan_task: z6.object({
938
1134
  task_id: UlidSchema,
939
- stages: z5.array(PlanEditStageSchema),
1135
+ stages: z6.array(PlanEditStageSchema),
940
1136
  on_done: CompletionEffectSchema.optional()
941
1137
  }),
942
1138
  /** The note is posted to the task's thread as the caller, marked with the step. */
943
- advance_task: z5.object({ task_id: UlidSchema, note: z5.string().min(1).optional() }),
944
- configure_project: z5.object({
1139
+ advance_task: z6.object({ task_id: UlidSchema, note: z6.string().min(1).optional() }),
1140
+ configure_project: z6.object({
945
1141
  project: NameSchema,
946
1142
  on_done: CompletionEffectSchema.optional(),
947
1143
  /** A new display name; the slug, which everything refers to the project by, never changes. */
948
- name: z5.string().trim().min(1).max(120).optional(),
1144
+ name: z6.string().trim().min(1).max(120).optional(),
949
1145
  /** The branch tasks start from and land on from now; tasks in play keep their branches. */
950
1146
  default_branch: DefaultBranchSchema.optional()
951
1147
  }),
952
- archive_project: z5.object({ project: NameSchema, reason: z5.string().min(1) }),
953
- create_channel: z5.object({
1148
+ archive_project: z6.object({ project: NameSchema, reason: z6.string().min(1) }),
1149
+ create_channel: z6.object({
954
1150
  /** A project, or null for a channel of the society. */
955
1151
  project: NameSchema.nullable().default(null),
956
1152
  name: NameSchema,
957
- purpose: z5.string().min(1)
1153
+ purpose: z6.string().min(1)
958
1154
  }),
959
- archive_channel: z5.object({ channel: ChannelRefSchema, reason: z5.string().min(1) }),
960
- update_dashboard: z5.object({
1155
+ archive_channel: z6.object({ channel: ChannelRefSchema, reason: z6.string().min(1) }),
1156
+ update_dashboard: z6.object({
961
1157
  project: NameSchema,
962
1158
  /** The whole dashboard as markdown; the board keeps its frontmatter. */
963
- body: z5.string().min(1),
1159
+ body: z6.string().min(1),
964
1160
  /** The `revision` in the frontmatter of the dashboard you read, 0 when it names none. */
965
- revision: z5.number().int().min(0)
1161
+ revision: z6.number().int().min(0)
966
1162
  }),
967
- create_cron: z5.object({
1163
+ create_cron: z6.object({
968
1164
  title: CronTitleSchema,
969
1165
  /** What to do when it fires; quoted in the prompt of every turn it starts. */
970
1166
  note: CronNoteSchema,
@@ -983,10 +1179,10 @@ var VerbInputs = {
983
1179
  /** An open channel of that place other than general, whose conversation it fires in. */
984
1180
  channel: NameSchema.optional()
985
1181
  }),
986
- update_cron: z5.object({
1182
+ update_cron: z6.object({
987
1183
  cron_id: UlidSchema,
988
1184
  /** Pause it, or resume it, which starts its schedule afresh from now. */
989
- paused: z5.boolean().optional(),
1185
+ paused: z6.boolean().optional(),
990
1186
  title: CronTitleSchema.optional(),
991
1187
  note: CronNoteSchema.optional(),
992
1188
  /** A new expression, or `at` for one time; either starts the schedule afresh. */
@@ -995,8 +1191,8 @@ var VerbInputs = {
995
1191
  timezone: TimeZoneSchema.nullable().optional(),
996
1192
  at: MomentSchema.optional()
997
1193
  }),
998
- remove_cron: z5.object({ cron_id: UlidSchema, reason: z5.string().trim().min(1) }),
999
- remove_skill: z5.object({ name: NameSchema, reason: z5.string().trim().min(1) })
1194
+ remove_cron: z6.object({ cron_id: UlidSchema, reason: z6.string().trim().min(1) }),
1195
+ remove_skill: z6.object({ name: NameSchema, reason: z6.string().trim().min(1) })
1000
1196
  };
1001
1197
  var VERB_DESCRIPTIONS = {
1002
1198
  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.",
@@ -1007,7 +1203,7 @@ var VERB_DESCRIPTIONS = {
1007
1203
  create_task: "Create a task in a project with its plan: stages of {name, role or agent, gate}. Without stages it gets one stage, work, that anyone in the project may take. File it in a channel of the project, such as a release's, or general by default; its thread opens there with it, under the task's id, for everything said about the work.",
1008
1204
  claim_task: "Hold the task's current stage. Claims are leases renewed by every turn that touches the task.",
1009
1205
  release_task: "Let go of the stage you hold so someone else can take it.",
1010
- update_task: "Move a task back to an earlier stage, abandon it, set what it is blocked by, or link its pull request (pull_request: url, with the merge commit's merge_subject and merge_body), which a ghpr task needs before its last stage completes. A note is posted to the task's thread.",
1206
+ update_task: "Move a task back to an earlier stage, abandon it, set the tasks it is blocked by (blocked_by: its stage wakes nobody until they have all ended), or link its pull request (pull_request: url, with the merge commit's merge_subject and merge_body), which a ghpr task needs before its last stage completes. A note is posted to the task's thread.",
1011
1207
  get_task: "Read a task with its plan, its brief, and the messages of its thread.",
1012
1208
  subscribe: "Subscribe to a channel. Subscriptions feed your digest; they never wake you.",
1013
1209
  unsubscribe: "Unsubscribe from a channel.",
@@ -1033,7 +1229,7 @@ var VERB_DESCRIPTIONS = {
1033
1229
  };
1034
1230
 
1035
1231
  // packages/shared/dist/events.js
1036
- import { z as z6 } from "zod";
1232
+ import { z as z7 } from "zod";
1037
1233
  var BOARD_EVENT_TYPES = [
1038
1234
  "society.initialized",
1039
1235
  "project.added",
@@ -1078,6 +1274,7 @@ var BOARD_EVENT_TYPES = [
1078
1274
  "user.signed_out",
1079
1275
  "user.token_rotated",
1080
1276
  "runner.changed",
1277
+ "runner.quota",
1081
1278
  "ops.signal",
1082
1279
  "paused.changed",
1083
1280
  "wake.requested",
@@ -1098,28 +1295,36 @@ var BOARD_EVENT_TYPES = [
1098
1295
  "cron.skipped",
1099
1296
  "cron.ended"
1100
1297
  ];
1101
- var BoardEventTypeSchema = z6.enum(BOARD_EVENT_TYPES);
1102
- var BoardEventSchema = z6.object({
1298
+ var BoardEventTypeSchema = z7.enum(BOARD_EVENT_TYPES);
1299
+ var BoardEventSchema = z7.object({
1103
1300
  id: UlidSchema,
1104
1301
  ts: IsoDateTimeSchema,
1105
1302
  type: BoardEventTypeSchema,
1106
1303
  actor: NameSchema,
1107
- payload: z6.record(z6.string(), z6.unknown())
1304
+ payload: z7.record(z7.string(), z7.unknown())
1108
1305
  });
1109
- var UsageSchema = z6.object({
1110
- inputTokens: z6.number().int().nonnegative(),
1111
- outputTokens: z6.number().int().nonnegative(),
1112
- cacheReadTokens: z6.number().int().nonnegative().default(0),
1113
- cacheWriteTokens: z6.number().int().nonnegative().default(0)
1306
+ var UsageSchema = z7.object({
1307
+ inputTokens: z7.number().int().nonnegative(),
1308
+ outputTokens: z7.number().int().nonnegative(),
1309
+ cacheReadTokens: z7.number().int().nonnegative().default(0),
1310
+ cacheWriteTokens: z7.number().int().nonnegative().default(0)
1114
1311
  });
1115
- var TurnStatusSchema = z6.object({
1116
- summary: z6.string().min(1),
1117
- claimsHeld: z6.array(UlidSchema).default([]),
1118
- blockedOn: z6.array(z6.string()).default([]),
1119
- needsUserDecision: z6.boolean().default(false),
1120
- memoryUpdated: z6.boolean().default(false)
1312
+ function addUsage(a, b) {
1313
+ return {
1314
+ inputTokens: a.inputTokens + b.inputTokens,
1315
+ outputTokens: a.outputTokens + b.outputTokens,
1316
+ cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
1317
+ cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens
1318
+ };
1319
+ }
1320
+ var TurnStatusSchema = z7.object({
1321
+ summary: z7.string().min(1),
1322
+ claimsHeld: z7.array(UlidSchema).default([]),
1323
+ blockedOn: z7.array(z7.string()).default([]),
1324
+ needsUserDecision: z7.boolean().default(false),
1325
+ memoryUpdated: z7.boolean().default(false)
1121
1326
  });
1122
- var TurnExitReasonSchema = z6.enum([
1327
+ var TurnExitReasonSchema = z7.enum([
1123
1328
  "completed",
1124
1329
  "timeout",
1125
1330
  "error",
@@ -1127,50 +1332,50 @@ var TurnExitReasonSchema = z6.enum([
1127
1332
  "blocked",
1128
1333
  "stopped"
1129
1334
  ]);
1130
- var AgentEventSchema = z6.discriminatedUnion("type", [
1131
- z6.object({
1132
- type: z6.literal("turn_started"),
1335
+ var AgentEventSchema = z7.discriminatedUnion("type", [
1336
+ z7.object({
1337
+ type: z7.literal("turn_started"),
1133
1338
  agent: NameSchema,
1134
- session: z6.string(),
1339
+ session: z7.string(),
1135
1340
  runner: NameSchema,
1136
1341
  /** The model the CLI reports running, when it reports one. */
1137
- model: z6.string().optional()
1342
+ model: z7.string().optional()
1138
1343
  }),
1139
1344
  /**
1140
1345
  * The CLI took a steer into the running turn. Adapters name the steer; the server adds the
1141
1346
  * messages it delivered and the text the CLI was given.
1142
1347
  */
1143
- z6.object({
1144
- type: z6.literal("steered"),
1145
- steer: z6.string().min(1),
1146
- messages: z6.array(UlidSchema).optional(),
1147
- text: z6.string().optional()
1348
+ z7.object({
1349
+ type: z7.literal("steered"),
1350
+ steer: z7.string().min(1),
1351
+ messages: z7.array(UlidSchema).optional(),
1352
+ text: z7.string().optional()
1148
1353
  }),
1149
- z6.object({ type: z6.literal("text"), delta: z6.string() }),
1150
- z6.object({ type: z6.literal("tool_call"), name: z6.string(), input: z6.unknown() }),
1151
- z6.object({
1152
- type: z6.literal("tool_result"),
1153
- name: z6.string(),
1154
- ok: z6.boolean(),
1354
+ z7.object({ type: z7.literal("text"), delta: z7.string() }),
1355
+ z7.object({ type: z7.literal("tool_call"), name: z7.string(), input: z7.unknown() }),
1356
+ z7.object({
1357
+ type: z7.literal("tool_result"),
1358
+ name: z7.string(),
1359
+ ok: z7.boolean(),
1155
1360
  /** What the tool returned, cut to `OUTPUT_LIMIT` characters by `capOutput`. */
1156
- output: z6.string().optional()
1361
+ output: z7.string().optional()
1157
1362
  }),
1158
- z6.object({ type: z6.literal("approval_requested"), kind: z6.string(), detail: z6.unknown() }),
1159
- z6.object({
1160
- type: z6.literal("turn_completed"),
1363
+ z7.object({ type: z7.literal("approval_requested"), kind: z7.string(), detail: z7.unknown() }),
1364
+ z7.object({
1365
+ type: z7.literal("turn_completed"),
1161
1366
  usage: UsageSchema,
1162
- costUsd: z6.number().nonnegative(),
1367
+ costUsd: z7.number().nonnegative(),
1163
1368
  status: TurnStatusSchema.nullable(),
1164
1369
  exitReason: TurnExitReasonSchema
1165
1370
  }),
1166
- z6.object({ type: z6.literal("error"), message: z6.string() })
1371
+ z7.object({ type: z7.literal("error"), message: z7.string() })
1167
1372
  ]);
1168
- var TranscriptEntrySchema = z6.object({
1373
+ var TranscriptEntrySchema = z7.object({
1169
1374
  ts: IsoDateTimeSchema,
1170
1375
  event: AgentEventSchema
1171
1376
  });
1172
- var LiveTurnEventSchema = z6.object({
1173
- seq: z6.number().int().positive(),
1377
+ var LiveTurnEventSchema = z7.object({
1378
+ seq: z7.number().int().positive(),
1174
1379
  ts: IsoDateTimeSchema,
1175
1380
  agent: NameSchema,
1176
1381
  project: NameSchema,
@@ -1182,10 +1387,10 @@ var LiveTurnEventSchema = z6.object({
1182
1387
  });
1183
1388
 
1184
1389
  // packages/shared/dist/turns.js
1185
- import { z as z8 } from "zod";
1390
+ import { z as z9 } from "zod";
1186
1391
 
1187
1392
  // packages/shared/dist/runner.js
1188
- import { z as z7 } from "zod";
1393
+ import { z as z8 } from "zod";
1189
1394
  var RUNNER_PROTOCOL = 1;
1190
1395
  var PATH_TOKENS = {
1191
1396
  home: "{{stellaris:home}}",
@@ -1196,67 +1401,75 @@ var SERVER_TOKEN = "{{stellaris:server}}";
1196
1401
  var WORKSPACE_TOKEN = "{{stellaris:workspace}}";
1197
1402
  var HOME_SCRATCH = "scratch";
1198
1403
  var HOME_FILE_LIMIT_BYTES = 8 * 1024 * 1024;
1199
- var RunnerHelloSchema = z7.object({
1200
- protocol: z7.number().int().positive(),
1201
- version: z7.string().min(1),
1404
+ var RunnerHelloSchema = z8.object({
1405
+ protocol: z8.number().int().positive(),
1406
+ version: z8.string().min(1),
1202
1407
  os: RunnerOsSchema,
1203
- clis: z7.array(CliKindSchema),
1408
+ clis: z8.array(CliKindSchema),
1204
1409
  /** The CLIs whose adapter can keep a session warm between turns. */
1205
- residentClis: z7.array(CliKindSchema).default([]),
1410
+ residentClis: z8.array(CliKindSchema).default([]),
1206
1411
  /** The CLIs whose adapter can take input into a running turn; the server steers only these. */
1207
- steerableClis: z7.array(CliKindSchema).default([]),
1412
+ steerableClis: z8.array(CliKindSchema).default([]),
1208
1413
  /** The CLIs whose adapter can stop a running turn; the server stops only these. */
1209
- stoppableClis: z7.array(CliKindSchema).default([]),
1414
+ stoppableClis: z8.array(CliKindSchema).default([]),
1210
1415
  /** Whether the runner answers file reads on its projects' branches; one from before never does. */
1211
- branchReads: z7.boolean().default(false),
1416
+ branchReads: z8.boolean().default(false),
1212
1417
  /** Whether the runner lists what a branch changed; one from before never does. */
1213
- branchChanges: z7.boolean().default(false),
1214
- capabilities: z7.array(z7.string()).default([]),
1418
+ branchChanges: z8.boolean().default(false),
1419
+ /** Whether the runner answers a read of a CLI's quota; one from before never does. */
1420
+ quotaReads: z8.boolean().default(false),
1421
+ capabilities: z8.array(z8.string()).default([]),
1422
+ /**
1423
+ * What the runner's account lacks of the git identity citizens' own commits carry, checked
1424
+ * before each hello outside any repository; empty when it has both. Absent from a runner from
1425
+ * before the check, which no default fills in, so the board marks only what a runner reported.
1426
+ */
1427
+ gitIdentityMissing: z8.array(GitIdentityPartSchema).optional(),
1215
1428
  /** Turns the runner runs at once, or null for no limit. */
1216
- slots: z7.number().int().positive().nullable(),
1429
+ slots: z8.number().int().positive().nullable(),
1217
1430
  /** Turns the runner is still running, when it registers again; the server fails the rest of its turns. */
1218
- turns: z7.array(UlidSchema).default([])
1431
+ turns: z8.array(UlidSchema).default([])
1219
1432
  });
1220
- var RunnerWelcomeSchema = z7.object({ name: NameSchema, version: z7.string() });
1221
- var RunnerEnrollRequestSchema = z7.object({
1222
- protocol: z7.number().int().positive(),
1223
- version: z7.string().min(1).max(64),
1224
- hostname: z7.string().min(1).max(253),
1433
+ var RunnerWelcomeSchema = z8.object({ name: NameSchema, version: z8.string() });
1434
+ var RunnerEnrollRequestSchema = z8.object({
1435
+ protocol: z8.number().int().positive(),
1436
+ version: z8.string().min(1).max(64),
1437
+ hostname: z8.string().min(1).max(253),
1225
1438
  os: RunnerOsSchema,
1226
- clis: z7.array(CliKindSchema).max(8),
1227
- capabilities: z7.array(z7.string().min(1).max(64)).max(32).default([])
1439
+ clis: z8.array(CliKindSchema).max(8),
1440
+ capabilities: z8.array(z8.string().min(1).max(64)).max(32).default([])
1228
1441
  });
1229
- var RunnerEnrollmentSchema = z7.object({
1230
- deviceCode: z7.string().min(1),
1231
- userCode: z7.string().min(1),
1442
+ var RunnerEnrollmentSchema = z8.object({
1443
+ deviceCode: z8.string().min(1),
1444
+ userCode: z8.string().min(1),
1232
1445
  expiresAt: IsoDateTimeSchema,
1233
- intervalMs: z7.number().int().positive()
1446
+ intervalMs: z8.number().int().positive()
1234
1447
  });
1235
- var RunnerEnrollPollSchema = z7.object({ deviceCode: z7.string().min(1) });
1236
- var RunnerEnrollStatusSchema = z7.discriminatedUnion("status", [
1237
- z7.object({ status: z7.literal("pending") }),
1238
- z7.object({ status: z7.literal("approved"), name: NameSchema, token: z7.string().min(1) }),
1239
- z7.object({ status: z7.literal("denied") })
1448
+ var RunnerEnrollPollSchema = z8.object({ deviceCode: z8.string().min(1) });
1449
+ var RunnerEnrollStatusSchema = z8.discriminatedUnion("status", [
1450
+ z8.object({ status: z8.literal("pending") }),
1451
+ z8.object({ status: z8.literal("approved"), name: NameSchema, token: z8.string().min(1) }),
1452
+ z8.object({ status: z8.literal("denied") })
1240
1453
  ]);
1241
- var PendingEnrollmentSchema = z7.object({
1242
- userCode: z7.string().min(1),
1243
- hostname: z7.string(),
1454
+ var PendingEnrollmentSchema = z8.object({
1455
+ userCode: z8.string().min(1),
1456
+ hostname: z8.string(),
1244
1457
  os: RunnerOsSchema,
1245
- clis: z7.array(CliKindSchema),
1246
- capabilities: z7.array(z7.string()),
1247
- version: z7.string(),
1458
+ clis: z8.array(CliKindSchema),
1459
+ capabilities: z8.array(z8.string()),
1460
+ version: z8.string(),
1248
1461
  requestedAt: IsoDateTimeSchema,
1249
1462
  expiresAt: IsoDateTimeSchema
1250
1463
  });
1251
- var ProjectRepoSchema = z7.object({
1464
+ var ProjectRepoSchema = z8.object({
1252
1465
  slug: NameSchema,
1253
- origin: z7.string().nullable(),
1254
- defaultBranch: z7.string().min(1),
1466
+ origin: z8.string().nullable(),
1467
+ defaultBranch: z8.string().min(1),
1255
1468
  /**
1256
1469
  * The board lands finished tasks on the default branch, itself or through their pull requests,
1257
1470
  * so the runner lets nothing else move it.
1258
1471
  */
1259
- boardLands: z7.boolean()
1472
+ boardLands: z8.boolean()
1260
1473
  });
1261
1474
  function projectRepo(project) {
1262
1475
  return {
@@ -1266,29 +1479,29 @@ function projectRepo(project) {
1266
1479
  boardLands: project.onDone !== "none"
1267
1480
  };
1268
1481
  }
1269
- var TurnWorkspaceSchema = z7.discriminatedUnion("kind", [
1270
- z7.object({
1271
- kind: z7.literal("home"),
1482
+ var TurnWorkspaceSchema = z8.discriminatedUnion("kind", [
1483
+ z8.object({
1484
+ kind: z8.literal("home"),
1272
1485
  thread: UlidSchema.optional(),
1273
1486
  channel: NameSchema.optional()
1274
1487
  }),
1275
- z7.object({
1276
- kind: z7.literal("project"),
1488
+ z8.object({
1489
+ kind: z8.literal("project"),
1277
1490
  repo: ProjectRepoSchema,
1278
- branches: z7.array(z7.string().min(1)),
1491
+ branches: z8.array(z8.string().min(1)),
1279
1492
  thread: UlidSchema.optional(),
1280
1493
  channel: NameSchema.optional()
1281
1494
  }),
1282
- z7.object({ kind: z7.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
1495
+ z8.object({ kind: z8.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
1283
1496
  ]);
1284
- var TurnJobSchema = z7.object({
1497
+ var TurnJobSchema = z8.object({
1285
1498
  turnId: UlidSchema,
1286
1499
  agent: NameSchema,
1287
1500
  role: NameSchema,
1288
1501
  cli: CliKindSchema,
1289
- model: z7.string().optional(),
1502
+ model: z8.string().optional(),
1290
1503
  /** The reasoning effort set on the agent; absent for the model's own default. */
1291
- effort: z7.string().optional(),
1504
+ effort: z8.string().optional(),
1292
1505
  /** A project slug, or the society scope. */
1293
1506
  scope: NameSchema,
1294
1507
  /** The thread whose conversation the turn is in; absent for the home conversation. */
@@ -1299,125 +1512,130 @@ var TurnJobSchema = z7.object({
1299
1512
  * The session to resume, or null to start one: the CLI's adapter picks a fresh session's id, and
1300
1513
  * the outcome reports it.
1301
1514
  */
1302
- session: z7.string().min(1).nullable(),
1515
+ session: z8.string().min(1).nullable(),
1303
1516
  /** The running total the session reported after its last turn, which a resumed session starts from. */
1304
- costSoFarUsd: z7.number().nonnegative(),
1517
+ costSoFarUsd: z8.number().nonnegative(),
1305
1518
  /** The rendered charter, norms, memory core, and skills index, with `PATH_TOKENS` for places. */
1306
- instructions: z7.string(),
1307
- prompt: z7.string(),
1308
- mcp: z7.object({ url: z7.string().min(1), token: z7.string().min(1) }),
1309
- limits: z7.object({
1310
- timeoutMs: z7.number().int().positive().nullable(),
1311
- maxTurns: z7.number().int().positive().nullable()
1519
+ instructions: z8.string(),
1520
+ prompt: z8.string(),
1521
+ mcp: z8.object({ url: z8.string().min(1), token: z8.string().min(1) }),
1522
+ limits: z8.object({
1523
+ timeoutMs: z8.number().int().positive().nullable(),
1524
+ maxTurns: z8.number().int().positive().nullable()
1312
1525
  }),
1313
1526
  /** Present when the runner keeps the conversation's session warm between turns, under `key`. */
1314
- resident: z7.object({ key: z7.string().min(1), idleMs: z7.number().int().positive() }).optional(),
1527
+ resident: z8.object({ key: z8.string().min(1), idleMs: z8.number().int().positive() }).optional(),
1315
1528
  workspace: TurnWorkspaceSchema
1316
1529
  });
1317
- var TurnWorkSchema = z7.object({ branch: z7.string().min(1), head: z7.string().min(1) });
1318
- var TurnEventsSchema = z7.object({ entries: z7.array(TranscriptEntrySchema) });
1319
- var TurnOutcomeSchema = z7.object({
1530
+ var TurnWorkSchema = z8.object({ branch: z8.string().min(1), head: z8.string().min(1) });
1531
+ var TurnEventsSchema = z8.object({ entries: z8.array(TranscriptEntrySchema) });
1532
+ var TurnOutcomeSchema = z8.object({
1320
1533
  exitReason: TurnExitReasonSchema,
1321
1534
  status: TurnStatusSchema.nullable(),
1322
- error: z7.string().nullable(),
1535
+ error: z8.string().nullable(),
1323
1536
  usage: UsageSchema,
1324
- costUsd: z7.number().nonnegative(),
1537
+ costUsd: z8.number().nonnegative(),
1325
1538
  /** The session's running total after this turn, for CLIs that report one. */
1326
- sessionCostUsd: z7.number().nonnegative().optional(),
1539
+ sessionCostUsd: z8.number().nonnegative().optional(),
1327
1540
  /** The session the CLI used, which differs from the job's for CLIs that assign their own ids. */
1328
- session: z7.string().min(1),
1541
+ session: z8.string().min(1),
1329
1542
  /** The model the CLI reported, when it reported one. */
1330
- model: z7.string().nullable(),
1543
+ model: z8.string().nullable(),
1331
1544
  work: TurnWorkSchema.nullable(),
1332
1545
  /**
1333
1546
  * Whether the turn's own workspace, a thread's, a channel's, or a task's, holds work on no
1334
1547
  * branch after the hand-back: uncommitted changes, or commits no branch contains.
1335
1548
  */
1336
- leftovers: z7.boolean().default(false)
1549
+ leftovers: z8.boolean().default(false),
1550
+ /**
1551
+ * The CLI's plan quota as the turn's stream last reported it, when it reported any; a runner from
1552
+ * before never sends it.
1553
+ */
1554
+ quota: CliQuotaSchema.optional()
1337
1555
  });
1338
- var TurnAckSchema = z7.object({ dropWorkspace: z7.boolean() });
1339
- var LandRequestSchema = z7.object({
1556
+ var TurnAckSchema = z8.object({ dropWorkspace: z8.boolean() });
1557
+ var LandRequestSchema = z8.object({
1340
1558
  repo: ProjectRepoSchema,
1341
- branch: z7.string().min(1),
1559
+ branch: z8.string().min(1),
1342
1560
  /**
1343
1561
  * For a `ghpr` task, the pull request to merge in place of the branch, which must hold the
1344
1562
  * branch's files, and the merge commit's subject and body, the pull request's title and an
1345
1563
  * empty body when left out.
1346
1564
  */
1347
- pullRequest: z7.object({
1348
- url: z7.string().min(1),
1349
- subject: z7.string().min(1).optional(),
1350
- body: z7.string().optional()
1565
+ pullRequest: z8.object({
1566
+ url: z8.string().min(1),
1567
+ subject: z8.string().min(1).optional(),
1568
+ body: z8.string().optional()
1351
1569
  }).optional()
1352
1570
  });
1353
- var CloseRequestSchema = z7.object({
1571
+ var CloseRequestSchema = z8.object({
1354
1572
  repo: ProjectRepoSchema,
1355
- url: z7.string().min(1),
1356
- comment: z7.string().min(1)
1573
+ url: z8.string().min(1),
1574
+ comment: z8.string().min(1)
1357
1575
  });
1358
- var MergeOutcomeSchema = z7.object({ ok: z7.boolean(), detail: z7.string() });
1359
- var ModelListSchema = z7.array(ModelOptionSchema);
1576
+ var MergeOutcomeSchema = z8.object({ ok: z8.boolean(), detail: z8.string() });
1577
+ var ModelListSchema = z8.array(ModelOptionSchema);
1360
1578
  var BRANCH_FILE_LIMIT_BYTES = 8 * 1024 * 1024;
1361
- 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" });
1362
- var BranchNameSchema = z7.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/);
1363
- var BranchReadSchema = z7.object({
1579
+ var BranchPathSchema = z8.string().max(4096).refine((value) => value === "" || value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".." && !segment.includes("\0")), { message: "a path inside the branch, with no empty, `.`, or `..` segment" });
1580
+ var BranchNameSchema = z8.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/);
1581
+ var BranchReadSchema = z8.object({
1364
1582
  repo: ProjectRepoSchema,
1365
1583
  branch: BranchNameSchema,
1366
1584
  path: BranchPathSchema
1367
1585
  });
1368
- var BranchCommitSchema = z7.object({
1369
- id: z7.string().min(1),
1586
+ var BranchCommitSchema = z8.object({
1587
+ id: z8.string().min(1),
1370
1588
  at: IsoDateTimeSchema,
1371
- author: z7.string()
1589
+ author: z8.string()
1372
1590
  });
1373
- var BranchFileSchema = z7.discriminatedUnion("kind", [
1374
- z7.object({
1375
- kind: z7.literal("file"),
1591
+ var BranchFileSchema = z8.discriminatedUnion("kind", [
1592
+ z8.object({
1593
+ kind: z8.literal("file"),
1376
1594
  path: BranchPathSchema,
1377
1595
  commit: BranchCommitSchema,
1378
- size: z7.number().int().nonnegative(),
1379
- content: z7.string().nullable()
1596
+ size: z8.number().int().nonnegative(),
1597
+ content: z8.string().nullable()
1380
1598
  }),
1381
- z7.object({
1382
- kind: z7.literal("dir"),
1599
+ z8.object({
1600
+ kind: z8.literal("dir"),
1383
1601
  path: BranchPathSchema,
1384
1602
  commit: BranchCommitSchema,
1385
- entries: z7.array(z7.object({
1386
- name: z7.string().min(1),
1387
- kind: z7.enum(["file", "dir"]),
1388
- size: z7.number().int().nonnegative().nullable()
1603
+ entries: z8.array(z8.object({
1604
+ name: z8.string().min(1),
1605
+ kind: z8.enum(["file", "dir"]),
1606
+ size: z8.number().int().nonnegative().nullable()
1389
1607
  }))
1390
1608
  })
1391
1609
  ]);
1392
- var BranchChangesReadSchema = z7.object({
1610
+ var BranchChangesReadSchema = z8.object({
1393
1611
  repo: ProjectRepoSchema,
1394
1612
  branch: BranchNameSchema
1395
1613
  });
1396
- var BranchFileChangeSchema = z7.object({
1397
- path: z7.string().min(1),
1398
- status: z7.enum(["added", "modified", "deleted"]),
1614
+ var BranchFileChangeSchema = z8.object({
1615
+ path: z8.string().min(1),
1616
+ status: z8.enum(["added", "modified", "deleted"]),
1399
1617
  /** Lines added and removed, or null for a binary file. */
1400
- added: z7.number().int().nonnegative().nullable(),
1401
- removed: z7.number().int().nonnegative().nullable(),
1618
+ added: z8.number().int().nonnegative().nullable(),
1619
+ removed: z8.number().int().nonnegative().nullable(),
1402
1620
  /** Absent when the file changed only through a merge of another branch. */
1403
- lastChange: z7.object({ author: z7.string(), at: IsoDateTimeSchema }).optional()
1621
+ lastChange: z8.object({ author: z8.string(), at: IsoDateTimeSchema }).optional()
1404
1622
  });
1405
- var BranchChangesSchema = z7.object({
1623
+ var BranchChangesSchema = z8.object({
1406
1624
  head: BranchCommitSchema,
1407
- files: z7.array(BranchFileChangeSchema),
1408
- total: z7.number().int().nonnegative()
1625
+ files: z8.array(BranchFileChangeSchema),
1626
+ total: z8.number().int().nonnegative()
1409
1627
  });
1410
- var BranchDiffReadSchema = z7.object({
1628
+ var BranchDiffReadSchema = z8.object({
1411
1629
  repo: ProjectRepoSchema,
1412
1630
  branch: BranchNameSchema,
1413
1631
  path: BranchPathSchema.min(1)
1414
1632
  });
1415
- var BranchFileDiffSchema = z7.object({
1633
+ var BranchFileDiffSchema = z8.object({
1416
1634
  head: BranchCommitSchema,
1417
1635
  path: BranchPathSchema,
1418
1636
  status: BranchFileChangeSchema.shape.status,
1419
- patch: z7.string().nullable(),
1420
- truncated: z7.boolean()
1637
+ patch: z8.string().nullable(),
1638
+ truncated: z8.boolean()
1421
1639
  });
1422
1640
  function patchHunks(patch, limit2) {
1423
1641
  if (/^Binary files .* differ$/m.test(patch)) {
@@ -1431,9 +1649,9 @@ function patchHunks(patch, limit2) {
1431
1649
  const cut = hunks.lastIndexOf("\n", limit2);
1432
1650
  return { patch: hunks.slice(0, cut === -1 ? limit2 : cut), truncated: true };
1433
1651
  }
1434
- var WorkspaceConversationSchema = z7.discriminatedUnion("kind", [
1435
- z7.object({ kind: z7.literal("thread"), id: UlidSchema }),
1436
- z7.object({ kind: z7.literal("channel"), scope: NameSchema, name: NameSchema })
1652
+ var WorkspaceConversationSchema = z8.discriminatedUnion("kind", [
1653
+ z8.object({ kind: z8.literal("thread"), id: UlidSchema }),
1654
+ z8.object({ kind: z8.literal("channel"), scope: NameSchema, name: NameSchema })
1437
1655
  ]);
1438
1656
  function workspaceConversationOf(turn) {
1439
1657
  if (turn.thread !== void 0) {
@@ -1441,60 +1659,65 @@ function workspaceConversationOf(turn) {
1441
1659
  }
1442
1660
  return turn.channel === void 0 ? null : { kind: "channel", scope: turn.scope, name: turn.channel };
1443
1661
  }
1444
- var HeldWorkspaceSchema = z7.object({
1662
+ var HeldWorkspaceSchema = z8.object({
1445
1663
  agent: NameSchema,
1446
1664
  scope: NameSchema,
1447
1665
  conversation: WorkspaceConversationSchema
1448
1666
  });
1449
- var HeldWorkspacesSchema = z7.object({ workspaces: z7.array(HeldWorkspaceSchema) });
1450
- var SweepResultSchema = z7.object({ kept: z7.array(HeldWorkspaceSchema) });
1451
- var TurnSteerSchema = z7.object({ id: z7.uuid(), text: z7.string().min(1) });
1452
- var RunnerMessageSchema = z7.discriminatedUnion("type", [
1453
- z7.object({ type: z7.literal("turn"), job: TurnJobSchema }),
1454
- z7.object({ type: z7.literal("land"), request: z7.string().min(1), land: LandRequestSchema }),
1667
+ var HeldWorkspacesSchema = z8.object({ workspaces: z8.array(HeldWorkspaceSchema) });
1668
+ var SweepResultSchema = z8.object({ kept: z8.array(HeldWorkspaceSchema) });
1669
+ var TurnSteerSchema = z8.object({ id: z8.uuid(), text: z8.string().min(1) });
1670
+ var RunnerMessageSchema = z8.discriminatedUnion("type", [
1671
+ z8.object({ type: z8.literal("turn"), job: TurnJobSchema }),
1672
+ z8.object({ type: z8.literal("land"), request: z8.string().min(1), land: LandRequestSchema }),
1455
1673
  /** Answered with a `MergeOutcome`. */
1456
- z7.object({ type: z7.literal("close"), request: z7.string().min(1), close: CloseRequestSchema }),
1457
- z7.object({ type: z7.literal("models"), request: z7.string().min(1), cli: CliKindSchema }),
1674
+ z8.object({ type: z8.literal("close"), request: z8.string().min(1), close: CloseRequestSchema }),
1675
+ z8.object({ type: z8.literal("models"), request: z8.string().min(1), cli: CliKindSchema }),
1676
+ /**
1677
+ * Answered with a `CliQuota` read from the CLI without a turn, or null when its adapter cannot
1678
+ * read one. Sent only to runners whose hello says `quotaReads`.
1679
+ */
1680
+ z8.object({ type: z8.literal("quota"), request: z8.string().min(1), cli: CliKindSchema }),
1458
1681
  /** Answered with a `BranchFile`, or null when the branch or the path does not exist. */
1459
- z7.object({ type: z7.literal("file"), request: z7.string().min(1), read: BranchReadSchema }),
1682
+ z8.object({ type: z8.literal("file"), request: z8.string().min(1), read: BranchReadSchema }),
1460
1683
  /** Answered with `BranchChanges`, or null when the repository or the branch does not exist. */
1461
- z7.object({
1462
- type: z7.literal("changes"),
1463
- request: z7.string().min(1),
1684
+ z8.object({
1685
+ type: z8.literal("changes"),
1686
+ request: z8.string().min(1),
1464
1687
  read: BranchChangesReadSchema
1465
1688
  }),
1466
1689
  /** Answered with a `BranchFileDiff`, or null when the branch does not exist or left the path alone. */
1467
- z7.object({ type: z7.literal("diff"), request: z7.string().min(1), read: BranchDiffReadSchema }),
1690
+ z8.object({ type: z8.literal("diff"), request: z8.string().min(1), read: BranchDiffReadSchema }),
1468
1691
  /** Answered with whether the CLI accepted it; that it took it comes as a `steered` event. */
1469
- z7.object({
1470
- type: z7.literal("steer"),
1471
- request: z7.string().min(1),
1692
+ z8.object({
1693
+ type: z8.literal("steer"),
1694
+ request: z8.string().min(1),
1472
1695
  turnId: UlidSchema,
1473
1696
  steer: TurnSteerSchema
1474
1697
  }),
1475
1698
  /** Answered with whether the turn was there to stop; it ends `stopped`. */
1476
- z7.object({ type: z7.literal("stop"), request: z7.string().min(1), turnId: UlidSchema }),
1699
+ z8.object({ type: z8.literal("stop"), request: z8.string().min(1), turnId: UlidSchema }),
1477
1700
  /**
1478
1701
  * A conversation ended: the runner removes every citizen's workspace for it but those a turn
1479
1702
  * still uses, and answers with a `SweepResult` naming those it kept for holding work on no branch.
1480
1703
  */
1481
- z7.object({
1482
- type: z7.literal("sweep"),
1483
- request: z7.string().min(1),
1704
+ z8.object({
1705
+ type: z8.literal("sweep"),
1706
+ request: z8.string().min(1),
1484
1707
  conversation: WorkspaceConversationSchema
1485
1708
  })
1486
1709
  ]);
1487
- var RunnerAnswerSchema = z7.discriminatedUnion("ok", [
1488
- z7.object({ ok: z7.literal(true), value: z7.unknown() }),
1489
- z7.object({ ok: z7.literal(false), error: z7.string() })
1710
+ var RunnerAnswerSchema = z8.discriminatedUnion("ok", [
1711
+ z8.object({ ok: z8.literal(true), value: z8.unknown() }),
1712
+ z8.object({ ok: z8.literal(false), error: z8.string() })
1490
1713
  ]);
1491
- var WarmSessionsSchema = z7.object({ warm: z7.array(z7.string()) });
1492
- var FileManifestSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
1714
+ var WarmSessionsSchema = z8.object({ warm: z8.array(z8.string()) });
1715
+ var FileManifestSchema = z8.object({ files: z8.record(z8.string(), z8.string()) });
1493
1716
  function fileVersion(sha, executable) {
1494
1717
  return executable ? `${sha}+x` : sha;
1495
1718
  }
1496
- var FileReadSchema = z7.object({ paths: z7.array(z7.string()) });
1497
- var FileContentsSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
1719
+ var FileReadSchema = z8.object({ paths: z8.array(z8.string()) });
1720
+ var FileContentsSchema = z8.object({ files: z8.record(z8.string(), z8.string()) });
1498
1721
  function isSafeRelativePath(file) {
1499
1722
  if (file.length === 0 || file.startsWith("/") || file.includes("\\") || file.includes("\0")) {
1500
1723
  return false;
@@ -1503,7 +1726,7 @@ function isSafeRelativePath(file) {
1503
1726
  }
1504
1727
 
1505
1728
  // packages/shared/dist/turns.js
1506
- var TriggerKindSchema = z8.enum([
1729
+ var TriggerKindSchema = z9.enum([
1507
1730
  "mention",
1508
1731
  "stage",
1509
1732
  "task_done",
@@ -1517,11 +1740,11 @@ var TriggerKindSchema = z8.enum([
1517
1740
  "closing",
1518
1741
  "cron"
1519
1742
  ]);
1520
- var TriggerSchema = z8.object({
1743
+ var TriggerSchema = z9.object({
1521
1744
  kind: TriggerKindSchema,
1522
1745
  from: NameSchema.optional(),
1523
- fromUser: z8.boolean().default(false),
1524
- reason: z8.string().default(""),
1746
+ fromUser: z9.boolean().default(false),
1747
+ reason: z9.string().default(""),
1525
1748
  taskId: UlidSchema.optional(),
1526
1749
  messageId: UlidSchema.optional(),
1527
1750
  /** The channel the waking message was posted in. */
@@ -1529,17 +1752,17 @@ var TriggerSchema = z8.object({
1529
1752
  /** The cron that came due, for a `cron` wake. */
1530
1753
  cronId: UlidSchema.optional()
1531
1754
  });
1532
- var TurnThreadSchema = z8.object({ id: UlidSchema, task: z8.boolean() });
1533
- var TurnDispatchSchema = z8.object({
1755
+ var TurnThreadSchema = z9.object({ id: UlidSchema, task: z9.boolean() });
1756
+ var TurnDispatchSchema = z9.object({
1534
1757
  agent: NameSchema,
1535
1758
  project: NameSchema,
1536
1759
  thread: TurnThreadSchema.optional(),
1537
1760
  channel: NameSchema.optional(),
1538
1761
  trigger: TriggerSchema,
1539
- priority: z8.number().int().min(0).max(2),
1540
- onboarding: z8.boolean().default(false),
1762
+ priority: z9.number().int().min(0).max(2),
1763
+ onboarding: z9.boolean().default(false),
1541
1764
  /** Every cron that came due for this turn, whatever wake it merged into; its prompt quotes each. */
1542
- crons: z8.array(UlidSchema).optional()
1765
+ crons: z9.array(UlidSchema).optional()
1543
1766
  });
1544
1767
  function conversationPart(conversation) {
1545
1768
  return conversation.thread ?? (conversation.channel === void 0 ? void 0 : `#${conversation.channel}`);
@@ -1563,7 +1786,7 @@ function channelConversation(scope, channel) {
1563
1786
  const name = at < 0 ? channel : channel.slice(at + 1);
1564
1787
  return place === scope && name !== "general" ? name : void 0;
1565
1788
  }
1566
- var TurnRecordSchema = z8.object({
1789
+ var TurnRecordSchema = z9.object({
1567
1790
  /** Assigned by the board when the turn begins; the turn's transcript is filed under it. */
1568
1791
  id: UlidSchema.optional(),
1569
1792
  agent: NameSchema,
@@ -1574,31 +1797,31 @@ var TurnRecordSchema = z8.object({
1574
1797
  channel: NameSchema.optional(),
1575
1798
  runner: NameSchema,
1576
1799
  cli: CliKindSchema.nullable(),
1577
- session: z8.string().nullable(),
1800
+ session: z9.string().nullable(),
1578
1801
  trigger: TriggerSchema,
1579
1802
  startedAt: IsoDateTimeSchema,
1580
1803
  endedAt: IsoDateTimeSchema.nullable(),
1581
1804
  exitReason: TurnExitReasonSchema.nullable(),
1582
1805
  status: TurnStatusSchema.nullable(),
1583
- error: z8.string().nullable(),
1806
+ error: z9.string().nullable(),
1584
1807
  usage: UsageSchema.nullable(),
1585
- costUsd: z8.number().nonnegative().default(0),
1808
+ costUsd: z9.number().nonnegative().default(0),
1586
1809
  /**
1587
1810
  * The running total the CLI reported for the session after this turn, when it reports one. A
1588
1811
  * resumed session starts from it, so the next turn's own cost is measured from here.
1589
1812
  */
1590
- sessionCostUsd: z8.number().nonnegative().optional(),
1591
- toolCalls: z8.number().int().nonnegative().default(0),
1813
+ sessionCostUsd: z9.number().nonnegative().optional(),
1814
+ toolCalls: z9.number().int().nonnegative().default(0),
1592
1815
  /** The model the CLI reported for this turn, else the model configured for the agent, else null. */
1593
- model: z8.string().nullable().default(null),
1816
+ model: z9.string().nullable().default(null),
1594
1817
  /** The task branch the turn left work on, with its commit, as the runner reported it. */
1595
1818
  work: TurnWorkSchema.optional(),
1596
1819
  /** Who stopped the turn, for one that ended `stopped`. */
1597
1820
  stoppedBy: NameSchema.optional(),
1598
1821
  /** The crons whose fire started the turn, which record how it ended. */
1599
- crons: z8.array(UlidSchema).optional()
1822
+ crons: z9.array(UlidSchema).optional()
1600
1823
  });
1601
- var RunningTurnSchema = z8.object({
1824
+ var RunningTurnSchema = z9.object({
1602
1825
  turnId: UlidSchema,
1603
1826
  agent: NameSchema,
1604
1827
  scope: NameSchema,
@@ -1606,49 +1829,49 @@ var RunningTurnSchema = z8.object({
1606
1829
  channel: NameSchema.optional(),
1607
1830
  cli: CliKindSchema,
1608
1831
  askedIn: ChannelRefSchema.optional(),
1609
- steerable: z8.boolean(),
1610
- stoppable: z8.boolean()
1832
+ steerable: z9.boolean(),
1833
+ stoppable: z9.boolean()
1611
1834
  });
1612
- var SessionsFileSchema = z8.object({
1613
- claude: z8.string().optional(),
1614
- codex: z8.string().optional(),
1835
+ var SessionsFileSchema = z9.object({
1836
+ claude: z9.string().optional(),
1837
+ codex: z9.string().optional(),
1615
1838
  runner: NameSchema.optional()
1616
1839
  });
1617
- var WakeRequestSchema = z8.object({
1840
+ var WakeRequestSchema = z9.object({
1618
1841
  agent: NameSchema,
1619
1842
  project: NameSchema,
1620
- reason: z8.string().default("manual wake"),
1843
+ reason: z9.string().default("manual wake"),
1621
1844
  /** A manual wake, or a reflection turn requested ahead of the cadence. */
1622
- kind: z8.enum(["manual", "reflection"]).default("manual")
1845
+ kind: z9.enum(["manual", "reflection"]).default("manual")
1623
1846
  });
1624
- var TurnHistoryEntrySchema = z8.object({
1847
+ var TurnHistoryEntrySchema = z9.object({
1625
1848
  /** The id of the event that ended the turn. */
1626
1849
  id: UlidSchema,
1627
1850
  /** The turn's own id, when it has a transcript to open; turns before transcripts have none. */
1628
1851
  turnId: UlidSchema.optional(),
1629
1852
  ts: IsoDateTimeSchema,
1630
- outcome: z8.enum(["completed", "failed"]),
1853
+ outcome: z9.enum(["completed", "failed"]),
1631
1854
  /** A project slug, or the society scope. */
1632
1855
  project: NameSchema,
1633
1856
  /** The thread whose conversation the turn was in; absent for the home conversation. */
1634
1857
  thread: UlidSchema.optional(),
1635
1858
  /** The channel, never general, whose conversation the turn was in. */
1636
1859
  channel: NameSchema.optional(),
1637
- trigger: z8.string(),
1638
- exitReason: z8.string().nullable(),
1639
- costUsd: z8.number(),
1860
+ trigger: z9.string(),
1861
+ exitReason: z9.string().nullable(),
1862
+ costUsd: z9.number(),
1640
1863
  /** The tokens the turn used; absent for turns logged before turns recorded them. */
1641
1864
  usage: UsageSchema.optional(),
1642
- model: z8.string().nullable(),
1643
- summary: z8.string().nullable(),
1644
- error: z8.string().nullable(),
1865
+ model: z9.string().nullable(),
1866
+ summary: z9.string().nullable(),
1867
+ error: z9.string().nullable(),
1645
1868
  /** When the log recorded the turn's start; absent when the start is missing from the log. */
1646
1869
  startedAt: IsoDateTimeSchema.optional(),
1647
- toolCalls: z8.number().int().nonnegative().optional()
1870
+ toolCalls: z9.number().int().nonnegative().optional()
1648
1871
  });
1649
1872
 
1650
1873
  // packages/shared/dist/ops.js
1651
- import { z as z9 } from "zod";
1874
+ import { z as z10 } from "zod";
1652
1875
  var OPS_SIGNAL_KINDS = [
1653
1876
  "waiting_stage",
1654
1877
  "backlog",
@@ -1664,7 +1887,7 @@ var OPS_SIGNAL_KINDS = [
1664
1887
  "stuck_workspace",
1665
1888
  "cron_failing"
1666
1889
  ];
1667
- var OpsSignalKindSchema = z9.enum(OPS_SIGNAL_KINDS);
1890
+ var OpsSignalKindSchema = z10.enum(OPS_SIGNAL_KINDS);
1668
1891
  var WAKING_SIGNAL_KINDS = [
1669
1892
  "backlog",
1670
1893
  "role_gap",
@@ -1674,15 +1897,15 @@ var WAKING_SIGNAL_KINDS = [
1674
1897
  "blocked_capability",
1675
1898
  "scaled"
1676
1899
  ];
1677
- var OpsSignalSchema = z9.object({
1900
+ var OpsSignalSchema = z10.object({
1678
1901
  kind: OpsSignalKindSchema,
1679
1902
  /** Identifies the condition, for example `backlog:demo:engineer`, so a persisting one is not logged again every pass. */
1680
- key: z9.string().min(1),
1903
+ key: z10.string().min(1),
1681
1904
  /** One line a reader can act on. */
1682
- summary: z9.string().min(1),
1905
+ summary: z10.string().min(1),
1683
1906
  /** The counter or timer value behind the signal. */
1684
- value: z9.number(),
1685
- threshold: z9.number().optional(),
1907
+ value: z10.number(),
1908
+ threshold: z10.number().optional(),
1686
1909
  project: NameSchema.optional(),
1687
1910
  agent: NameSchema.optional(),
1688
1911
  role: NameSchema.optional(),
@@ -1690,8 +1913,8 @@ var OpsSignalSchema = z9.object({
1690
1913
  });
1691
1914
 
1692
1915
  // packages/shared/dist/metrics.js
1693
- import { z as z10 } from "zod";
1694
- var MetricsWindowSchema = z10.enum(["24h", "7d", "all"]);
1916
+ import { z as z11 } from "zod";
1917
+ var MetricsWindowSchema = z11.enum(["24h", "7d", "all"]);
1695
1918
  var METRICS_WINDOW_MS = {
1696
1919
  "24h": 24 * 36e5,
1697
1920
  "7d": 7 * 24 * 36e5,
@@ -1702,9 +1925,9 @@ function isBoardAction(tool) {
1702
1925
  const verb = /^mcp__board__(.+)$/.exec(tool)?.[1];
1703
1926
  return verb !== void 0 && !READ_VERBS.some((read2) => read2 === verb);
1704
1927
  }
1705
- var CountSchema = z10.number().int().nonnegative();
1706
- var MsSchema = z10.number().nonnegative();
1707
- var MetricsSchema = z10.object({
1928
+ var CountSchema = z11.number().int().nonnegative();
1929
+ var MsSchema = z11.number().nonnegative();
1930
+ var MetricsSchema = z11.object({
1708
1931
  window: MetricsWindowSchema,
1709
1932
  since: IsoDateTimeSchema.nullable(),
1710
1933
  until: IsoDateTimeSchema,
@@ -1712,12 +1935,12 @@ var MetricsSchema = z10.object({
1712
1935
  * Finished turns that changed nothing on the board. Reflections are left out, since they write
1713
1936
  * memory, not the board; turns without a transcript are counted apart, their actions unknown.
1714
1937
  */
1715
- idle: z10.object({
1938
+ idle: z11.object({
1716
1939
  turns: CountSchema,
1717
1940
  idle: CountSchema,
1718
1941
  unknown: CountSchema,
1719
- byTrigger: z10.array(z10.object({ trigger: z10.string(), turns: CountSchema, idle: CountSchema })),
1720
- byAgent: z10.array(z10.object({
1942
+ byTrigger: z11.array(z11.object({ trigger: z11.string(), turns: CountSchema, idle: CountSchema })),
1943
+ byAgent: z11.array(z11.object({
1721
1944
  agent: NameSchema,
1722
1945
  role: NameSchema.nullable(),
1723
1946
  turns: CountSchema,
@@ -1725,33 +1948,33 @@ var MetricsSchema = z10.object({
1725
1948
  }))
1726
1949
  }),
1727
1950
  /** Work sent back to an earlier stage: over the tasks finished in the window, and by where. */
1728
- sentBack: z10.object({
1951
+ sentBack: z11.object({
1729
1952
  finished: CountSchema,
1730
1953
  sendBacks: CountSchema,
1731
1954
  tasksSentBack: CountSchema,
1732
- byStage: z10.array(z10.object({ project: NameSchema, stage: z10.string(), count: CountSchema })),
1733
- bySender: z10.array(z10.object({ agent: NameSchema, count: CountSchema }))
1955
+ byStage: z11.array(z11.object({ project: NameSchema, stage: z11.string(), count: CountSchema })),
1956
+ bySender: z11.array(z11.object({ agent: NameSchema, count: CountSchema }))
1734
1957
  }),
1735
1958
  /** Posts in the threads of the tasks finished in the window, the board's own notices left out. */
1736
- messages: z10.object({
1959
+ messages: z11.object({
1737
1960
  finished: CountSchema,
1738
1961
  messages: CountSchema,
1739
- byProject: z10.array(z10.object({ project: NameSchema, finished: CountSchema, messages: CountSchema })),
1740
- busiest: z10.array(z10.object({
1962
+ byProject: z11.array(z11.object({ project: NameSchema, finished: CountSchema, messages: CountSchema })),
1963
+ busiest: z11.array(z11.object({
1741
1964
  taskId: UlidSchema,
1742
- title: z10.string(),
1965
+ title: z11.string(),
1743
1966
  project: NameSchema,
1744
1967
  messages: CountSchema
1745
1968
  }))
1746
1969
  }),
1747
1970
  /** Time from a mention to the start of the turn it woke, in the conversation it was made in. */
1748
- latency: z10.object({
1971
+ latency: z11.object({
1749
1972
  mentions: CountSchema,
1750
1973
  /** Mentions no turn has followed yet. */
1751
1974
  unanswered: CountSchema,
1752
1975
  medianMs: MsSchema.nullable(),
1753
1976
  slowestMs: MsSchema.nullable(),
1754
- byAgent: z10.array(z10.object({
1977
+ byAgent: z11.array(z11.object({
1755
1978
  agent: NameSchema,
1756
1979
  mentions: CountSchema,
1757
1980
  medianMs: MsSchema,
@@ -1759,61 +1982,73 @@ var MetricsSchema = z10.object({
1759
1982
  }))
1760
1983
  }),
1761
1984
  /** The user's decisions: proposals decided, questions answered, and stages passed or sent back. */
1762
- decisions: z10.object({
1985
+ decisions: z11.object({
1763
1986
  total: CountSchema,
1764
- byDay: z10.array(z10.object({
1765
- day: z10.string(),
1987
+ byDay: z11.array(z11.object({
1988
+ day: z11.string(),
1766
1989
  proposals: CountSchema,
1767
1990
  answers: CountSchema,
1768
1991
  stages: CountSchema
1769
1992
  }))
1770
1993
  }),
1771
1994
  /** Tasks that needed a capability no connected runner offered, and whether they still do. */
1772
- blocked: z10.array(z10.object({
1995
+ blocked: z11.array(z11.object({
1773
1996
  taskId: UlidSchema,
1774
- title: z10.string().nullable(),
1997
+ title: z11.string().nullable(),
1775
1998
  project: NameSchema.nullable(),
1776
- summary: z10.string(),
1999
+ summary: z11.string(),
1777
2000
  firstAt: IsoDateTimeSchema,
1778
2001
  lastAt: IsoDateTimeSchema,
1779
- holds: z10.boolean()
2002
+ holds: z11.boolean()
1780
2003
  }))
1781
2004
  });
2005
+ var CitizenTokensSchema = z11.object({
2006
+ agent: NameSchema,
2007
+ turns: CountSchema,
2008
+ withUsage: CountSchema,
2009
+ usage: UsageSchema.nullable()
2010
+ });
2011
+ var TokenUseSchema = z11.object({
2012
+ window: MetricsWindowSchema,
2013
+ since: IsoDateTimeSchema.nullable(),
2014
+ until: IsoDateTimeSchema,
2015
+ citizens: z11.array(CitizenTokensSchema)
2016
+ });
1782
2017
 
1783
2018
  // packages/shared/dist/config.js
1784
- import { z as z11 } from "zod";
1785
- var LogLevelSchema = z11.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
1786
- var GithubSignInSchema = z11.object({
1787
- clientId: z11.string().min(1),
1788
- clientSecret: z11.string().min(1),
2019
+ import { z as z12 } from "zod";
2020
+ var LogLevelSchema = z12.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
2021
+ var GithubSignInSchema = z12.object({
2022
+ clientId: z12.string().min(1),
2023
+ clientSecret: z12.string().min(1),
1789
2024
  /** Logins, lowercased; GitHub logins are case-insensitive. */
1790
- users: z11.array(z11.string().min(1)).min(1)
2025
+ users: z12.array(z12.string().min(1)).min(1)
1791
2026
  });
1792
- var ServerConfigSchema = z11.object({
1793
- dataDir: z11.string().min(1),
1794
- host: z11.string().min(1),
1795
- port: z11.number().int().positive().max(65535),
2027
+ var ServerConfigSchema = z12.object({
2028
+ dataDir: z12.string().min(1),
2029
+ host: z12.string().min(1),
2030
+ port: z12.number().int().positive().max(65535),
1796
2031
  /**
1797
2032
  * An address every runner's agents reach this server at, for example https://board.example, or
1798
2033
  * null for each runner to use the address it reaches the server at itself.
1799
2034
  */
1800
- publicUrl: z11.string().min(1).nullable(),
2035
+ publicUrl: z12.string().min(1).nullable(),
1801
2036
  logLevel: LogLevelSchema,
1802
2037
  /** Turns running at once across every runner, or null for no limit beyond each runner's own. */
1803
- concurrency: z11.number().int().positive().nullable(),
2038
+ concurrency: z12.number().int().positive().nullable(),
1804
2039
  /** How long one turn may run before it is stopped, or null for no limit. */
1805
- turnTimeoutMs: z11.number().int().positive().nullable(),
2040
+ turnTimeoutMs: z12.number().int().positive().nullable(),
1806
2041
  /** Rounds of tool calls one Claude turn may take, or null for no limit. */
1807
- toolRounds: z11.number().int().positive().nullable(),
2042
+ toolRounds: z12.number().int().positive().nullable(),
1808
2043
  /** How long a resident session stays warm after its last turn. */
1809
- residentIdleMs: z11.number().int().positive(),
2044
+ residentIdleMs: z12.number().int().positive(),
1810
2045
  /** Signing in to the board with GitHub, or null when only a token lets anyone in. */
1811
2046
  github: GithubSignInSchema.nullable(),
1812
2047
  /**
1813
2048
  * Reverse proxies in front of the server whose X-Forwarded-For entries name the client, for the
1814
2049
  * rate limits on what anyone may ask without a token: 0 when clients connect directly.
1815
2050
  */
1816
- trustedProxies: z11.number().int().min(0).max(16)
2051
+ trustedProxies: z12.number().int().min(0).max(16)
1817
2052
  });
1818
2053
  function limit(value, fallback) {
1819
2054
  if (value === void 0 || value.trim() === "") {
@@ -1851,25 +2086,73 @@ function loadServerConfig(env) {
1851
2086
  trustedProxies: Number(env["STELLARIS_TRUSTED_PROXIES"] ?? "0")
1852
2087
  });
1853
2088
  }
1854
- var RunnerConfigSchema = z11.object({
2089
+ var RunnerConfigSchema = z12.object({
1855
2090
  /** The board server, for example http://127.0.0.1:4700. */
1856
- serverUrl: z11.string().url(),
2091
+ serverUrl: z12.string().url(),
1857
2092
  /**
1858
2093
  * The runner's token, from `runner add`, or null for the one it saved when it was enrolled, or
1859
2094
  * to enroll it.
1860
2095
  */
1861
- token: z11.string().min(1).nullable(),
2096
+ token: z12.string().min(1).nullable(),
1862
2097
  /** The runner's own data directory: the board mirror, agent homes, repositories, worktrees. */
1863
- dataDir: z11.string().min(1),
2098
+ dataDir: z12.string().min(1),
1864
2099
  logLevel: LogLevelSchema,
1865
2100
  /** Turns this machine runs at once, or null for no limit. */
1866
- slots: z11.number().int().positive().nullable(),
1867
- clis: z11.array(CliKindSchema).min(1),
1868
- capabilities: z11.array(z11.string())
2101
+ slots: z12.number().int().positive().nullable(),
2102
+ clis: z12.array(CliKindSchema).min(1),
2103
+ capabilities: z12.array(z12.string())
1869
2104
  });
1870
2105
 
1871
2106
  // packages/shared/dist/version.js
1872
- var STELLARIS_VERSION = "0.3.20";
2107
+ var STELLARIS_VERSION = "0.3.22";
2108
+
2109
+ // packages/shared/dist/mentions.js
2110
+ import { parse, postprocess, preprocess } from "micromark";
2111
+ import { gfm } from "micromark-extension-gfm";
2112
+ var MENTION_PATTERN = /(^|[^\w@])@([a-z0-9][a-z0-9-]{0,31})(?![\w-])/g;
2113
+ var CODE = /* @__PURE__ */ new Set(["codeText", "codeFenced", "codeIndented"]);
2114
+ var SLOW = {
2115
+ disable: {
2116
+ null: [
2117
+ "wwwAutolink",
2118
+ "protocolAutolink",
2119
+ "emailAutolink",
2120
+ "gfmFootnoteCall",
2121
+ "gfmPotentialFootnoteCall"
2122
+ ]
2123
+ }
2124
+ };
2125
+ function codeRanges(markdown) {
2126
+ const shift = markdown.startsWith("\uFEFF") ? 1 : 0;
2127
+ const events = postprocess(parse({ extensions: [gfm(), SLOW] }).document().write(preprocess()(markdown, void 0, true)));
2128
+ return events.flatMap(([kind, token]) => kind === "enter" && CODE.has(token.type) ? [[token.start.offset + shift, token.end.offset + shift]] : []);
2129
+ }
2130
+ function addMentions(found, text) {
2131
+ for (const match of text.matchAll(MENTION_PATTERN)) {
2132
+ const name = match[2];
2133
+ if (name !== void 0) {
2134
+ found.add(name);
2135
+ }
2136
+ }
2137
+ }
2138
+ function mentionsIn(markdown) {
2139
+ if (markdown.search(MENTION_PATTERN) === -1) {
2140
+ return [];
2141
+ }
2142
+ const found = /* @__PURE__ */ new Set();
2143
+ let from = 0;
2144
+ for (const [start, end] of codeRanges(markdown)) {
2145
+ addMentions(found, markdown.slice(from, start));
2146
+ from = end;
2147
+ }
2148
+ addMentions(found, markdown.slice(from));
2149
+ return [...found].toSorted();
2150
+ }
2151
+ function mentionsAnywhere(text) {
2152
+ const found = /* @__PURE__ */ new Set();
2153
+ addMentions(found, text);
2154
+ return [...found].toSorted();
2155
+ }
1873
2156
 
1874
2157
  // packages/board-core/dist/errors.js
1875
2158
  var BoardError = class extends Error {
@@ -2515,33 +2798,59 @@ function filesOf(lines, status) {
2515
2798
  return [...files.values()];
2516
2799
  }
2517
2800
 
2801
+ // packages/board-core/dist/mentions.js
2802
+ import vm from "node:vm";
2803
+ var MENTION_PARSE_MS = 500;
2804
+ var context = vm.createContext({});
2805
+ var call = new vm.Script("work()");
2806
+ function within(work, ms) {
2807
+ let result;
2808
+ context["work"] = () => {
2809
+ result = work();
2810
+ };
2811
+ try {
2812
+ call.runInContext(context, { timeout: ms });
2813
+ } catch (error) {
2814
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
2815
+ return void 0;
2816
+ }
2817
+ throw error;
2818
+ } finally {
2819
+ context["work"] = void 0;
2820
+ }
2821
+ return result;
2822
+ }
2823
+ function postMentions(body) {
2824
+ return within(() => mentionsIn(body), MENTION_PARSE_MS) ?? mentionsAnywhere(body);
2825
+ }
2826
+
2518
2827
  // packages/board-core/dist/metrics.js
2519
- import { z as z12 } from "zod";
2520
- var TurnPayload = z12.object({
2521
- turnId: z12.string().optional(),
2522
- project: z12.string(),
2523
- thread: z12.string().optional(),
2524
- channel: z12.string().optional(),
2525
- trigger: z12.string()
2828
+ import { z as z13 } from "zod";
2829
+ var TurnPayload = z13.object({
2830
+ turnId: z13.string().optional(),
2831
+ project: z13.string(),
2832
+ thread: z13.string().optional(),
2833
+ channel: z13.string().optional(),
2834
+ trigger: z13.string()
2526
2835
  });
2527
- var TaskPayload = z12.object({
2528
- taskId: z12.string(),
2529
- project: z12.string(),
2530
- from: z12.string().optional()
2836
+ var TaskPayload = z13.object({
2837
+ taskId: z13.string(),
2838
+ project: z13.string(),
2839
+ from: z13.string().optional()
2531
2840
  });
2532
- var MessagePayload = z12.object({
2533
- id: z12.string().optional(),
2534
- channel: z12.string(),
2535
- thread: z12.string().nullable().optional(),
2536
- mentions: z12.array(z12.string()).default([])
2841
+ var MessagePayload = z13.object({
2842
+ id: z13.string().optional(),
2843
+ channel: z13.string(),
2844
+ thread: z13.string().nullable().optional(),
2845
+ mentions: z13.array(z13.string()).default([])
2537
2846
  });
2538
- var SteeredPayload = z12.object({ messages: z12.array(z12.string()) });
2539
- var SignalPayload = z12.object({
2540
- kind: z12.string(),
2541
- key: z12.string(),
2542
- summary: z12.string(),
2543
- taskId: z12.string().optional(),
2544
- project: z12.string().optional()
2847
+ var SteeredPayload = z13.object({ messages: z13.array(z13.string()) });
2848
+ var SignalPayload = z13.object({
2849
+ kind: z13.string(),
2850
+ key: z13.string(),
2851
+ summary: z13.string(),
2852
+ taskId: z13.string().optional(),
2853
+ project: z13.string().optional()
2545
2854
  });
2546
2855
  function median(values) {
2547
2856
  const sorted = values.toSorted((a, b) => a - b);
@@ -2830,6 +3139,59 @@ function computeMetrics(input) {
2830
3139
  };
2831
3140
  }
2832
3141
 
3142
+ // packages/board-core/dist/turn-history.js
3143
+ import { z as z14 } from "zod";
3144
+ var TurnHistoryPayloadSchema = z14.object({
3145
+ project: NameSchema,
3146
+ thread: UlidSchema.optional(),
3147
+ channel: NameSchema.optional(),
3148
+ trigger: z14.string().default("manual"),
3149
+ exitReason: z14.string().nullable().default(null),
3150
+ costUsd: z14.number().default(0),
3151
+ usage: UsageSchema.optional(),
3152
+ model: z14.string().nullable().default(null),
3153
+ summary: z14.string().nullable().default(null),
3154
+ error: z14.string().nullable().default(null),
3155
+ toolCalls: z14.number().int().nonnegative().optional(),
3156
+ turnId: UlidSchema.optional()
3157
+ });
3158
+ function tokenUse(events, window, now) {
3159
+ const span = METRICS_WINDOW_MS[window];
3160
+ const since = span === null ? null : now.getTime() - span;
3161
+ const rows = /* @__PURE__ */ new Map();
3162
+ for (const event of events) {
3163
+ if (event.type !== "turn.completed" && event.type !== "turn.failed") {
3164
+ continue;
3165
+ }
3166
+ if (since !== null && Date.parse(event.ts) < since) {
3167
+ continue;
3168
+ }
3169
+ const turn = TurnHistoryPayloadSchema.safeParse(event.payload);
3170
+ if (!turn.success) {
3171
+ continue;
3172
+ }
3173
+ const row = rows.get(event.actor) ?? {
3174
+ agent: event.actor,
3175
+ turns: 0,
3176
+ withUsage: 0,
3177
+ usage: null
3178
+ };
3179
+ const usage = turn.data.usage;
3180
+ rows.set(event.actor, {
3181
+ agent: event.actor,
3182
+ turns: row.turns + 1,
3183
+ withUsage: row.withUsage + (usage === void 0 ? 0 : 1),
3184
+ usage: usage === void 0 ? row.usage : row.usage === null ? usage : addUsage(row.usage, usage)
3185
+ });
3186
+ }
3187
+ return {
3188
+ window,
3189
+ since: since === null ? null : new Date(since).toISOString(),
3190
+ until: now.toISOString(),
3191
+ citizens: [...rows.values()].toSorted((a, b) => a.agent.localeCompare(b.agent))
3192
+ };
3193
+ }
3194
+
2833
3195
  // packages/board-core/dist/mutex.js
2834
3196
  var Mutex = class {
2835
3197
  tail = Promise.resolve();
@@ -3071,41 +3433,27 @@ function namesList(names) {
3071
3433
  return names.length <= 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}`;
3072
3434
  }
3073
3435
  var SKIPPED_IN_HOME = /* @__PURE__ */ new Set([".git", "turns", ".claude", ".codex"]);
3074
- var RunnerTokensSchema = z13.object({
3075
- runners: z13.record(z13.string(), z13.object({ tokenHash: z13.string().min(1) }))
3436
+ var RunnerTokensSchema = z15.object({
3437
+ runners: z15.record(z15.string(), z15.object({ tokenHash: z15.string().min(1) }))
3076
3438
  });
3077
- var SignInsSchema = z13.object({
3078
- signIns: z13.array(z13.object({
3079
- tokenHash: z13.string().min(1),
3080
- login: z13.string().min(1),
3439
+ var SignInsSchema = z15.object({
3440
+ signIns: z15.array(z15.object({
3441
+ tokenHash: z15.string().min(1),
3442
+ login: z15.string().min(1),
3081
3443
  /** The account's picture at the identity provider; sign-ins from before it have none. */
3082
- avatarUrl: z13.url().optional(),
3444
+ avatarUrl: z15.url().optional(),
3083
3445
  createdAt: IsoDateTimeSchema,
3084
3446
  expiresAt: IsoDateTimeSchema
3085
3447
  })).default([])
3086
3448
  });
3087
- var TurnHistoryPayloadSchema = z13.object({
3088
- project: NameSchema,
3089
- thread: UlidSchema.optional(),
3090
- channel: NameSchema.optional(),
3091
- trigger: z13.string().default("manual"),
3092
- exitReason: z13.string().nullable().default(null),
3093
- costUsd: z13.number().default(0),
3094
- usage: UsageSchema.optional(),
3095
- model: z13.string().nullable().default(null),
3096
- summary: z13.string().nullable().default(null),
3097
- error: z13.string().nullable().default(null),
3098
- toolCalls: z13.number().int().nonnegative().optional(),
3099
- turnId: UlidSchema.optional()
3100
- });
3101
- var TurnStartPayloadSchema = z13.object({ project: NameSchema, turnId: UlidSchema.optional() });
3102
- var CursorsSchema = z13.object({
3103
- digest: z13.string().nullable(),
3104
- scopes: z13.record(z13.string(), z13.string()).default({}),
3105
- threadsFrom: z13.record(z13.string(), z13.string().nullable()).default({}),
3106
- channelsFrom: z13.record(z13.string(), z13.string().nullable()).default({}),
3449
+ var TurnStartPayloadSchema = z15.object({ project: NameSchema, turnId: UlidSchema.optional() });
3450
+ var CursorsSchema = z15.object({
3451
+ digest: z15.string().nullable(),
3452
+ scopes: z15.record(z15.string(), z15.string()).default({}),
3453
+ threadsFrom: z15.record(z15.string(), z15.string().nullable()).default({}),
3454
+ channelsFrom: z15.record(z15.string(), z15.string().nullable()).default({}),
3107
3455
  /** How far a work role has read news, which goes to whichever of its general conversations reads first. */
3108
- news: z13.string().optional()
3456
+ news: z15.string().optional()
3109
3457
  });
3110
3458
  var NO_CURSORS = { digest: null, scopes: {}, threadsFrom: {}, channelsFrom: {} };
3111
3459
  function conversationKey(scope, part) {
@@ -3144,9 +3492,8 @@ function byId(a, b) {
3144
3492
  function turnTime(record) {
3145
3493
  return record.endedAt ?? record.startedAt;
3146
3494
  }
3147
- var PausedSchema = z13.object({ paused: z13.boolean() });
3495
+ var PausedSchema = z15.object({ paused: z15.boolean() });
3148
3496
  var DEFAULT_LEASE_MS = 30 * 60 * 1e3;
3149
- var MENTION_PATTERN = /(^|[^\w@])@([a-z0-9][a-z0-9-]{0,31})(?![\w-])/g;
3150
3497
  function withoutMentions(text) {
3151
3498
  return text.replaceAll(MENTION_PATTERN, "$1$2");
3152
3499
  }
@@ -3170,7 +3517,7 @@ var OPS_WAKE_TRIGGER = "ops_event";
3170
3517
  var FRONT_DESK_TRIGGER = "user_post";
3171
3518
  var OPS_CHANNELS = ["governance"];
3172
3519
  var FRONT_DESK_CHANNELS = [ASK_CHANNEL];
3173
- var AlignmentsSchema = z13.object({ applied: z13.array(z13.string()).default([]) });
3520
+ var AlignmentsSchema = z15.object({ applied: z15.array(z15.string()).default([]) });
3174
3521
  var FOLLOW_DECISIONS = "ops-readers-follow-decisions";
3175
3522
  var THREAD_RECORDS = "threads-as-records";
3176
3523
  var DIGEST_CURSORS = "digest-replaces-inbox";
@@ -3184,10 +3531,10 @@ var CHANNEL_CONVERSATIONS = "channel-conversations";
3184
3531
  var DASHBOARD_UPDATES = "dashboard-updates-granted";
3185
3532
  var CRON_VERBS = "cron-verbs-granted";
3186
3533
  var KNOWLEDGE_NOTICES = "knowledge-notices";
3187
- var KnowledgeEventSchema = z13.object({
3534
+ var KnowledgeEventSchema = z15.object({
3188
3535
  topic: NameSchema,
3189
3536
  project: NameSchema.nullable(),
3190
- replaced: z13.boolean().optional()
3537
+ replaced: z15.boolean().optional()
3191
3538
  });
3192
3539
  var CRON_VERB_NAMES = ["create_cron", "update_cron", "remove_cron"];
3193
3540
  var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
@@ -3305,16 +3652,6 @@ function unanswered(messages) {
3305
3652
  }
3306
3653
  return waiting;
3307
3654
  }
3308
- function extractMentions(body) {
3309
- const found = /* @__PURE__ */ new Set();
3310
- for (const match of body.matchAll(MENTION_PATTERN)) {
3311
- const name = match[2];
3312
- if (name !== void 0) {
3313
- found.add(name);
3314
- }
3315
- }
3316
- return [...found].toSorted();
3317
- }
3318
3655
  function snippetAround(text, query, radius = 80) {
3319
3656
  const index = text.toLowerCase().indexOf(query.toLowerCase());
3320
3657
  if (index === -1) {
@@ -3642,7 +3979,7 @@ var Board = class _Board {
3642
3979
  }
3643
3980
  }
3644
3981
  for (const file of await listFiles(this.paths.tasks(slug))) {
3645
- const raw = await readMarkdown(path6.join(this.paths.tasks(slug), file), z13.record(z13.string(), z13.unknown()));
3982
+ const raw = await readMarkdown(path6.join(this.paths.tasks(slug), file), z15.record(z15.string(), z15.unknown()));
3646
3983
  const legacy = raw.data["thread"];
3647
3984
  if (legacy === void 0) {
3648
3985
  continue;
@@ -3720,7 +4057,7 @@ var Board = class _Board {
3720
4057
  * turns, so it follows nothing now.
3721
4058
  */
3722
4059
  async renameInboxCursors() {
3723
- const Legacy = z13.object({ inbox: z13.string().nullable() });
4060
+ const Legacy = z15.object({ inbox: z15.string().nullable() });
3724
4061
  for (const agent of await this.listAgents()) {
3725
4062
  const file = this.paths.agentCursors(agent.name);
3726
4063
  if (await exists(file)) {
@@ -3766,7 +4103,7 @@ ${charter.purpose}
3766
4103
  */
3767
4104
  async alignSeedRole(seed) {
3768
4105
  const file = this.paths.role(seed.name);
3769
- const raw = await readMarkdown(file, z13.record(z13.string(), z13.unknown()));
4106
+ const raw = await readMarkdown(file, z15.record(z15.string(), z15.unknown()));
3770
4107
  const current = RoleCharterSchema.parse(raw.data);
3771
4108
  const verbsAdded = seed.verbs.filter((verb) => !current.verbs.includes(verb));
3772
4109
  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));
@@ -4083,7 +4420,7 @@ ${charter.purpose}
4083
4420
  /** The society's norms, the one knowledge topic every turn loads. Empty until the steward writes it. */
4084
4421
  async readSocietyNorms() {
4085
4422
  const file = this.paths.societyKnowledgeFile("norms");
4086
- return await exists(file) ? (await readMarkdown(file, z13.record(z13.string(), z13.unknown()))).body : "";
4423
+ return await exists(file) ? (await readMarkdown(file, z15.record(z15.string(), z15.unknown()))).body : "";
4087
4424
  }
4088
4425
  /** Knowledge topics of a project, or of the society when the project is null. */
4089
4426
  async listKnowledge(project) {
@@ -4103,7 +4440,7 @@ ${charter.purpose}
4103
4440
  */
4104
4441
  async knowledgeAt(file, project, topic) {
4105
4442
  const { data, body } = await readLooseMarkdown(file);
4106
- const revision = z13.number().int().min(0).safeParse(data["revision"]);
4443
+ const revision = z15.number().int().min(0).safeParse(data["revision"]);
4107
4444
  const updatedBy = NameSchema.safeParse(data["updatedBy"]);
4108
4445
  const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
4109
4446
  return {
@@ -4436,8 +4773,11 @@ ${DASHBOARD_LINE}
4436
4773
  throw new BoardError("NOT_FOUND", `runner ${name} not found`);
4437
4774
  }
4438
4775
  const doc = await readMarkdown(file, RunnerSchema);
4776
+ const { gitIdentityMissing: reported, ...rest } = doc.data;
4777
+ const gitIdentityMissing = patch.gitIdentityMissing === void 0 ? reported : patch.gitIdentityMissing ?? void 0;
4439
4778
  const next = RunnerSchema.parse({
4440
- ...doc.data,
4779
+ ...rest,
4780
+ ...gitIdentityMissing === void 0 ? {} : { gitIdentityMissing: [...gitIdentityMissing] },
4441
4781
  ...patch.status === void 0 ? {} : { status: patch.status },
4442
4782
  ...patch.os === void 0 ? {} : { os: patch.os },
4443
4783
  ...patch.clis === void 0 ? {} : { clis: [...patch.clis] },
@@ -4465,6 +4805,36 @@ ${DASHBOARD_LINE}
4465
4805
  return next;
4466
4806
  });
4467
4807
  }
4808
+ /**
4809
+ * Records a CLI's plan quota as a runner reported it, from a turn's stream or a read, merged over
4810
+ * what the record held. The record is written on every report, so it says when the numbers were
4811
+ * last seen; `runner.quota` is appended only when what a reader would see changes, since every
4812
+ * turn reports.
4813
+ */
4814
+ async recordQuota(name, cli, quota) {
4815
+ return this.mutex.run(async () => {
4816
+ const file = this.paths.runner(name);
4817
+ if (!await exists(file)) {
4818
+ throw new BoardError("NOT_FOUND", `runner ${name} not found`);
4819
+ }
4820
+ const doc = await readMarkdown(file, RunnerSchema);
4821
+ const previous = doc.data.quotas?.[cli];
4822
+ const merged = mergeQuota(previous, quota);
4823
+ const next = RunnerSchema.parse({
4824
+ ...doc.data,
4825
+ quotas: { ...doc.data.quotas, [cli]: merged }
4826
+ });
4827
+ await writeMarkdown(file, next, doc.body);
4828
+ if (quotaChanged(previous, merged)) {
4829
+ await this.events.append("runner.quota", SYSTEM_ACTOR.name, {
4830
+ name,
4831
+ cli,
4832
+ status: merged.status
4833
+ });
4834
+ }
4835
+ return next;
4836
+ });
4837
+ }
4468
4838
  /**
4469
4839
  * Logs an operations signal as an `ops.signal` event. No channel carries it: the scheduler wakes
4470
4840
  * its readers from the event, the runner lists it in their prompts, and the interface's log shows
@@ -4606,18 +4976,16 @@ ${DASHBOARD_LINE}
4606
4976
  async readDigest(actor, input = {}) {
4607
4977
  const args = VerbInputs.read_inbox.parse(input);
4608
4978
  await this.authorize(actor, "read_inbox");
4609
- return this.mutex.run(async () => {
4610
- const stored = await this.readCursors(actor.name);
4611
- const from = args.since_cursor === void 0 ? stored : { ...NO_CURSORS, digest: args.since_cursor };
4612
- const conversation = actor.scope === void 0 ? void 0 : { scope: actor.scope, part: conversationPart(actor) ?? null };
4613
- const filed = (await this.digestSince(actor, from, conversation)).slice(0, args.limit);
4614
- if (args.advance) {
4615
- await this.advanceCursors(actor.name, filed);
4616
- }
4617
- const last = filed.at(-1)?.message.id;
4618
- const cursor = last ?? (conversation === void 0 ? from.digest : cursorOf(from, conversation.scope, conversation.part));
4619
- return { messages: filed.map((entry) => entry.message), cursor };
4620
- });
4979
+ const { cursors: stored, upto } = await this.digestSnapshot(actor.name);
4980
+ const from = args.since_cursor === void 0 ? stored : { ...NO_CURSORS, digest: args.since_cursor };
4981
+ const conversation = actor.scope === void 0 ? void 0 : { scope: actor.scope, part: conversationPart(actor) ?? null };
4982
+ const filed = (await this.digestSince(actor, from, upto, conversation)).slice(0, args.limit);
4983
+ if (args.advance) {
4984
+ await this.mutex.run(() => this.advanceCursors(actor.name, filed));
4985
+ }
4986
+ const last = filed.at(-1)?.message.id;
4987
+ const cursor = last ?? (conversation === void 0 ? from.digest : cursorOf(from, conversation.scope, conversation.part));
4988
+ return { messages: filed.map((entry) => entry.message), cursor };
4621
4989
  }
4622
4990
  /**
4623
4991
  * How many unread digest messages a member has in each conversation: a scope's home, for its
@@ -4629,28 +4997,39 @@ ${DASHBOARD_LINE}
4629
4997
  * whose signals that call for action wake their readers directly.
4630
4998
  */
4631
4999
  async unreadByConversation(actor) {
4632
- return this.mutex.run(async () => {
4633
- const counts = /* @__PURE__ */ new Map();
4634
- const unscoped = { name: actor.name, role: actor.role };
4635
- for (const { scope, part, message, news } of await this.digestSince(unscoped, await this.readCursors(actor.name))) {
4636
- if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
4637
- continue;
4638
- }
4639
- const posted = parseChannelRef(message.channel).project ?? SOCIETY_SCOPE;
4640
- if (posted !== scope && scope !== SOCIETY_SCOPE) {
4641
- continue;
4642
- }
4643
- const key = conversationKey(scope, part);
4644
- const entry = counts.get(key) ?? {
4645
- scope,
4646
- thread: threadOfPart(part) ?? null,
4647
- channel: channelOfPart(part) ?? null,
4648
- count: 0
4649
- };
4650
- counts.set(key, { ...entry, count: entry.count + 1 });
5000
+ const { cursors, upto } = await this.digestSnapshot(actor.name);
5001
+ const counts = /* @__PURE__ */ new Map();
5002
+ const unscoped = { name: actor.name, role: actor.role };
5003
+ for (const { scope, part, message, news } of await this.digestSince(unscoped, cursors, upto)) {
5004
+ if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
5005
+ continue;
4651
5006
  }
4652
- return [...counts.values()];
4653
- });
5007
+ const posted = parseChannelRef(message.channel).project ?? SOCIETY_SCOPE;
5008
+ if (posted !== scope && scope !== SOCIETY_SCOPE) {
5009
+ continue;
5010
+ }
5011
+ const key = conversationKey(scope, part);
5012
+ const entry = counts.get(key) ?? {
5013
+ scope,
5014
+ thread: threadOfPart(part) ?? null,
5015
+ channel: channelOfPart(part) ?? null,
5016
+ count: 0
5017
+ };
5018
+ counts.set(key, { ...entry, count: entry.count + 1 });
5019
+ }
5020
+ return [...counts.values()];
5021
+ }
5022
+ /**
5023
+ * A reader's cursors and the id below which every message is already on disk, taken under the
5024
+ * mutex, which a digest scan then leaves: messages carry ids from the same monotonic source and are
5025
+ * written under the mutex, so the scan, which counts nothing newer than the mark, cannot pass over
5026
+ * one written while it reads, and a post never waits for a scan.
5027
+ */
5028
+ digestSnapshot(agent) {
5029
+ return this.mutex.run(async () => ({
5030
+ cursors: await this.readCursors(agent),
5031
+ upto: this.newId()
5032
+ }));
4654
5033
  }
4655
5034
  async readCursors(agent) {
4656
5035
  const file = this.paths.agentCursors(agent);
@@ -4692,9 +5071,10 @@ ${DASHBOARD_LINE}
4692
5071
  * by the user wherever it is. With a conversation, only what is filed there: for a thread, every
4693
5072
  * message of the thread, since the conversation is about it; for a channel's, that channel's
4694
5073
  * top-level messages; for a scope's home, general's and those of a project the member is not in,
4695
- * since each thread and each other channel is a conversation of its own.
5074
+ * since each thread and each other channel is a conversation of its own. Nothing newer than
5075
+ * `upto` counts (see `digestSnapshot`).
4696
5076
  */
4697
- async digestSince(actor, cursors, conversation) {
5077
+ async digestSince(actor, cursors, upto, conversation) {
4698
5078
  const conversationThread = threadOfPart(conversation?.part);
4699
5079
  if (conversation !== void 0 && conversationThread !== void 0) {
4700
5080
  const { scope, part } = conversation;
@@ -4702,7 +5082,7 @@ ${DASHBOARD_LINE}
4702
5082
  if (found === null) {
4703
5083
  return [];
4704
5084
  }
4705
- const messages = await this.readMessagesIn(this.paths.threadMessages(found.threads, conversationThread), cursorOf(cursors, scope, part));
5085
+ const messages = await this.readMessagesIn(this.paths.threadMessages(found.threads, conversationThread), cursorOf(cursors, scope, part), upto);
4706
5086
  return messages.filter((message) => message.author !== actor.name).map((message) => ({ scope, part, message })).toSorted(byId);
4707
5087
  }
4708
5088
  const agent = await this.readAgent(actor.name);
@@ -4720,7 +5100,7 @@ ${DASHBOARD_LINE}
4720
5100
  ];
4721
5101
  const known = starts.filter((start) => start !== null);
4722
5102
  const since = known.length < starts.length ? null : known.toSorted((a, b) => a < b ? -1 : a > b ? 1 : 0)[0] ?? null;
4723
- for await (const message of this.iterateMessages(since)) {
5103
+ for await (const message of this.iterateMessages(since, void 0, upto)) {
4724
5104
  const filedIn = wakeScope(agent, parseChannelRef(message.channel).project);
4725
5105
  const thread = message.thread ?? null;
4726
5106
  const asked = message.mentions.includes(actor.name) || frontDesk && message.author === USER_NAME;
@@ -5844,6 +6224,9 @@ ${args.summary}`);
5844
6224
  }
5845
6225
  if (args.blocked_by !== void 0) {
5846
6226
  for (const id of args.blocked_by) {
6227
+ if (id === current.id) {
6228
+ throw new BoardError("VALIDATION", `task ${current.id} cannot be blocked by itself`);
6229
+ }
5847
6230
  await this.findTask(id);
5848
6231
  }
5849
6232
  next = { ...next, blockedBy: [...args.blocked_by] };
@@ -5880,6 +6263,7 @@ ${args.summary}`);
5880
6263
  from: current.status,
5881
6264
  to: task.status,
5882
6265
  note: args.note ?? null,
6266
+ ...args.blocked_by === void 0 ? {} : { blockedBy: task.blockedBy, previousBlockedBy: current.blockedBy },
5883
6267
  ...args.pull_request === void 0 ? {} : { pullRequest: task.pullRequest?.url ?? null }
5884
6268
  });
5885
6269
  }
@@ -6285,7 +6669,7 @@ ${rationale}`}`);
6285
6669
  }
6286
6670
  async readAgentRoleBody(agent) {
6287
6671
  const file = this.paths.agentRole(agent);
6288
- return await exists(file) ? (await readMarkdown(file, z13.record(z13.string(), z13.unknown()))).body : "";
6672
+ return await exists(file) ? (await readMarkdown(file, z15.record(z15.string(), z15.unknown()))).body : "";
6289
6673
  }
6290
6674
  /** The steps of one finished turn, as the runner handed them over when it ended. */
6291
6675
  /**
@@ -6316,6 +6700,14 @@ ${rationale}`}`);
6316
6700
  }
6317
6701
  return computeMetrics({ events, tasks, citizens, actions, activeSignals, window, now });
6318
6702
  }
6703
+ /**
6704
+ * Each citizen's finished turns over a window and the tokens of those that counted them, from
6705
+ * the event log alone, which holds every turn's end whatever runner it ran on.
6706
+ */
6707
+ async tokenUse(window) {
6708
+ const events = await this.events.readSince(null, Number.MAX_SAFE_INTEGER);
6709
+ return tokenUse(events, window, this.now());
6710
+ }
6319
6711
  /** The board actions a finished turn took, from its transcript; null when it kept none. */
6320
6712
  async turnActions(agent, turnId) {
6321
6713
  const known = this.actionCounts.get(turnId);
@@ -6685,7 +7077,7 @@ ${rationale}`}`);
6685
7077
  return { project: slug, revision: 0, body: "" };
6686
7078
  }
6687
7079
  const { data, body } = await readLooseMarkdown(file);
6688
- const revision = z13.number().int().min(0).safeParse(data["revision"]);
7080
+ const revision = z15.number().int().min(0).safeParse(data["revision"]);
6689
7081
  const updatedBy = NameSchema.safeParse(data["updatedBy"]);
6690
7082
  const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
6691
7083
  return {
@@ -6976,7 +7368,7 @@ ${seed}
6976
7368
  continue;
6977
7369
  }
6978
7370
  const file = this.paths.agentRole(agent.name);
6979
- const body = await exists(file) ? (await readMarkdown(file, z13.record(z13.string(), z13.unknown()))).body : "";
7371
+ const body = await exists(file) ? (await readMarkdown(file, z15.record(z15.string(), z15.unknown()))).body : "";
6980
7372
  const at = body.indexOf(`
6981
7373
  ${SEED_INSTRUCTIONS_HEADING}
6982
7374
  `);
@@ -7178,12 +7570,37 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7178
7570
  case "archive":
7179
7571
  await this.validateArchive(ArchiveProposalSchema.parse(charter).project);
7180
7572
  return;
7181
- case "reallocation":
7573
+ case "reallocation": {
7574
+ const parsed = ReallocationProposalSchema.safeParse(charter);
7575
+ if (parsed.success) {
7576
+ await this.reallocatedRole(parsed.data);
7577
+ } else {
7578
+ LegacyReallocationSchema.parse(charter);
7579
+ }
7182
7580
  return;
7581
+ }
7183
7582
  default:
7184
7583
  return;
7185
7584
  }
7186
7585
  }
7586
+ /**
7587
+ * The role as a reallocation would leave it, with what changes: refused for the user's charter,
7588
+ * a role that does not exist, a charter the changes would make invalid, and changes the role
7589
+ * has already.
7590
+ */
7591
+ async reallocatedRole(reallocation) {
7592
+ if (reallocation.role === USER_ROLE) {
7593
+ throw new BoardError("FORBIDDEN", "the user charter is not subject to proposals");
7594
+ }
7595
+ const current = await this.readRole(reallocation.role);
7596
+ const next = RoleCharterSchema.parse(applyRoleChanges(current, reallocation.changes));
7597
+ await this.validateRole(next);
7598
+ const changed = roleChangeLines(current, reallocation.changes);
7599
+ if (changed.length === 0) {
7600
+ throw new BoardError("VALIDATION", `role ${current.name} has these values already; the reallocation changes nothing`);
7601
+ }
7602
+ return { next, changed };
7603
+ }
7187
7604
  async executeProvision(by, proposal) {
7188
7605
  const meta = { proposalId: proposal.id };
7189
7606
  switch (proposal.kind) {
@@ -7220,8 +7637,15 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7220
7637
  const { members } = await this.archiveUnlocked(by, input.project, input.reason, meta);
7221
7638
  return { project: input.project, members };
7222
7639
  }
7223
- case "reallocation":
7224
- return void 0;
7640
+ case "reallocation": {
7641
+ const parsed = ReallocationProposalSchema.safeParse(proposal.charter);
7642
+ if (!parsed.success) {
7643
+ return void 0;
7644
+ }
7645
+ const { next, changed } = await this.reallocatedRole(parsed.data);
7646
+ await this.writeRoleUnlocked(by, next, meta);
7647
+ return { role: next.name, changed };
7648
+ }
7225
7649
  default:
7226
7650
  return void 0;
7227
7651
  }
@@ -7782,7 +8206,7 @@ ${charter.purpose}
7782
8206
  id: this.newId(),
7783
8207
  ...head,
7784
8208
  ts: this.now().toISOString(),
7785
- mentions: extractMentions(body)
8209
+ mentions: postMentions(body)
7786
8210
  });
7787
8211
  const { author } = head;
7788
8212
  await writeMarkdown(this.paths.messageFile(dir, frontmatter.id, author), frontmatter, body);
@@ -7921,7 +8345,7 @@ ${charter.purpose}
7921
8345
  if (filed.proposedBy === actor.name) {
7922
8346
  throw new BoardError("FORBIDDEN", "a proposer never decides its own proposal");
7923
8347
  }
7924
- const approvers = ROLE_KIND_APPROVERS[filed.kind];
8348
+ const approvers = proposalApprovers(filed.kind, filed.charter);
7925
8349
  if (!approvers.includes(actor.role)) {
7926
8350
  throw new BoardError("FORBIDDEN", `a ${filed.kind} proposal requires one of: ${approvers.join(", ")}`);
7927
8351
  }
@@ -8004,7 +8428,7 @@ ${charter.purpose}
8004
8428
  }
8005
8429
  if (subject?.kind === "proposal") {
8006
8430
  const proposal = await this.readProposal(subject.id);
8007
- if (proposal.proposedBy === actor.name || proposal.decidedBy === actor.name || ROLE_KIND_APPROVERS[proposal.kind].includes(actor.role)) {
8431
+ if (proposal.proposedBy === actor.name || proposal.decidedBy === actor.name || proposalApprovers(proposal.kind, proposal.charter).includes(actor.role)) {
8008
8432
  return true;
8009
8433
  }
8010
8434
  return (await this.readRole(actor.role)).wakeTriggers.includes(OPS_WAKE_TRIGGER);
@@ -8061,11 +8485,12 @@ ${charter.purpose}
8061
8485
  });
8062
8486
  await this.endCronsUnlocked(by, `its thread closed: its ${found.thread.subject?.kind ?? "subject"} was ${ended}`, (cron) => cron.thread === subjectId);
8063
8487
  }
8064
- async readMessagesIn(dir, since) {
8488
+ /** The messages in a folder after `since` and, when given, up to `upto`, read by name first. */
8489
+ async readMessagesIn(dir, since, upto) {
8065
8490
  const messages = [];
8066
8491
  for (const file of await listFiles(dir)) {
8067
8492
  const id = file.slice(0, 26);
8068
- if (since !== null && id <= since) {
8493
+ if (since !== null && id <= since || upto !== void 0 && id > upto) {
8069
8494
  continue;
8070
8495
  }
8071
8496
  const doc = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
@@ -8073,20 +8498,23 @@ ${charter.purpose}
8073
8498
  }
8074
8499
  return messages;
8075
8500
  }
8076
- /** Every message newer than `since` across society channels, project channels, and threads. */
8077
- async *iterateMessages(since, project) {
8501
+ /**
8502
+ * Every message newer than `since`, and no newer than `upto` when given, across society
8503
+ * channels, project channels, and threads.
8504
+ */
8505
+ async *iterateMessages(since, project, upto) {
8078
8506
  if (project === void 0) {
8079
8507
  for (const channel of await listDirs(this.paths.societyChannels())) {
8080
- yield* await this.readMessagesIn(this.paths.societyChannel(channel), since);
8508
+ yield* await this.readMessagesIn(this.paths.societyChannel(channel), since, upto);
8081
8509
  }
8082
- yield* this.threadMessagesIn(this.paths.societyThreads(), since);
8510
+ yield* this.threadMessagesIn(this.paths.societyThreads(), since, upto);
8083
8511
  }
8084
8512
  const projects = project === void 0 ? await listDirs(this.paths.projects()) : [project];
8085
8513
  for (const slug of projects) {
8086
8514
  for (const channel of await listDirs(this.paths.projectChannels(slug))) {
8087
- yield* await this.readMessagesIn(this.paths.projectChannel(slug, channel), since);
8515
+ yield* await this.readMessagesIn(this.paths.projectChannel(slug, channel), since, upto);
8088
8516
  }
8089
- yield* this.threadMessagesIn(this.paths.threads(slug), since);
8517
+ yield* this.threadMessagesIn(this.paths.threads(slug), since, upto);
8090
8518
  }
8091
8519
  }
8092
8520
  /** Every folder that holds messages: each channel's and each thread's, in the society and every project. */
@@ -8106,9 +8534,9 @@ ${charter.purpose}
8106
8534
  }
8107
8535
  }
8108
8536
  }
8109
- async *threadMessagesIn(threads, since) {
8537
+ async *threadMessagesIn(threads, since, upto) {
8110
8538
  for (const id of await listDirs(threads)) {
8111
- yield* await this.readMessagesIn(this.paths.threadMessages(threads, id), since);
8539
+ yield* await this.readMessagesIn(this.paths.threadMessages(threads, id), since, upto);
8112
8540
  }
8113
8541
  }
8114
8542
  };
@@ -8156,7 +8584,7 @@ function decideWake(input) {
8156
8584
  }
8157
8585
 
8158
8586
  // packages/scheduler/dist/scheduler.js
8159
- import { z as z14 } from "zod";
8587
+ import { z as z16 } from "zod";
8160
8588
  function besideFields(beside) {
8161
8589
  if (beside === void 0) {
8162
8590
  return {};
@@ -8183,21 +8611,21 @@ var DEFAULT_TIMINGS = Object.freeze({
8183
8611
  scaleCooldownMs: 36e5,
8184
8612
  reflectionMs: 24 * 36e5
8185
8613
  });
8186
- var SchedulerTimingsSchema = z14.object({
8187
- pollMs: z14.number().positive(),
8188
- debounceMs: z14.number().nonnegative(),
8189
- userDebounceMs: z14.number().nonnegative(),
8190
- heartbeatMs: z14.number().positive(),
8191
- waitingStageMs: z14.number().positive(),
8192
- leaseSweepMs: z14.number().positive(),
8193
- opsIntervalMs: z14.number().positive(),
8194
- signalRepeatMs: z14.number().positive(),
8195
- costReportMs: z14.number().positive(),
8196
- staleThreadMs: z14.number().positive(),
8197
- idleMemberMs: z14.number().positive(),
8198
- homeConflictMs: z14.number().positive(),
8199
- scaleCooldownMs: z14.number().positive(),
8200
- reflectionMs: z14.number().positive()
8614
+ var SchedulerTimingsSchema = z16.object({
8615
+ pollMs: z16.number().positive(),
8616
+ debounceMs: z16.number().nonnegative(),
8617
+ userDebounceMs: z16.number().nonnegative(),
8618
+ heartbeatMs: z16.number().positive(),
8619
+ waitingStageMs: z16.number().positive(),
8620
+ leaseSweepMs: z16.number().positive(),
8621
+ opsIntervalMs: z16.number().positive(),
8622
+ signalRepeatMs: z16.number().positive(),
8623
+ costReportMs: z16.number().positive(),
8624
+ staleThreadMs: z16.number().positive(),
8625
+ idleMemberMs: z16.number().positive(),
8626
+ homeConflictMs: z16.number().positive(),
8627
+ scaleCooldownMs: z16.number().positive(),
8628
+ reflectionMs: z16.number().positive()
8201
8629
  }).partial();
8202
8630
  function parseTimings(input) {
8203
8631
  const parsed = SchedulerTimingsSchema.parse(input);
@@ -8209,32 +8637,32 @@ function parseTimings(input) {
8209
8637
  }
8210
8638
  return overrides;
8211
8639
  }
8212
- var PendingSchema = z14.object({
8640
+ var PendingSchema = z16.object({
8213
8641
  dispatch: TurnDispatchSchema,
8214
- readyAt: z14.number(),
8215
- stageWakes: z14.array(TriggerSchema).optional(),
8216
- holder: z14.boolean().optional(),
8642
+ readyAt: z16.number(),
8643
+ stageWakes: z16.array(TriggerSchema).optional(),
8644
+ holder: z16.boolean().optional(),
8217
8645
  newestMessage: UlidSchema.optional(),
8218
- onlyMessages: z14.boolean().optional(),
8646
+ onlyMessages: z16.boolean().optional(),
8219
8647
  steeredThrough: UlidSchema.optional(),
8220
- onlyCrons: z14.boolean().optional()
8648
+ onlyCrons: z16.boolean().optional()
8221
8649
  });
8222
- var StateSchema = z14.object({
8223
- cursor: z14.string().nullable(),
8224
- lastHeartbeat: z14.record(z14.string(), z14.string()),
8650
+ var StateSchema = z16.object({
8651
+ cursor: z16.string().nullable(),
8652
+ lastHeartbeat: z16.record(z16.string(), z16.string()),
8225
8653
  /** Waiting stages already signalled, as `task:stage`, so each is logged once while it waits. */
8226
- waitingSeen: z14.record(z14.string(), z14.string()).default({}),
8654
+ waitingSeen: z16.record(z16.string(), z16.string()).default({}),
8227
8655
  /** Queued dispatches survive a restart; the cursor has already moved past the events that made them. */
8228
- pending: z14.record(z14.string(), PendingSchema).default({}),
8229
- opsReported: z14.record(z14.string(), z14.string()).default({}),
8230
- releaseCounts: z14.record(z14.string(), z14.number().int().nonnegative()).default({}),
8231
- lastOps: z14.string().nullable().default(null),
8232
- lastScaled: z14.record(z14.string(), z14.string()).default({}),
8233
- lastCostReport: z14.string().nullable().default(null),
8234
- costSinceReport: z14.number().nonnegative().default(0),
8235
- turnsSinceReport: z14.number().int().nonnegative().default(0),
8656
+ pending: z16.record(z16.string(), PendingSchema).default({}),
8657
+ opsReported: z16.record(z16.string(), z16.string()).default({}),
8658
+ releaseCounts: z16.record(z16.string(), z16.number().int().nonnegative()).default({}),
8659
+ lastOps: z16.string().nullable().default(null),
8660
+ lastScaled: z16.record(z16.string(), z16.string()).default({}),
8661
+ lastCostReport: z16.string().nullable().default(null),
8662
+ costSinceReport: z16.number().nonnegative().default(0),
8663
+ turnsSinceReport: z16.number().int().nonnegative().default(0),
8236
8664
  /** When each member last reflected, or was first seen; the cadence counts from here. */
8237
- lastReflection: z14.record(z14.string(), z14.string()).default({})
8665
+ lastReflection: z16.record(z16.string(), z16.string()).default({})
8238
8666
  });
8239
8667
  function messageOf(trigger) {
8240
8668
  return trigger.kind === "mention" || trigger.kind === "user_post" ? trigger.messageId : void 0;
@@ -8492,7 +8920,7 @@ var Scheduler = class {
8492
8920
  return;
8493
8921
  }
8494
8922
  if (stringOf(payload["thread"]) !== taskId) {
8495
- await this.wakeHolder(taskId, event.actor, now);
8923
+ await this.wakeHolder(taskId, event.actor, event.actor, now);
8496
8924
  }
8497
8925
  await this.pruneStageWakes(taskId);
8498
8926
  return;
@@ -8518,6 +8946,7 @@ var Scheduler = class {
8518
8946
  if (creator !== null && creator !== event.actor) {
8519
8947
  await this.wakeCreator(taskId, project, creator, event.actor, now);
8520
8948
  }
8949
+ await this.wakeDependents(taskId, event.actor, now);
8521
8950
  const threaded = await this.board.readThread(taskId).then(() => true, () => false);
8522
8951
  if (!threaded) {
8523
8952
  this.sweep({ kind: "thread", id: taskId });
@@ -8537,12 +8966,17 @@ var Scheduler = class {
8537
8966
  }
8538
8967
  case "task.updated": {
8539
8968
  const taskId = stringOf(payload["taskId"]);
8540
- if (taskId !== null && payload["to"] === "abandoned") {
8969
+ if (taskId === null) {
8970
+ return;
8971
+ }
8972
+ if (payload["to"] === "abandoned") {
8541
8973
  delete this.state.releaseCounts[taskId];
8542
8974
  await this.queueClosing(taskId);
8975
+ await this.wakeDependents(taskId, event.actor, now);
8543
8976
  }
8544
- if (taskId !== null) {
8545
- await this.pruneStageWakes(taskId);
8977
+ await this.pruneStageWakes(taskId);
8978
+ if (Array.isArray(payload["blockedBy"]) && (await this.blockersInPlay(stringArray(payload["previousBlockedBy"]))).length > 0) {
8979
+ await this.wakeUnblocked(taskId, event.actor, now);
8546
8980
  }
8547
8981
  return;
8548
8982
  }
@@ -8713,9 +9147,10 @@ var Scheduler = class {
8713
9147
  }
8714
9148
  /**
8715
9149
  * Entering a stage wakes its named citizen, else its last holder when work returns to it, else
8716
- * the project's members of its role, else every member of the project who may hold it.
9150
+ * the project's members of its role, else every member of the project who may hold it. A stage
9151
+ * of a blocked task wakes nobody until its last blocker ends (`wakeUnblocked`).
8717
9152
  */
8718
- async wakeStage(taskId, from, now, only) {
9153
+ async wakeStage(taskId, from, now, options = {}) {
8719
9154
  let task;
8720
9155
  try {
8721
9156
  task = (await this.board.findTask(taskId)).task;
@@ -8726,6 +9161,10 @@ var Scheduler = class {
8726
9161
  if (stage === void 0 || task.status !== "open" || task.completing) {
8727
9162
  return;
8728
9163
  }
9164
+ if ((await this.blockersInPlay(task.blockedBy)).length > 0) {
9165
+ return;
9166
+ }
9167
+ const { only, state = "is waiting for you" } = options;
8729
9168
  const lastHolder = stage.holders.at(-1);
8730
9169
  const targets = stage.agent !== void 0 ? [stage.agent] : lastHolder !== void 0 ? [lastHolder] : stage.role !== void 0 ? (await this.board.membersWithRole(task.project, stage.role)).map((a) => a.name) : (await this.board.projectMembers(task.project)).map((a) => a.name);
8731
9170
  for (const name of targets) {
@@ -8736,7 +9175,7 @@ var Scheduler = class {
8736
9175
  if (agent === null || !mayHoldStage(agent, task)) {
8737
9176
  continue;
8738
9177
  }
8739
- await this.enqueueStage(agent, task, from, now, "is waiting for you");
9178
+ await this.enqueueStage(agent, task, from, now, state);
8740
9179
  }
8741
9180
  }
8742
9181
  /**
@@ -8745,7 +9184,7 @@ var Scheduler = class {
8745
9184
  */
8746
9185
  async wakeNewcomer(name, project, now) {
8747
9186
  for (const task of await this.board.openTasks(project).catch(() => [])) {
8748
- await this.wakeStage(task.id, name, now, name);
9187
+ await this.wakeStage(task.id, name, now, { only: name });
8749
9188
  }
8750
9189
  }
8751
9190
  /**
@@ -8753,7 +9192,7 @@ var Scheduler = class {
8753
9192
  * worked in that conversation, which nothing else would start before the next heartbeat. Turns
8754
9193
  * of one conversation run one at a time, so the turn this queues is the one that sees the claim.
8755
9194
  */
8756
- async wakeHolder(taskId, holder, now) {
9195
+ async wakeHolder(taskId, holder, from, now, state = "is yours; its work happens here") {
8757
9196
  let task;
8758
9197
  try {
8759
9198
  task = (await this.board.findTask(taskId)).task;
@@ -8764,12 +9203,66 @@ var Scheduler = class {
8764
9203
  if (agent === null || task.status !== "claimed" || task.claimedBy !== holder) {
8765
9204
  return;
8766
9205
  }
8767
- const key = await this.enqueueStage(agent, task, holder, now, "is yours; its work happens here");
9206
+ const key = await this.enqueueStage(agent, task, from, now, state);
8768
9207
  const item = key === null ? void 0 : this.pending.get(key);
8769
9208
  if (item?.stageWakes !== void 0) {
8770
9209
  item.holder = true;
8771
9210
  }
8772
9211
  }
9212
+ /**
9213
+ * The ids among these that name a task still open or claimed, a landing one included. A task
9214
+ * blocked by any of them waits for them rather than for a holder: its stage wakes nobody, by
9215
+ * coming up or on a heartbeat, until the last one ends.
9216
+ */
9217
+ async blockersInPlay(ids) {
9218
+ const blocking = [];
9219
+ for (const id of ids) {
9220
+ try {
9221
+ const { task } = await this.board.findTask(id);
9222
+ if (task.status === "open" || task.status === "claimed") {
9223
+ blocking.push(id);
9224
+ }
9225
+ } catch {
9226
+ }
9227
+ }
9228
+ return blocking;
9229
+ }
9230
+ /**
9231
+ * A task's last blocker has ended, or its blockers were changed to none in play: its stage wakes
9232
+ * as on coming up, or its holder in the task's conversation, since nothing woke either for it
9233
+ * while it was blocked.
9234
+ */
9235
+ async wakeUnblocked(taskId, from, now) {
9236
+ let task;
9237
+ try {
9238
+ task = (await this.board.findTask(taskId)).task;
9239
+ } catch {
9240
+ return;
9241
+ }
9242
+ if (task.completing || (await this.blockersInPlay(task.blockedBy)).length > 0) {
9243
+ return;
9244
+ }
9245
+ if (task.status === "open") {
9246
+ await this.wakeStage(task.id, from, now, {
9247
+ state: "is waiting for you, now that nothing blocks it"
9248
+ });
9249
+ } else if (task.status === "claimed" && task.claimedBy !== void 0) {
9250
+ await this.wakeHolder(task.id, task.claimedBy, from, now, "is yours, and nothing blocks it any more");
9251
+ }
9252
+ }
9253
+ /** A task that ended unblocks every task in play that names it, once nothing else blocks them. */
9254
+ async wakeDependents(taskId, from, now) {
9255
+ for (const project of await this.board.listProjects()) {
9256
+ if (project.archived !== void 0) {
9257
+ continue;
9258
+ }
9259
+ for (const task of await this.board.listTasks(project.slug)) {
9260
+ if (task.blockedBy.includes(taskId) && (task.status === "open" || task.status === "claimed")) {
9261
+ await this.wakeUnblocked(task.id, from, now);
9262
+ }
9263
+ }
9264
+ }
9265
+ }
8773
9266
  /** Queues a stage wake in the task's conversation and returns its key, or null for no wake. */
8774
9267
  async enqueueStage(agent, task, from, now, state) {
8775
9268
  if (agent.status !== "active" || agent.cli === null) {
@@ -8805,9 +9298,9 @@ var Scheduler = class {
8805
9298
  }
8806
9299
  }
8807
9300
  /**
8808
- * The first of these stage wakes still due: its task waits, unheld, on a stage the agent may
8809
- * hold, or, with `holder`, the agent holds it, having claimed it outside the task's conversation.
8810
- * A wake for a stage the agent took in the turn the wake waited on is dropped.
9301
+ * The first of these stage wakes still due: its task waits, unheld and unblocked, on a stage the
9302
+ * agent may hold, or, with `holder`, the agent holds it, having claimed it outside the task's
9303
+ * conversation. A wake for a stage the agent took in the turn the wake waited on is dropped.
8811
9304
  */
8812
9305
  async stillWaiting(name, wakes, holder) {
8813
9306
  const agent = await this.tryReadAgent(name);
@@ -8823,7 +9316,7 @@ var Scheduler = class {
8823
9316
  if (task.completing || currentStage(task) === void 0) {
8824
9317
  continue;
8825
9318
  }
8826
- if (task.status === "open" && mayHoldStage(agent, task)) {
9319
+ if (task.status === "open" && mayHoldStage(agent, task) && (await this.blockersInPlay(task.blockedBy)).length === 0) {
8827
9320
  return wake;
8828
9321
  }
8829
9322
  if (holder && task.status === "claimed" && task.claimedBy === agent.name) {
@@ -9046,9 +9539,9 @@ var Scheduler = class {
9046
9539
  /**
9047
9540
  * A heartbeat per member and scope on the cadence, which then looks at every conversation of the
9048
9541
  * scope: the unread messages of its home, of each other channel, and of each thread, a stage the
9049
- * member holds, and a stage
9050
- * waiting that it may take, each in its task's thread. Each conversation with something wakes on
9051
- * its own, so a heartbeat never brings one task's business into another's turn.
9542
+ * member holds, and a stage of an unblocked task waiting that it may take, each in its task's
9543
+ * thread. Each conversation with something wakes on its own, so a heartbeat never brings one
9544
+ * task's business into another's turn.
9052
9545
  */
9053
9546
  async checkHeartbeats(now) {
9054
9547
  for (const agent of await this.board.listAgents()) {
@@ -9100,7 +9593,7 @@ var Scheduler = class {
9100
9593
  }
9101
9594
  if (project !== SOCIETY_SCOPE) {
9102
9595
  for (const task of await this.board.openTasks(project)) {
9103
- if (mayHoldStage(agent, task)) {
9596
+ if (mayHoldStage(agent, task) && (await this.blockersInPlay(task.blockedBy)).length === 0) {
9104
9597
  reasonsIn({ thread: { id: task.id, task: true } }).waiting += 1;
9105
9598
  }
9106
9599
  }
@@ -9252,9 +9745,10 @@ var Scheduler = class {
9252
9745
  return latest;
9253
9746
  }
9254
9747
  /**
9255
- * A current stage without a holder past the threshold is logged as a signal once while it waits.
9256
- * It wakes nobody by itself: its assignees see the stage on their heartbeat, and the steward reads
9257
- * the signal at its next turn and decides whether to replan or to mention someone.
9748
+ * A current stage without a holder past the threshold is logged as a signal once while it waits,
9749
+ * naming the tasks that still block it. It wakes nobody by itself: its assignees see the stage on
9750
+ * their heartbeat, or a blocked one when its last blocker ends, and the steward reads the signal
9751
+ * at its next turn and decides whether to replan or to mention someone.
9258
9752
  */
9259
9753
  async checkWaitingStages(now) {
9260
9754
  const stillWaiting = /* @__PURE__ */ new Set();
@@ -9275,10 +9769,12 @@ var Scheduler = class {
9275
9769
  }
9276
9770
  this.state.waitingSeen[key] = iso(now);
9277
9771
  const assignee2 = stage.agent ?? stage.role ?? "anyone in the project";
9772
+ const blockers = await this.blockersInPlay(task.blockedBy);
9773
+ const waited = blockers.length === 0 ? "for a holder" : `blocked by ${blockers.map((id) => `task ${id}`).join(", ")}`;
9278
9774
  await this.board.publishSignal({
9279
9775
  kind: "waiting_stage",
9280
9776
  key: `waiting_stage:${key}`,
9281
- summary: `stage "${stage.name}" of task ${task.id} "${task.title}" in ${project.slug}, for ${assignee2}, has waited ${describeDuration(age)} for a holder`,
9777
+ summary: `stage "${stage.name}" of task ${task.id} "${task.title}" in ${project.slug}, for ${assignee2}, has waited ${describeDuration(age)} ${waited}`,
9282
9778
  value: age,
9283
9779
  threshold: this.timings.waitingStageMs,
9284
9780
  project: project.slug,
@@ -9739,8 +10235,9 @@ var TURN_CONTRACT = [
9739
10235
  "- 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.",
9740
10236
  "- Silence is allowed. If the digest needs no reply, post nothing.",
9741
10237
  "- 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.",
9742
- "- 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.",
10238
+ "- An @mention wakes the citizen named, and every wake costs a turn. Address someone with @ only when you need them to act; when you merely refer to citizens, write their names plainly. A name in a code span or a code block is no mention, so code keeps its @ as written. @user wakes nobody, since the user takes no turns; it is what puts a question before the user, so use it only when you need the user to answer or decide.",
9743
10239
  "- A thread is a conversation off a channel that reaches only its participants and whoever is mentioned in it. Every task has one, opened with it under the task's id on the channel of its project it was filed in, general unless it was filed in another: talk about a task there, with post_message and its thread_id, and the notes you give advance_task and update_task are posted there as you. Its participants are the task's creator, its stage holders and assignees, whoever may take the stage that waits, and anyone who posted; it closes when the task ends. Every proposal has one too, in governance, opened with its pitch and closed by its decision. Open other threads with open_thread on a channel with a title, for any other topic; close_thread posts your summary to its channel.",
10240
+ "- To show an image in a post, such as a screenshot or a chart, commit the file to a task's branch and link it by its path in the task's worktree, as `![what it shows](<path>)`: the board shows a file on a task's branch in place once it is committed, at the latest when your turn ends. An image at an https address shows too. Never put an image or any other file into a message as base64 or a data: URL: the board does not show it, and every reader of the conversation gets it in full in their prompt.",
9744
10241
  "- Route every lesson: about you, your craft, or the user, write it to memory/core.md in your home directory, and keep that file short by moving detail to memory/<topic>.md, your archive; a durable fact about a project's codebase or process goes through `write_knowledge` on that project; something the whole project should know now, post it to the project channel, and anything about one task, in the task's thread.",
9745
10242
  '- A procedure you have followed twice is a skill: write it to skills/<name>/SKILL.md in your home, frontmatter with `name` and a one-line `description` and then the steps, and your skills index lists it from the next turn. Files the steps use, such as scripts/ and references/, go in the same folder, named in SKILL.md by paths relative to it. Propose it with kind "skill" when the whole society would use it.',
9746
10243
  "- To take a published skill for yourself, install it in a temporary folder and copy it into your home: in a folder from `mktemp -d`, `npx -y skills add <source> --skill <name> -a universal --copy -y` writes `.agents/skills/<name>/` and a lock file there, and you copy that skill folder to skills/<name>/ in your home, named in lowercase letters, digits, and hyphens. Never install one inside a worktree, where it would be committed into the project, or with -g, which writes to the runner's own home, shared by every citizen on it and listed in no skills index.",
@@ -9754,32 +10251,33 @@ var PLANNING = [
9754
10251
  "- Name stages by what gets done. Plan the next few steps rather than everything, and reshape with plan_task as the work teaches you.",
9755
10252
  "- Assign a stage to a role when anyone in it could do it, to a citizen only when it must be them, and to nobody when anyone in the project could. A citizen named on a stage must be a member of the task's project; add them first. A stage wakes everyone it is open to when it comes up, so the narrower the assignee, the fewer turns it costs.",
9756
10253
  "- Reshape rather than force: another round is an inserted stage, and a wait on something outside the society is a stage named for what it waits on. When a check finds work unfinished, send the task back to an earlier stage with update_task.",
10254
+ "- A task that cannot start until other tasks finish names them with update_task blocked_by. While any of them is in play, its stage wakes nobody, neither when it comes up nor on a heartbeat; once the last has ended, done or abandoned, the stage wakes as if it had just come up, or wakes its holder. Nobody needs to clear the block or mention anyone when they end; change blocked_by only when the plan changes.",
9757
10255
  "- A gated stage is an independent check: nobody who held an earlier stage of the task may hold it, and only the user, the steward, and the concierge may add, remove, move, reassign, or ungate it. Ask them in the task's thread when a gate should change.",
9758
10256
  "- Commit your work on the task's branch before you advance, and say what you did in the advance note; it is the handover the next holder reads in the task's thread."
9759
10257
  ].join("\n");
9760
10258
  var GOVERNANCE = [
9761
- "- Anything the society lacks is a proposal: call `propose` with a kind and a charter. It opens the proposal's thread in governance with your rationale, where it is discussed and where the decision is posted. Approval provisions it on the spot, and either decision wakes the proposer. The user decides members, roles, retirements, and archives; the steward may also decide channels, reallocations, and skills. Nobody decides their own proposal.",
10259
+ "- Anything the society lacks is a proposal: call `propose` with a kind and a charter. It opens the proposal's thread in governance with your rationale, where it is discussed and where the decision is posted. Approval provisions it on the spot, and either decision wakes the proposer. The user decides members, roles, retirements, archives, and reallocations that change a role's verbs or replicas; the steward may also decide channels, skills, and other reallocations. Nobody decides their own proposal.",
9762
10260
  "- Charter shapes, as JSON objects:",
9763
10261
  ' - member: {"name", "role", "cli": "claude" or "codex", "memberships": [project slugs], "model"?, "homeRunner"?, "subscriptions"?, "seedInstructions"?}',
9764
10262
  ' - role: {"name", "purpose", "verbs": [board verbs], "wakeTriggers"?: any of "user_post", "ops_event", "heartbeat" (heartbeat when omitted), "maxReplicas"?, "backlogThreshold"?, "societyScope"?: true for a society role, whose members are never in a project and keep watch over the whole society}. Mentions, stages that become yours, and finished tasks you created wake every role; a heartbeat wakes you when something is unread, held, or waiting for you.',
9765
10263
  ' - channel: {"project": slug or null, "name", "purpose"}',
9766
10264
  ' - retirement: {"agent", "reason"}',
9767
10265
  ' - archive: {"project", "reason"}: approval archives the project: 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',
9768
- ' - reallocation: {"description"}',
9769
- ' - skill: {"name", "summary"}: the folder skills/<name>/ in your home, SKILL.md and every file beside it, which the board takes as it stands when this turn ends into society/proposals/<id>/, where whoever decides reads it; a proposal whose folder has no SKILL.md then is rejected by the board. Approval publishes it under society/skills/<name>/, where every citizen\'s skills index lists it until the steward or the user removes it with `remove_skill`; ask the steward to remove one that is wrong or superseded. A proposal from before folders carries "body", its SKILL.md text alone',
10266
+ ` - reallocation: {"role", "changes": {"maxReplicas"?, "backlogThreshold"?, "wakeTriggers"?, "verbs"?}, "description"}: approval applies the changes to the existing role, each the field's new value and verbs and wakeTriggers whole lists, for its members from their next turn; refused for a role that does not exist or that has those values already. Only the user decides one that changes verbs or maxReplicas`,
10267
+ ' - skill: {"name", "summary"}: the folder skills/<name>/ in your home, SKILL.md and every file beside it, which the board takes as it stands when this turn ends into society/proposals/<id>/, where whoever decides reads it; a proposal whose folder has no SKILL.md then is rejected by the board. Approval publishes it under society/skills/<name>/, where every citizen\'s skills index lists it until the steward or the user removes it with `remove_skill`; ask the steward to remove one that is wrong or superseded. Once yours is approved, delete skills/<name>/ from your home and report memoryUpdated: the board leaves your copy there, and your index would list it beside the published one and follow it as it drifts from what was reviewed. To revise a published skill, copy its folder home, edit it, and propose it again. A proposal from before folders carries "body", its SKILL.md text alone',
9770
10268
  "- Projects and membership: a project is a lasting area of work, not one request; `create_project` opens one with its general channel (front desk and user), and `configure_project` sets its completion effect, its display name, or its default branch, never its slug (user, steward, concierge). A workstream within a project or the society, such as a release, gets a channel of its own: `create_channel` opens it, which its opener follows, and announces it in the place's general, where members subscribe when it concerns them, `create_task` with `channel` files the workstream's tasks there, and `archive_channel` ends it once none of its tasks is in play, leaving its history readable (user, steward, concierge; others propose a channel). A project whose work is finished or has moved to another project is archived by proposal, once no task there is in play. `join_project` and `leave_project` move yourself, or another citizen when you are the concierge, the steward, or the user; joining gives the pair a worktree and an onboarding turn. A society role, one that keeps watch over the whole society as the concierge and the steward do, is never in a project: it moves others, never itself.",
9771
- "- Prefer scaling an existing role over inventing one; a new role is justified by work a project needs that no existing role covers. A role that needs a tool the board lacks is an engineering task, not a hiring request.",
10269
+ "- Prefer scaling an existing role over inventing one, with a reallocation of its replicas, backlog threshold, wakes, or verbs; a new role is justified by work a project needs that no existing role covers. A role that needs a tool the board lacks is an engineering task, not a hiring request.",
9772
10270
  "- Operations signals are counters and timers the board logs, listed in the prompt of a role that reads them: stages waiting for a holder, backlog per role, stages waiting on a role nobody fills, churn, stale threads, idle members, missing capabilities, replicas added, spend. They are not posts in any channel; interpreting them is your judgment."
9773
10271
  ].join("\n");
9774
- function renderOnboardingPreamble(context) {
10272
+ function renderOnboardingPreamble(context2) {
9775
10273
  return [
9776
- `This is your first turn as ${context.agentName}.`,
10274
+ `This is your first turn as ${context2.agentName}.`,
9777
10275
  "",
9778
- `Your role in one paragraph: ${context.roleSummary}`,
10276
+ `Your role in one paragraph: ${context2.roleSummary}`,
9779
10277
  "",
9780
- `You are working on project "${context.project}" in the worktree at ${context.worktree}.`,
9781
- ...(context.otherChannels ?? []).length === 0 ? [] : [
9782
- `You follow its general channel. It also has ${(context.otherChannels ?? []).map((ref) => `#${ref}`).join(", ")}, each a workstream such as a release: subscribe to one when your work there needs its posts. Its tasks and mentions reach you either way.`
10278
+ `You are working on project "${context2.project}" in the worktree at ${context2.worktree}.`,
10279
+ ...(context2.otherChannels ?? []).length === 0 ? [] : [
10280
+ `You follow its general channel. It also has ${(context2.otherChannels ?? []).map((ref) => `#${ref}`).join(", ")}, each a workstream such as a release: subscribe to one when your work there needs its posts. Its tasks and mentions reach you either way.`
9783
10281
  ],
9784
10282
  "Your memory is empty. Write durable lessons about yourself, your craft, or the user to memory/core.md in your home directory.",
9785
10283
  "Write profile.md in your home directory: one paragraph on what you do well and what to send your way. The roster the front desk routes with is built from it.",
@@ -9865,10 +10363,25 @@ function signalLine(ts, signal) {
9865
10363
  ].filter((fact) => fact.length > 0);
9866
10364
  return `- [${ts}] ${signal.kind}: ${signal.summary} (${facts.join("; ")})`;
9867
10365
  }
9868
- function runnerLine(runner, projects) {
10366
+ function quotaPhrase(cli, quota, now) {
10367
+ const level = quotaLevel(quota, now);
10368
+ if (level === "none") {
10369
+ return `${cli} reports no plan limits`;
10370
+ }
10371
+ if (level === "limited") {
10372
+ return `${cli} limited until ${quota.limitedUntil ?? "an unknown time"}`;
10373
+ }
10374
+ const windows = quota.windows.map((window) => !windowCurrent(window, now) ? `${windowName(window)} reset since` : `${windowName(window)} ${window.usedPercent === null ? "?" : Math.floor(window.usedPercent)}%`);
10375
+ return `${cli} ${windows.length === 0 ? "allowed" : windows.join(", ")}${level === "warning" ? " (near a limit)" : ""} as of ${quota.observedAt}`;
10376
+ }
10377
+ function runnerLine(runner, projects, now) {
9869
10378
  const living = projects.filter((project) => project.runner === runner.name).map((project) => project.slug);
9870
10379
  const state = runner.status === "connected" ? "connected" : `away${runner.lastSeen === void 0 ? "" : ` since ${runner.lastSeen}`}`;
9871
- return `- ${runner.name}: ${state}; ${runner.os}; CLIs ${runner.clis.length === 0 ? "none" : runner.clis.join(", ")}; offers ${runner.capabilities.length === 0 ? "nothing beyond its CLIs" : runner.capabilities.join(", ")}; ${living.length === 0 ? "no projects" : `projects ${living.join(", ")}`}`;
10380
+ const quotas = runner.clis.flatMap((cli) => {
10381
+ const quota = runner.quotas?.[cli];
10382
+ return quota === void 0 ? [] : [quotaPhrase(cli, quota, now)];
10383
+ });
10384
+ return `- ${runner.name}: ${state}; ${runner.os}; CLIs ${runner.clis.length === 0 ? "none" : runner.clis.join(", ")}; offers ${runner.capabilities.length === 0 ? "nothing beyond its CLIs" : runner.capabilities.join(", ")}; ${living.length === 0 ? "no projects" : `projects ${living.join(", ")}`}${quotas.length === 0 ? "" : `; quota ${quotas.join("; ")}`}`;
9872
10385
  }
9873
10386
  function profileLine(profile) {
9874
10387
  const line = profile.split("\n").map((entry) => entry.trim()).find((entry) => entry.length > 0 && !entry.startsWith("#"));
@@ -9951,7 +10464,7 @@ function buildTurnPrompt(input) {
9951
10464
  lines.push("A task you created is done. Tell whoever asked for it, if anyone did.");
9952
10465
  }
9953
10466
  if (dispatch.trigger.kind === "proposal_decided") {
9954
- 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.");
10467
+ 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. An approved skill is published under society/skills/<name>/: delete your own skills/<name>/ from your home and report memoryUpdated, so your index lists the published copy alone.");
9955
10468
  }
9956
10469
  if (dispatch.trigger.kind === "cron") {
9957
10470
  lines.push("A cron came due: what it asks is under Crons below.");
@@ -10018,12 +10531,12 @@ function buildTurnPrompt(input) {
10018
10531
  }
10019
10532
  const machines = input.runners;
10020
10533
  if (machines !== void 0 && machines !== null) {
10021
- lines.push("", "## Runners", "", "The machines turns run on. A project lives on the runner that takes its first turn, one that offers every capability the project and that turn's task require, and never moves. A task's required_capabilities must name capabilities exactly as a runner below offers them; a task needing one its project's runner lacks waits.", "");
10534
+ lines.push("", "## Runners", "", "The machines turns run on. A project lives on the runner that takes its first turn, one that offers every capability the project and that turn's task require, and never moves. A task's required_capabilities must name capabilities exactly as a runner below offers them; a task needing one its project's runner lacks waits. A runner's quota is what each CLI's login there last reported of its plan, in percent of each window; a CLI limited until a time fails its turns there until then.", "");
10022
10535
  if (machines.runners.length === 0) {
10023
10536
  lines.push("None registered yet.");
10024
10537
  }
10025
10538
  for (const runner of machines.runners) {
10026
- lines.push(runnerLine(runner, machines.projects));
10539
+ lines.push(runnerLine(runner, machines.projects, input.clock?.now ?? /* @__PURE__ */ new Date()));
10027
10540
  }
10028
10541
  }
10029
10542
  const knowledge = input.knowledge;
@@ -10556,6 +11069,11 @@ var TurnHost = class {
10556
11069
  }
10557
11070
  }
10558
11071
  });
11072
+ if (outcome.quota !== void 0) {
11073
+ await this.board.recordQuota(turn.runner, job.cli, outcome.quota).catch((error) => {
11074
+ this.log.warn({ runner: turn.runner, cli: job.cli, error: String(error) }, "could not record a quota");
11075
+ });
11076
+ }
10559
11077
  if (recorded) {
10560
11078
  this.log.info({
10561
11079
  agent: job.agent,
@@ -10737,7 +11255,7 @@ var TurnHost = class {
10737
11255
  if (await this.board.hasMentioned(actor.name, USER_NAME, since)) {
10738
11256
  return;
10739
11257
  }
10740
- const proposed = (await this.board.listProposals()).some((proposal) => proposal.proposedBy === actor.name && proposal.createdAt >= since && ROLE_KIND_APPROVERS[proposal.kind].includes(USER_ROLE));
11258
+ const proposed = (await this.board.listProposals()).some((proposal) => proposal.proposedBy === actor.name && proposal.createdAt >= since && proposalApprovers(proposal.kind, proposal.charter).includes(USER_ROLE));
10741
11259
  if (proposed) {
10742
11260
  return;
10743
11261
  }
@@ -10885,7 +11403,8 @@ var RunnerHub = class {
10885
11403
  os: hello.os,
10886
11404
  clis: hello.clis,
10887
11405
  capabilities: hello.capabilities,
10888
- version: hello.version
11406
+ version: hello.version,
11407
+ gitIdentityMissing: hello.gitIdentityMissing ?? null
10889
11408
  });
10890
11409
  this.runnerChanged(name);
10891
11410
  this.log.info({ runner: name, version: hello.version, clis: hello.clis, slots: hello.slots }, "runner registered");
@@ -11327,6 +11846,26 @@ var RunnerHub = class {
11327
11846
  }
11328
11847
  return ModelListSchema.parse(await this.request(seat, (request) => ({ type: "models", request, cli })));
11329
11848
  }
11849
+ /**
11850
+ * A CLI's plan quota, read on a runner without a turn and recorded on the runner's record.
11851
+ * Refused for a runner not connected with the CLI, and for one from before quota reads, which
11852
+ * would skip the request and leave the reader waiting.
11853
+ */
11854
+ async quota(runner, cli) {
11855
+ const seat = this.seats.get(runner);
11856
+ if (seat === void 0 || seat.send === null || !seat.hello.clis.includes(cli)) {
11857
+ throw new RunnerAwayError(`runner ${runner} is not connected with ${cli}`);
11858
+ }
11859
+ if (!seat.hello.quotaReads) {
11860
+ throw new BoardError("INVALID_STATE", `runner ${runner} cannot read quotas yet; upgrade it`);
11861
+ }
11862
+ const quota = CliQuotaSchema.nullable().parse(await this.request(seat, (request) => ({ type: "quota", request, cli }), FILE_TIMEOUT_MS));
11863
+ if (quota === null) {
11864
+ throw new BoardError("NOT_FOUND", `${cli} on runner ${runner} cannot read its quota`);
11865
+ }
11866
+ await this.board.recordQuota(runner, cli, quota);
11867
+ return quota;
11868
+ }
11330
11869
  /** Calls `listener` whenever a runner registers, connects, or goes away. */
11331
11870
  onRunnerChange(listener) {
11332
11871
  this.runnerListeners.add(listener);
@@ -11559,20 +12098,20 @@ async function handleMcpRequest(board2, actor, request, version) {
11559
12098
  import { Hono as Hono3 } from "hono";
11560
12099
  import { compress } from "hono/compress";
11561
12100
  import { streamSSE as streamSSE2 } from "hono/streaming";
11562
- import { z as z16, ZodError } from "zod";
12101
+ import { z as z18, ZodError } from "zod";
11563
12102
 
11564
12103
  // apps/server/dist/github-auth.js
11565
12104
  import { randomBytes as randomBytes2 } from "node:crypto";
11566
12105
  import { Hono } from "hono";
11567
- import { z as z15 } from "zod";
12106
+ import { z as z17 } from "zod";
11568
12107
  var SIGN_IN_TTL_MS = 30 * 24 * 36e5;
11569
12108
  var STATE_TTL_MS = 10 * 6e4;
11570
12109
  var MAX_STATES = 1e3;
11571
- var TokenAnswerSchema = z15.object({ access_token: z15.string().min(1) });
11572
- var GithubUserSchema = z15.object({
11573
- login: z15.string().min(1),
11574
- id: z15.number().int(),
11575
- avatar_url: z15.string().optional()
12110
+ var TokenAnswerSchema = z17.object({ access_token: z17.string().min(1) });
12111
+ var GithubUserSchema = z17.object({
12112
+ login: z17.string().min(1),
12113
+ id: z17.number().int(),
12114
+ avatar_url: z17.string().optional()
11576
12115
  });
11577
12116
  function githubAvatar(url) {
11578
12117
  try {
@@ -11995,7 +12534,7 @@ async function isFile(file) {
11995
12534
  return false;
11996
12535
  }
11997
12536
  }
11998
- function within(root, pathname) {
12537
+ function within2(root, pathname) {
11999
12538
  let decoded;
12000
12539
  try {
12001
12540
  decoded = decodeURIComponent(pathname);
@@ -12012,7 +12551,7 @@ function spaHandler(staticDir) {
12012
12551
  if (pathname.startsWith("/api/")) {
12013
12552
  return c.json({ error: "NOT_FOUND", message: `no route ${pathname}` }, 404);
12014
12553
  }
12015
- const file = within(root, pathname);
12554
+ const file = within2(root, pathname);
12016
12555
  const headers = { "x-content-type-options": "nosniff" };
12017
12556
  if (file !== null && await isFile(file)) {
12018
12557
  return c.body(await readFile5(file), 200, {
@@ -12047,19 +12586,19 @@ var ERROR_STATUS = {
12047
12586
  INVALID_STATE: 409
12048
12587
  };
12049
12588
  var EVENT_PAGE = 100;
12050
- var RetireBodySchema = z16.object({ reason: z16.string().min(1) });
12051
- var RunnerBodySchema = z16.object({ name: NameSchema });
12052
- var EnrollmentApprovalSchema = z16.object({ name: NameSchema });
12053
- var HomeRunnerBodySchema = z16.object({ runner: NameSchema.nullable() });
12054
- var ModelBodySchema = z16.object({ model: ModelNameSchema.nullable() });
12055
- var EffortBodySchema = z16.object({ effort: EffortSchema.nullable() });
12056
- var HistoryLimitSchema = z16.coerce.number().int().min(1).max(500);
12057
- var MessageIdsSchema = z16.string().transform((ids) => ids.split(",").filter((id) => id !== "")).pipe(z16.array(UlidSchema).min(1).max(MESSAGE_LOOKUP_LIMIT));
12058
- var TimezoneBodySchema = z16.object({ timezone: TimeZoneSchema });
12059
- var ChannelBodySchema = z16.object({
12589
+ var RetireBodySchema = z18.object({ reason: z18.string().min(1) });
12590
+ var RunnerBodySchema = z18.object({ name: NameSchema });
12591
+ var EnrollmentApprovalSchema = z18.object({ name: NameSchema });
12592
+ var HomeRunnerBodySchema = z18.object({ runner: NameSchema.nullable() });
12593
+ var ModelBodySchema = z18.object({ model: ModelNameSchema.nullable() });
12594
+ var EffortBodySchema = z18.object({ effort: EffortSchema.nullable() });
12595
+ var HistoryLimitSchema = z18.coerce.number().int().min(1).max(500);
12596
+ var MessageIdsSchema = z18.string().transform((ids) => ids.split(",").filter((id) => id !== "")).pipe(z18.array(UlidSchema).min(1).max(MESSAGE_LOOKUP_LIMIT));
12597
+ var TimezoneBodySchema = z18.object({ timezone: TimeZoneSchema });
12598
+ var ChannelBodySchema = z18.object({
12060
12599
  project: NameSchema.nullable().default(null),
12061
12600
  name: NameSchema,
12062
- purpose: z16.string().min(1)
12601
+ purpose: z18.string().min(1)
12063
12602
  });
12064
12603
  function bearer2(c) {
12065
12604
  const header = c.req.header("authorization") ?? "";
@@ -12067,7 +12606,7 @@ function bearer2(c) {
12067
12606
  return match?.[1]?.trim() ?? null;
12068
12607
  }
12069
12608
  function createApp(deps) {
12070
- const { board: board2, version, turns: turns2, scheduler: scheduler2, models } = deps;
12609
+ const { board: board2, version, turns: turns2, scheduler: scheduler2, models, quotas } = deps;
12071
12610
  const eventStreamPingMs = deps.eventStreamPingMs ?? 15e3;
12072
12611
  const app2 = new Hono3();
12073
12612
  app2.get("/health", (c) => c.json({ ok: true, version }));
@@ -12137,6 +12676,17 @@ function createApp(deps) {
12137
12676
  api.get("/agents/:name/history/:commit", async (c) => c.json(await board2.homeChange(c.req.param("name"), c.req.param("commit"))));
12138
12677
  api.get("/roles", async (c) => c.json(await board2.listRoles()));
12139
12678
  api.get("/runners", async (c) => c.json(await board2.listRunners()));
12679
+ api.post("/runners/:name/quota/:cli", async (c) => {
12680
+ if (c.get("actor").role !== USER_ROLE) {
12681
+ throw new BoardError("FORBIDDEN", "only the user reads a runner's quota");
12682
+ }
12683
+ const runner = NameSchema.parse(c.req.param("name"));
12684
+ const cli = CliKindSchema.parse(c.req.param("cli"));
12685
+ if (quotas === void 0) {
12686
+ throw new RunnerAwayError("this server asks no runner for quotas");
12687
+ }
12688
+ return c.json(await quotas.read(runner, cli));
12689
+ });
12140
12690
  api.post("/runners", async (c) => {
12141
12691
  const body = RunnerBodySchema.parse(await c.req.json());
12142
12692
  return c.json(await board2.addRunner(c.get("actor"), body.name));
@@ -12173,6 +12723,10 @@ function createApp(deps) {
12173
12723
  const window = MetricsWindowSchema.parse(c.req.query("window") ?? "7d");
12174
12724
  return c.json(await board2.metrics(window, new Set(scheduler2?.activeSignals ?? [])));
12175
12725
  });
12726
+ api.get("/tokens", async (c) => {
12727
+ const window = MetricsWindowSchema.parse(c.req.query("window") ?? "24h");
12728
+ return c.json(await board2.tokenUse(window));
12729
+ });
12176
12730
  api.get("/society/knowledge", async (c) => c.json(await board2.listKnowledge(null)));
12177
12731
  api.get("/skills", async (c) => c.json(await board2.listSocietySkills()));
12178
12732
  api.post("/agents/:name/retire", async (c) => {
@@ -12216,7 +12770,7 @@ function createApp(deps) {
12216
12770
  });
12217
12771
  api.put("/roles/:name", async (c) => {
12218
12772
  const charter = RoleCharterSchema.parse({
12219
- ...z16.record(z16.string(), z16.unknown()).parse(await c.req.json()),
12773
+ ...z18.record(z18.string(), z18.unknown()).parse(await c.req.json()),
12220
12774
  name: c.req.param("name")
12221
12775
  });
12222
12776
  return c.json(await board2.setRoleCharter(c.get("actor"), charter));
@@ -12566,6 +13120,35 @@ var ModelCatalog = class {
12566
13120
  }
12567
13121
  };
12568
13122
 
13123
+ // apps/server/dist/quotas.js
13124
+ var MINUTE = 6e4;
13125
+ var QuotaReads = class {
13126
+ runners;
13127
+ floorMs;
13128
+ now;
13129
+ kept = /* @__PURE__ */ new Map();
13130
+ constructor(runners2, floorMs = MINUTE, now = Date.now) {
13131
+ this.runners = runners2;
13132
+ this.floorMs = floorMs;
13133
+ this.now = now;
13134
+ }
13135
+ read(runner, cli) {
13136
+ const key = `${runner}/${cli}`;
13137
+ const kept = this.kept.get(key);
13138
+ if (kept !== void 0 && this.now() - kept.at < this.floorMs) {
13139
+ return kept.quota;
13140
+ }
13141
+ const quota = this.runners.quota(runner, cli);
13142
+ this.kept.set(key, { at: this.now(), quota });
13143
+ quota.catch(() => {
13144
+ if (this.kept.get(key)?.quota === quota) {
13145
+ this.kept.delete(key);
13146
+ }
13147
+ });
13148
+ return quota;
13149
+ }
13150
+ };
13151
+
12569
13152
  // apps/server/dist/turn-hub.js
12570
13153
  var TurnHub = class {
12571
13154
  capacity;
@@ -12673,6 +13256,7 @@ var app = createApp({
12673
13256
  turns,
12674
13257
  scheduler: view,
12675
13258
  models: new ModelCatalog(runners),
13259
+ quotas: new QuotaReads(runners),
12676
13260
  runners,
12677
13261
  webDir,
12678
13262
  signIn: { github: config.github, log },