@kubitnodes/stellaris 0.3.24 → 0.3.26

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 (70) hide show
  1. package/dist/cli/index.js +99 -553
  2. package/dist/server/index.js +318 -581
  3. package/package.json +4 -4
  4. package/web/dist/assets/BackLink-UTiLF1J4.js +1 -0
  5. package/web/dist/assets/Button-Cp4cNRUX.js +1 -0
  6. package/web/dist/assets/ChannelForms-DD2B0tey.js +1 -0
  7. package/web/dist/assets/{ChannelView-BIXouQMX.js → ChannelView-CY3oG9hZ.js} +2 -2
  8. package/web/dist/assets/CitizenView-BNK5ro-6.js +2 -0
  9. package/web/dist/assets/{CitizensView-CYFS3f9U.js → CitizensView-WPQ6Jhl3.js} +1 -1
  10. package/web/dist/assets/{CliIcon-DVX5iLhG.js → CliIcon-C7bwChMP.js} +1 -1
  11. package/web/dist/assets/CronsView-_pdgwX1I.js +1 -0
  12. package/web/dist/assets/FileTree-jyDUSu7g.js +1 -0
  13. package/web/dist/assets/FileView-DcQvlc8V.js +4 -0
  14. package/web/dist/assets/KnowledgeView-DDl0HQPG.js +1 -0
  15. package/web/dist/assets/{Markdown--whKJBYV.js → Markdown-DT3V-EVe.js} +7 -7
  16. package/web/dist/assets/{MessageItem-BvDOeJci.js → MessageItem-agtEugfG.js} +1 -1
  17. package/web/dist/assets/MetricsView-DiXyYeWP.js +1 -0
  18. package/web/dist/assets/NeedsYouView-CKmuYan_.js +1 -0
  19. package/web/dist/assets/Overview-CKcTqsMM.js +1 -0
  20. package/web/dist/assets/{Pane-DTmByqgA.js → Pane-BaM0TdE6.js} +1 -1
  21. package/web/dist/assets/{Picker-DsHm4f7s.js → Picker-C2ig-0dT.js} +1 -1
  22. package/web/dist/assets/ProjectView-T2OHTh5j.js +1 -0
  23. package/web/dist/assets/ProposalView-B5yjTAcn.js +1 -0
  24. package/web/dist/assets/ProposalsView-p4KjwV9p.js +1 -0
  25. package/web/dist/assets/{Quota-D8WDWp5H.js → Quota-CfFGIql9.js} +1 -1
  26. package/web/dist/assets/RunnersView-DdvwoOur.js +1 -0
  27. package/web/dist/assets/{SkillFileList-DhdEcFQA.js → SkillFileList-CgrCnuaC.js} +1 -1
  28. package/web/dist/assets/SkillView-Di5Z9h2L.js +1 -0
  29. package/web/dist/assets/SocietyView-BDiVxUC5.js +1 -0
  30. package/web/dist/assets/StickyList-BaV_KZ8x.js +1 -0
  31. package/web/dist/assets/{SubjectThread-BEXt-tub.js → SubjectThread-DD_ZCCnr.js} +1 -1
  32. package/web/dist/assets/TaskView-CKcs-MOC.js +3 -0
  33. package/web/dist/assets/TasksView-oZMjncOU.js +1 -0
  34. package/web/dist/assets/{ThreadForms-DzoRDeBh.js → ThreadForms-CG5Y926b.js} +1 -1
  35. package/web/dist/assets/{ThreadItem-CDVppcOZ.js → ThreadItem-D-n2arFA.js} +1 -1
  36. package/web/dist/assets/{ThreadView-B4F10_j1.js → ThreadView-CT9vd0P4.js} +1 -1
  37. package/web/dist/assets/{citizen-BPIJzKdn.js → citizen-BiHfph2A.js} +1 -1
  38. package/web/dist/assets/{compose-BUhbVoI-.js → compose-DY1WL8zB.js} +1 -1
  39. package/web/dist/assets/format-CENiwkJC.js +55 -0
  40. package/web/dist/assets/governance-C6ez5MLh.js +1 -0
  41. package/web/dist/assets/index-CPXALUV6.js +23 -0
  42. package/web/dist/assets/{index-Bj8MlyAn.css → index-DIa8Dlav.css} +1 -1
  43. package/web/dist/assets/mentions-BEL5U6ra.js +3 -0
  44. package/web/dist/assets/{tasks-CTaxXtGL.js → tasks-CAI8Eci7.js} +1 -1
  45. package/web/dist/assets/useMatch-CINzyDWa.js +1 -0
  46. package/web/dist/index.html +9 -9
  47. package/web/dist/assets/BackLink-DKMw_rHu.js +0 -1
  48. package/web/dist/assets/Button-BYJGqZ3Q.js +0 -1
  49. package/web/dist/assets/ChannelForms-CkYupEL2.js +0 -1
  50. package/web/dist/assets/CitizenView-C3AMFixP.js +0 -2
  51. package/web/dist/assets/CronsView-BGCcO7bH.js +0 -1
  52. package/web/dist/assets/KnowledgeView-DLLYjd1G.js +0 -1
  53. package/web/dist/assets/MetricsView-B-KUNOZm.js +0 -1
  54. package/web/dist/assets/NeedsYouView-Dee_bOKj.js +0 -1
  55. package/web/dist/assets/Overview-DFiHDFnA.js +0 -1
  56. package/web/dist/assets/ProjectView-DsRXW-qk.js +0 -1
  57. package/web/dist/assets/ProposalView-B-4P1S-s.js +0 -1
  58. package/web/dist/assets/ProposalsView-uEuj-ooV.js +0 -1
  59. package/web/dist/assets/RunnersView-BB3q65rG.js +0 -1
  60. package/web/dist/assets/SkillView-vAv7zvtJ.js +0 -1
  61. package/web/dist/assets/SocietyView-Dv4phSkz.js +0 -1
  62. package/web/dist/assets/StickyList-C4fC_KJv.js +0 -1
  63. package/web/dist/assets/TaskFileView-B9sex0tS.js +0 -4
  64. package/web/dist/assets/TaskView-BCT388kb.js +0 -3
  65. package/web/dist/assets/TasksView-CuPay34-.js +0 -1
  66. package/web/dist/assets/format-CvfmDNoY.js +0 -55
  67. package/web/dist/assets/governance-Bba0OHvJ.js +0 -1
  68. package/web/dist/assets/index-DD3J11PT.js +0 -23
  69. package/web/dist/assets/mentions-D838wlfQ.js +0 -3
  70. package/web/dist/assets/useMatch-B8j0hm1q.js +0 -1
@@ -768,7 +768,6 @@ var ReallocationProposalSchema = z5.object({
768
768
  changes: RoleChangesSchema,
769
769
  description: z5.string().min(1)
770
770
  });
771
- var LegacyReallocationSchema = z5.object({ description: z5.string().min(1) });
772
771
  function applyRoleChanges(role, changes) {
773
772
  return {
774
773
  ...role,
@@ -813,8 +812,7 @@ var ArchiveProposalSchema = z5.object({
813
812
  });
814
813
  var SkillProposalSchema = z5.object({
815
814
  name: NameSchema,
816
- summary: z5.string().min(1).max(200),
817
- body: z5.string().min(1).optional()
815
+ summary: z5.string().min(1).max(200)
818
816
  });
819
817
  var ProposalCharterSchemas = {
820
818
  role: RoleCharterSchema,
@@ -891,14 +889,10 @@ function describeCharter(kind, charter) {
891
889
  }
892
890
  case "reallocation": {
893
891
  const parsed = ReallocationProposalSchema.safeParse(charter);
894
- if (parsed.success) {
895
- const { role, changes, description } = parsed.data;
896
- return `reallocation of role ${role} (${describeRoleChanges(changes)}): ${description}`;
897
- }
898
- const legacy = LegacyReallocationSchema.safeParse(charter);
899
- if (!legacy.success)
892
+ if (!parsed.success)
900
893
  break;
901
- return `reallocation: ${legacy.data.description}`;
894
+ const { role, changes, description } = parsed.data;
895
+ return `reallocation of role ${role} (${describeRoleChanges(changes)}): ${description}`;
902
896
  }
903
897
  case "skill": {
904
898
  const parsed = SkillProposalSchema.safeParse(charter);
@@ -1109,12 +1103,10 @@ var VerbInputs = {
1109
1103
  channel: ChannelRefSchema.optional(),
1110
1104
  limit: z7.number().int().positive().max(100).default(20)
1111
1105
  }),
1112
- /** On a task or a proposal, whose id the thread takes, or on a channel with a title. */
1106
+ /** On a channel with a title; a task's and a proposal's open with them. */
1113
1107
  open_thread: z7.object({
1114
- task_id: UlidSchema.optional(),
1115
- proposal_id: UlidSchema.optional(),
1116
- channel: ChannelRefSchema.optional(),
1117
- title: z7.string().min(1).max(200).optional()
1108
+ channel: ChannelRefSchema,
1109
+ title: z7.string().min(1).max(200)
1118
1110
  }),
1119
1111
  close_thread: z7.object({ thread_id: UlidSchema, summary: z7.string().min(1) }),
1120
1112
  create_task: z7.object({
@@ -1628,6 +1620,12 @@ var TurnOutcomeSchema = z9.object({
1628
1620
  * runner from before never sends it.
1629
1621
  */
1630
1622
  resumeFailed: ResumeFailureSchema.optional(),
1623
+ /**
1624
+ * Set when the model service turned the turn away for now, as when it was overloaded or failed
1625
+ * on its side, which the scheduler answers by sending the same wake again within minutes. A
1626
+ * runner from before never sends it.
1627
+ */
1628
+ transient: z9.boolean().optional(),
1631
1629
  /** The model the CLI reported, when it reported one. */
1632
1630
  model: z9.string().nullable(),
1633
1631
  work: TurnWorkSchema.nullable(),
@@ -1953,6 +1951,8 @@ var TurnRecordSchema = z10.object({
1953
1951
  stoppedBy: NameSchema.optional(),
1954
1952
  /** Why the CLI could not resume the turn's session, for a turn that failed so. */
1955
1953
  resumeFailed: ResumeFailureSchema.optional(),
1954
+ /** Whether the model service turned the turn away for now: overloaded, or failing on its side. */
1955
+ transient: z10.boolean().optional(),
1956
1956
  /** How many turns in a row, this one included, could not resume the session for a `retry`. */
1957
1957
  resumeRetries: z10.number().int().positive().optional(),
1958
1958
  /** Whether the server dropped the conversation's session after this turn, which the next starts anew. */
@@ -2027,7 +2027,8 @@ var OPS_SIGNAL_KINDS = [
2027
2027
  "runner",
2028
2028
  "home_conflict",
2029
2029
  "stuck_workspace",
2030
- "cron_failing"
2030
+ "cron_failing",
2031
+ "turn_refused"
2031
2032
  ];
2032
2033
  var OpsSignalKindSchema = z11.enum(OPS_SIGNAL_KINDS);
2033
2034
  var WAKING_SIGNAL_KINDS = [
@@ -2051,8 +2052,18 @@ var OpsSignalSchema = z11.object({
2051
2052
  project: NameSchema.optional(),
2052
2053
  agent: NameSchema.optional(),
2053
2054
  role: NameSchema.optional(),
2054
- taskId: UlidSchema.optional()
2055
+ taskId: UlidSchema.optional(),
2056
+ /**
2057
+ * Whether this log of the condition wakes the roles charted for `ops_event`, as its publisher
2058
+ * decided: the operations pass wakes them when a condition is new, and again only at the steps
2059
+ * it names, not at every repeat. Absent, the kind decides.
2060
+ */
2061
+ wakes: z11.boolean().optional()
2055
2062
  });
2063
+ var WAKING = new Set(WAKING_SIGNAL_KINDS);
2064
+ function signalWakes(signal) {
2065
+ return signal.wakes ?? WAKING.has(signal.kind);
2066
+ }
2056
2067
 
2057
2068
  // packages/shared/dist/metrics.js
2058
2069
  import { z as z12 } from "zod";
@@ -2246,7 +2257,7 @@ var RunnerConfigSchema = z13.object({
2246
2257
  });
2247
2258
 
2248
2259
  // packages/shared/dist/version.js
2249
- var STELLARIS_VERSION = "0.3.24";
2260
+ var STELLARIS_VERSION = "0.3.26";
2250
2261
 
2251
2262
  // packages/shared/dist/mentions.js
2252
2263
  import { parse, postprocess, preprocess } from "micromark";
@@ -3577,7 +3588,7 @@ var BoardPaths = class {
3577
3588
 
3578
3589
  // packages/board-core/dist/skill-folder.js
3579
3590
  import { constants } from "node:fs";
3580
- import { lstat, open as open2, readdir as readdir3, realpath } from "node:fs/promises";
3591
+ import { lstat, open as open2, readdir as readdir3, readlink, realpath } from "node:fs/promises";
3581
3592
  import path6 from "node:path";
3582
3593
  var SKILL_TEXT_LIMIT_CHARS = 1e5;
3583
3594
  var SKILL_TEXT_LIMIT_BYTES = SKILL_TEXT_LIMIT_CHARS * 4;
@@ -3592,6 +3603,13 @@ function inside(root, file) {
3592
3603
  const relative = path6.relative(root, file);
3593
3604
  return relative !== ".." && !relative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(relative);
3594
3605
  }
3606
+ async function handleInside(root, handle) {
3607
+ if (process.platform !== "linux") {
3608
+ return true;
3609
+ }
3610
+ const file = await readlink(`/proc/self/fd/${handle.fd}`).catch(() => null);
3611
+ return file !== null && inside(root, file);
3612
+ }
3595
3613
  async function walk(root, prefix, found) {
3596
3614
  const entries = await readdir3(path6.join(root, ...prefix === "" ? [] : prefix.split("/")), {
3597
3615
  withFileTypes: true
@@ -3626,6 +3644,9 @@ async function readEntry(root, relative) {
3626
3644
  return null;
3627
3645
  }
3628
3646
  try {
3647
+ if (!await handleInside(root, handle)) {
3648
+ return null;
3649
+ }
3629
3650
  const stats = await handle.stat();
3630
3651
  if (!stats.isFile()) {
3631
3652
  return null;
@@ -3696,17 +3717,16 @@ var SignInsSchema = z16.object({
3696
3717
  signIns: z16.array(z16.object({
3697
3718
  tokenHash: z16.string().min(1),
3698
3719
  login: z16.string().min(1),
3699
- /** The account's picture at the identity provider; sign-ins from before it have none. */
3720
+ /** The account's picture, when GitHub gave one on its own avatar host. */
3700
3721
  avatarUrl: z16.url().optional(),
3701
3722
  createdAt: IsoDateTimeSchema,
3702
3723
  expiresAt: IsoDateTimeSchema
3703
3724
  })).default([])
3704
3725
  });
3705
- var TurnStartPayloadSchema = z16.object({ project: NameSchema, turnId: UlidSchema.optional() });
3726
+ var TurnStartPayloadSchema = z16.object({ turnId: UlidSchema });
3706
3727
  var CursorsSchema = z16.object({
3707
3728
  digest: z16.string().nullable(),
3708
3729
  scopes: z16.record(z16.string(), z16.string()).default({}),
3709
- threadsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3710
3730
  channelsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3711
3731
  placesFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3712
3732
  /** How far a work role has read news, which goes to whichever of its general conversations reads first. */
@@ -3715,7 +3735,6 @@ var CursorsSchema = z16.object({
3715
3735
  var NO_CURSORS = {
3716
3736
  digest: null,
3717
3737
  scopes: {},
3718
- threadsFrom: {},
3719
3738
  channelsFrom: {},
3720
3739
  placesFrom: {}
3721
3740
  };
@@ -3739,10 +3758,6 @@ function placeStart(cursors, scope) {
3739
3758
  const from = cursors.placesFrom[scope];
3740
3759
  return from === void 0 ? cursors.digest : from;
3741
3760
  }
3742
- function threadStart(cursors, scope) {
3743
- const from = cursors.threadsFrom[scope];
3744
- return from === void 0 ? cursors.digest : from;
3745
- }
3746
3761
  function newsStart(cursors) {
3747
3762
  if (cursors.news !== void 0) {
3748
3763
  return cursors.news;
@@ -3754,7 +3769,7 @@ function cursorOf(cursors, scope, part = null) {
3754
3769
  if (part === null) {
3755
3770
  return cursors.scopes[scope] ?? cursors.digest;
3756
3771
  }
3757
- return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : part.startsWith("~") ? placeStart(cursors, scope) : threadStart(cursors, scope));
3772
+ return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : part.startsWith("~") ? placeStart(cursors, scope) : cursors.digest);
3758
3773
  }
3759
3774
  function byId(a, b) {
3760
3775
  return a.message.id < b.message.id ? -1 : a.message.id > b.message.id ? 1 : 0;
@@ -3775,7 +3790,7 @@ function skillFolderOf(proposal) {
3775
3790
  return null;
3776
3791
  }
3777
3792
  const parsed = SkillProposalSchema.safeParse(proposal.charter);
3778
- return parsed.success && parsed.data.body === void 0 ? parsed.data.name : null;
3793
+ return parsed.success ? parsed.data.name : null;
3779
3794
  }
3780
3795
  function proposedFrom(origin, where) {
3781
3796
  return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel && origin.place === where.place;
@@ -3790,28 +3805,6 @@ var FRONT_DESK_TRIGGER = "user_post";
3790
3805
  var OPS_CHANNELS = ["governance"];
3791
3806
  var FRONT_DESK_CHANNELS = [ASK_CHANNEL];
3792
3807
  var AlignmentsSchema = z16.object({ applied: z16.array(z16.string()).default([]) });
3793
- var FOLLOW_DECISIONS = "ops-readers-follow-decisions";
3794
- var THREAD_RECORDS = "threads-as-records";
3795
- var DIGEST_CURSORS = "digest-replaces-inbox";
3796
- var TASK_RETURNS = "task-returns-recorded";
3797
- var OPS_CHANNEL_RETIRED = "ops-channel-retired";
3798
- var DECISIONS_CHANNEL_RETIRED = "decisions-channel-retired";
3799
- var THREAD_CONVERSATIONS = "thread-conversations";
3800
- var ASKS_CHANNEL_OPENED = "asks-channel-opened";
3801
- var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
3802
- var CHANNEL_CONVERSATIONS = "channel-conversations";
3803
- var PLACE_CONVERSATIONS = "place-conversations";
3804
- var DASHBOARD_UPDATES = "dashboard-updates-granted";
3805
- var CRON_VERBS = "cron-verbs-granted";
3806
- var KNOWLEDGE_NOTICES = "knowledge-notices";
3807
- var IMAGE_UPLOADS = "image-uploads-granted";
3808
- var KnowledgeEventSchema = z16.object({
3809
- topic: NameSchema,
3810
- project: NameSchema.nullable(),
3811
- replaced: z16.boolean().optional()
3812
- });
3813
- var CRON_VERB_NAMES = ["create_cron", "update_cron", "remove_cron"];
3814
- var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
3815
3808
  var DASHBOARD_LINE = "The project's members keep this page current with update_dashboard.";
3816
3809
  var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
3817
3810
  var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
@@ -3831,9 +3824,9 @@ function sentence(summary) {
3831
3824
  return summary.trim().replace(/\.+$/, "");
3832
3825
  }
3833
3826
  function skillSummary(data, body) {
3834
- const fromFrontmatter = data["description"] ?? data["summary"];
3835
- if (typeof fromFrontmatter === "string" && fromFrontmatter.trim().length > 0) {
3836
- return fromFrontmatter.trim();
3827
+ const description = data["description"];
3828
+ if (typeof description === "string" && description.trim().length > 0) {
3829
+ return description.trim();
3837
3830
  }
3838
3831
  return body.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#")) ?? "";
3839
3832
  }
@@ -3986,390 +3979,22 @@ var Board = class _Board {
3986
3979
  return board2;
3987
3980
  }
3988
3981
  /**
3989
- * Applies each one-time alignment an existing society has not had yet. Once applied it is not
3990
- * repeated, so a member that later chooses otherwise keeps its choice.
3982
+ * Applies each one-time alignment an existing society has not had yet, and remembers it, so a
3983
+ * member that later chooses otherwise keeps its choice. A new alignment is a name and a method in
3984
+ * this list; once every board has applied it, both are deleted. None is pending.
3991
3985
  */
3992
3986
  async applyAlignments() {
3987
+ const alignments = [];
3993
3988
  const file = path7.join(this.paths.state(), "alignments.json");
3994
3989
  const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
3995
- const pending = [
3996
- FOLLOW_DECISIONS,
3997
- THREAD_RECORDS,
3998
- DIGEST_CURSORS,
3999
- TASK_RETURNS,
4000
- OPS_CHANNEL_RETIRED,
4001
- DECISIONS_CHANNEL_RETIRED,
4002
- THREAD_CONVERSATIONS,
4003
- ASKS_CHANNEL_OPENED,
4004
- KNOWLEDGE_REMOVAL,
4005
- CHANNEL_CONVERSATIONS,
4006
- DASHBOARD_UPDATES,
4007
- CRON_VERBS,
4008
- KNOWLEDGE_NOTICES,
4009
- IMAGE_UPLOADS,
4010
- PLACE_CONVERSATIONS
4011
- ].filter((name) => !applied.includes(name));
3990
+ const pending = alignments.filter(({ name }) => !applied.includes(name));
4012
3991
  if (pending.length === 0) {
4013
3992
  return;
4014
3993
  }
4015
- if (pending.includes(FOLLOW_DECISIONS)) {
4016
- await this.followDecisions();
4017
- }
4018
- if (pending.includes(THREAD_RECORDS)) {
4019
- await this.recordTaskThreads();
4020
- }
4021
- if (pending.includes(DIGEST_CURSORS)) {
4022
- await this.renameInboxCursors();
4023
- }
4024
- if (pending.includes(TASK_RETURNS)) {
4025
- await this.recordTaskReturns();
4026
- }
4027
- if (pending.includes(OPS_CHANNEL_RETIRED)) {
4028
- await this.retireSocietyChannel("ops");
4029
- }
4030
- if (pending.includes(DECISIONS_CHANNEL_RETIRED)) {
4031
- await this.retireSocietyChannel("decisions");
4032
- }
4033
- if (pending.includes(THREAD_CONVERSATIONS)) {
4034
- await this.startThreadCursors();
4035
- }
4036
- if (pending.includes(ASKS_CHANNEL_OPENED)) {
4037
- await this.openAsksChannel();
4038
- }
4039
- if (pending.includes(KNOWLEDGE_REMOVAL)) {
4040
- await this.grantBeside("write_knowledge", "remove_knowledge");
4041
- }
4042
- if (pending.includes(CHANNEL_CONVERSATIONS)) {
4043
- await this.startChannelCursors();
4044
- }
4045
- if (pending.includes(DASHBOARD_UPDATES)) {
4046
- await this.grantBeside("write_knowledge", "update_dashboard");
4047
- await this.rewordDashboards();
4048
- }
4049
- if (pending.includes(CRON_VERBS)) {
4050
- await this.grantToEveryRole(CRON_VERB_NAMES);
4051
- }
4052
- if (pending.includes(KNOWLEDGE_NOTICES)) {
4053
- await this.noticeKnowledgeNotes();
4054
- }
4055
- if (pending.includes(IMAGE_UPLOADS)) {
4056
- await this.grantBeside("post_message", "upload_image");
4057
- }
4058
- if (pending.includes(PLACE_CONVERSATIONS)) {
4059
- await this.startPlaceCursors();
4060
- }
4061
- await writeJson(file, { applied: [...applied, ...pending] });
4062
- }
4063
- /**
4064
- * Gives each knowledge note from before notes carried their notice the notice it would carry
4065
- * now. The event log, not the note's words, says which post is one: a knowledge verb appends
4066
- * its event and then its note under the mutex, so a note is the post right after such an event,
4067
- * by the same actor, at the top of the general channel of the topic's place. A post written to
4068
- * look like a note follows no such event and is left as it is.
4069
- */
4070
- async noticeKnowledgeNotes() {
4071
- let before;
4072
- for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
4073
- const verb = before;
4074
- before = event;
4075
- if (event.type !== "message.posted" || verb === void 0 || verb.actor !== event.actor || verb.type !== "knowledge.written" && verb.type !== "knowledge.removed") {
4076
- continue;
4077
- }
4078
- const knowledge = KnowledgeEventSchema.safeParse(verb.payload);
4079
- const id = UlidSchema.safeParse(event.payload["id"]);
4080
- if (!knowledge.success || !id.success || (event.payload["thread"] ?? null) !== null || event.payload["channel"] !== channelRef(knowledge.data.project, "general")) {
4081
- continue;
4082
- }
4083
- const { topic, project, replaced } = knowledge.data;
4084
- const file = this.paths.messageFile(this.paths.channelDir(channelRef(project, "general")), id.data, event.actor);
4085
- if (!await exists(file)) {
4086
- continue;
4087
- }
4088
- const { data, body, error } = await readLooseMarkdown(file);
4089
- if (error !== null || data["notice"] !== void 0) {
4090
- continue;
4091
- }
4092
- const notice = {
4093
- kind: "knowledge",
4094
- action: verb.type === "knowledge.removed" ? "removed" : replaced === true ? "updated" : "written",
4095
- topic,
4096
- project
4097
- };
4098
- await writeMarkdown(file, { ...data, notice }, body);
4099
- }
4100
- }
4101
- /**
4102
- * Grants `verb` once to every charter granting `beside`, the work roles' charters in practice;
4103
- * seed roles have it from their seed.
4104
- */
4105
- async grantBeside(beside, verb) {
4106
- for (const charter of await this.listRoles()) {
4107
- if (charter.verbs.includes(beside) && !charter.verbs.includes(verb)) {
4108
- await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
4109
- }
4110
- }
4111
- }
4112
- /** Grants verbs once to every charter but the user's, which holds every verb already. */
4113
- async grantToEveryRole(verbs) {
4114
- for (const charter of await this.listRoles()) {
4115
- const missing = verbs.filter((verb) => !charter.verbs.includes(verb));
4116
- if (charter.name !== USER_ROLE && missing.length > 0) {
4117
- await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, ...missing] }, { verbsAdded: missing });
4118
- }
4119
- }
4120
- }
4121
- /** A dashboard still saying agents may edit its file says how it changes now. */
4122
- async rewordDashboards() {
4123
- for (const slug of await listDirs(this.paths.projects())) {
4124
- const file = this.paths.dashboard(slug);
4125
- if (!await exists(file)) {
4126
- continue;
4127
- }
4128
- const { data, body } = await readLooseMarkdown(file);
4129
- if (body.includes(OLD_DASHBOARD_LINE)) {
4130
- await writeMarkdown(file, data, body.replace(OLD_DASHBOARD_LINE, DASHBOARD_LINE));
4131
- }
4132
- }
4133
- }
4134
- /**
4135
- * A task sent back before tasks recorded it kept no trace outside the event log. A task still
4136
- * short of the stage its latest `task.moved` came from is in that rework, and records it.
4137
- */
4138
- async recordTaskReturns() {
4139
- const moves = /* @__PURE__ */ new Map();
4140
- for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
4141
- const taskId = event.payload["taskId"];
4142
- if (event.type === "task.moved" && typeof taskId === "string") {
4143
- moves.set(taskId, event);
4144
- }
4145
- }
4146
- for (const slug of await listDirs(this.paths.projects())) {
4147
- for (const task of await this.listTasks(slug)) {
4148
- const move = moves.get(task.id);
4149
- const from = move?.payload["from"];
4150
- if (move === void 0 || typeof from !== "string" || task.returned !== void 0 || task.completing || task.status !== "open" && task.status !== "claimed") {
4151
- continue;
4152
- }
4153
- const back = stageIndex(task, from);
4154
- if (back > stageIndex(task, task.stage)) {
4155
- await this.writeTask(slug, {
4156
- ...task,
4157
- returned: { from, by: move.actor, at: move.ts }
4158
- });
4159
- }
4160
- }
4161
- }
4162
- }
4163
- /**
4164
- * A society channel the board no longer uses leaves the society's list and every member's
4165
- * subscriptions; its old posts stay on disk. `ops` carried operations signals, which are only
4166
- * logged now; `decisions` carried proposals' outcomes, which are posts in their threads now, and
4167
- * requests to the user, which are mentions of the user wherever the question belongs.
4168
- */
4169
- async retireSocietyChannel(name) {
4170
- const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
4171
- if (doc.data.channels.includes(name)) {
4172
- await writeMarkdown(this.paths.societyFile(), { ...doc.data, channels: doc.data.channels.filter((channel) => channel !== name) }, doc.body);
4173
- }
4174
- for (const agent of await this.listAgents()) {
4175
- if (agent.subscriptions.includes(name)) {
4176
- await this.updateAgent(agent.name, (a) => ({
4177
- ...a,
4178
- subscriptions: a.subscriptions.filter((ref) => ref !== name)
4179
- }));
4180
- await this.refreshMember(agent.name);
4181
- }
4182
- }
4183
- }
4184
- /**
4185
- * Asks once opened their threads in general, where closing one posted its summary to every
4186
- * citizen and woke the steward's heartbeat for it. The society gains the asks channel, and the
4187
- * front desk follows it.
4188
- */
4189
- async openAsksChannel() {
4190
- const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
4191
- if (!doc.data.channels.includes(ASK_CHANNEL)) {
4192
- await writeMarkdown(this.paths.societyFile(), { ...doc.data, channels: [...doc.data.channels, ASK_CHANNEL] }, doc.body);
4193
- }
4194
- await ensureDir(this.paths.societyChannel(ASK_CHANNEL));
4195
- for (const agent of await this.listAgents()) {
4196
- if (agent.status !== "active" || agent.subscriptions.includes(ASK_CHANNEL)) {
4197
- continue;
4198
- }
4199
- if (!(await this.readRole(agent.role)).wakeTriggers.includes(FRONT_DESK_TRIGGER)) {
4200
- continue;
4201
- }
4202
- await this.updateAgent(agent.name, (a) => ({
4203
- ...a,
4204
- subscriptions: [...a.subscriptions, ASK_CHANNEL]
4205
- }));
4206
- await this.refreshMember(agent.name);
4207
- await this.events.append("subscription.changed", agent.name, {
4208
- channel: ASK_CHANNEL,
4209
- subscribed: true,
4210
- aligned: true
4211
- });
4212
- }
4213
- }
4214
- /** Members that predate `decisions` among the ops channels read proposals without their outcome. */
4215
- async followDecisions() {
4216
- if (!(await this.society()).channels.includes("decisions")) {
4217
- return;
4218
- }
4219
- for (const agent of await this.listAgents()) {
4220
- if (agent.status !== "active" || agent.subscriptions.includes("decisions")) {
4221
- continue;
4222
- }
4223
- if (!(await this.readRole(agent.role)).wakeTriggers.includes(OPS_WAKE_TRIGGER)) {
4224
- continue;
4225
- }
4226
- await this.updateAgent(agent.name, (a) => ({
4227
- ...a,
4228
- subscriptions: [...a.subscriptions, "decisions"]
4229
- }));
4230
- await this.refreshMember(agent.name);
4231
- await this.events.append("subscription.changed", agent.name, {
4232
- channel: "decisions",
4233
- subscribed: true,
4234
- aligned: true
4235
- });
4236
- }
4237
- }
4238
- /**
4239
- * Threads were once a state field on their task. Each becomes a record under the task's project,
4240
- * on its general channel, closed if its task has ended, and the field leaves the task file.
4241
- */
4242
- async recordTaskThreads() {
4243
- const opened = /* @__PURE__ */ new Map();
4244
- const closed = /* @__PURE__ */ new Map();
4245
- for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
4246
- const taskId = event.payload["taskId"];
4247
- if (typeof taskId !== "string") {
4248
- continue;
4249
- }
4250
- if (event.type === "thread.opened") {
4251
- opened.set(taskId, event);
4252
- } else if (event.type === "thread.closed") {
4253
- closed.set(taskId, event);
4254
- }
4255
- }
4256
- for (const slug of await listDirs(this.paths.projects())) {
4257
- const threads = this.paths.threads(slug);
4258
- const summaries = /* @__PURE__ */ new Map();
4259
- for (const message of await this.readMessagesIn(this.paths.projectChannel(slug, "general"), null)) {
4260
- if (message.task !== void 0) {
4261
- summaries.set(message.task, message.body.replace(/^Thread closed[^\n]*\n\n/, ""));
4262
- }
4263
- }
4264
- for (const file of await listFiles(this.paths.tasks(slug))) {
4265
- const raw = await readMarkdown(path7.join(this.paths.tasks(slug), file), z16.record(z16.string(), z16.unknown()));
4266
- const legacy = raw.data["thread"];
4267
- if (legacy === void 0) {
4268
- continue;
4269
- }
4270
- const task = { ...TaskFrontmatterSchema.parse(raw.data), body: raw.body };
4271
- if ((legacy === "open" || legacy === "closed") && !await exists(this.paths.threadFile(threads, task.id))) {
4272
- const ended = task.status === "done" || task.status === "abandoned";
4273
- const open3 = legacy === "open" && !ended;
4274
- const openedBy = opened.get(task.id);
4275
- const closedBy = closed.get(task.id);
4276
- await this.writeThread(threads, {
4277
- id: task.id,
4278
- channel: channelRef(slug, "general"),
4279
- title: task.title,
4280
- subject: { kind: "task", id: task.id },
4281
- state: open3 ? "open" : "closed",
4282
- openedBy: openedBy?.actor ?? task.createdBy,
4283
- openedAt: openedBy?.ts ?? task.createdAt,
4284
- ...open3 ? {} : {
4285
- closedBy: closedBy?.actor ?? SYSTEM_ACTOR.name,
4286
- closedAt: closedBy?.ts ?? this.now().toISOString()
4287
- },
4288
- body: summaries.get(task.id) ?? ""
4289
- });
4290
- }
4291
- await this.writeTask(slug, task);
4292
- }
4293
- }
4294
- }
4295
- /**
4296
- * Every channel of a scope was once read in its home conversation, so a member had read each up
4297
- * to its scope's cursor. Each channel beside general starts from that cursor now, as each takes a
4298
- * conversation of its own.
4299
- */
4300
- async startChannelCursors() {
4301
- for (const agent of await this.listAgents()) {
4302
- const file = this.paths.agentCursors(agent.name);
4303
- if (!await exists(file)) {
4304
- continue;
4305
- }
4306
- const cursors = await readJson(file, CursorsSchema);
4307
- const scopes = /* @__PURE__ */ new Set([
4308
- ...Object.keys(cursors.scopes).filter((key) => !key.includes("/")),
4309
- ...agent.memberships,
4310
- SOCIETY_SCOPE
4311
- ]);
4312
- const channelsFrom = Object.fromEntries([...scopes].map((scope) => [scope, cursorOf(cursors, scope)]));
4313
- await writeJson(file, { ...cursors, channelsFrom });
4314
- }
4315
- }
4316
- /**
4317
- * A project's channels were once read, from the society scope, in the society's home
4318
- * conversation, so a member had read each up to that cursor. Each project's conversation in the
4319
- * society scope starts from it now, as each takes a conversation of its own.
4320
- */
4321
- async startPlaceCursors() {
4322
- for (const agent of await this.listAgents()) {
4323
- const file = this.paths.agentCursors(agent.name);
4324
- if (!await exists(file)) {
4325
- continue;
4326
- }
4327
- const cursors = await readJson(file, CursorsSchema);
4328
- await writeJson(file, {
4329
- ...cursors,
4330
- placesFrom: { [SOCIETY_SCOPE]: cursorOf(cursors, SOCIETY_SCOPE) }
4331
- });
3994
+ for (const { apply } of pending) {
3995
+ await apply();
4332
3996
  }
4333
- }
4334
- /**
4335
- * Threads were once read in the scope's single conversation, so a member had read every thread
4336
- * up to its scope's cursor. Each scope's threads start from that cursor now, as each thread's
4337
- * conversation takes a cursor of its own.
4338
- */
4339
- async startThreadCursors() {
4340
- for (const agent of await this.listAgents()) {
4341
- const file = this.paths.agentCursors(agent.name);
4342
- if (!await exists(file)) {
4343
- continue;
4344
- }
4345
- const cursors = await readJson(file, CursorsSchema);
4346
- const scopes = /* @__PURE__ */ new Set([
4347
- ...Object.keys(cursors.scopes).filter((key) => !key.includes("/")),
4348
- ...agent.memberships,
4349
- SOCIETY_SCOPE
4350
- ]);
4351
- const threadsFrom = Object.fromEntries([...scopes].map((scope) => [scope, cursorOf(cursors, scope)]));
4352
- await writeJson(file, { ...cursors, threadsFrom });
4353
- }
4354
- }
4355
- /**
4356
- * The digest was once called the inbox: each cursor file keyed its cursor `inbox`, and the user
4357
- * followed every society channel and project general channel to read one. The user takes no
4358
- * turns, so it follows nothing now.
4359
- */
4360
- async renameInboxCursors() {
4361
- const Legacy = z16.object({ inbox: z16.string().nullable() });
4362
- for (const agent of await this.listAgents()) {
4363
- const file = this.paths.agentCursors(agent.name);
4364
- if (await exists(file)) {
4365
- const legacy = Legacy.safeParse(JSON.parse(await readFile4(file, "utf8")));
4366
- if (legacy.success) {
4367
- await writeJson(file, { digest: legacy.data.inbox });
4368
- }
4369
- }
4370
- }
4371
- await this.updateAgent(USER_NAME, (user) => ({ ...user, subscriptions: [] }));
4372
- await this.refreshMember(USER_NAME);
3997
+ await writeJson(file, { applied: [...applied, ...pending.map(({ name }) => name)] });
4373
3998
  }
4374
3999
  /** Writes every missing seed charter and aligns the existing ones with their seed. */
4375
4000
  async ensureSeedRoles() {
@@ -5173,7 +4798,7 @@ ${DASHBOARD_LINE}
5173
4798
  if (event.type === "turn.started") {
5174
4799
  const start = TurnStartPayloadSchema.safeParse(event.payload);
5175
4800
  if (start.success)
5176
- starts.set(start.data.turnId ?? start.data.project, event.ts);
4801
+ starts.set(start.data.turnId, event.ts);
5177
4802
  continue;
5178
4803
  }
5179
4804
  if (event.type !== "turn.completed" && event.type !== "turn.failed") {
@@ -5181,9 +4806,11 @@ ${DASHBOARD_LINE}
5181
4806
  }
5182
4807
  const parsed = TurnHistoryPayloadSchema.safeParse(event.payload);
5183
4808
  if (parsed.success) {
5184
- const key = parsed.data.turnId ?? parsed.data.project;
5185
- const startedAt = starts.get(key);
5186
- starts.delete(key);
4809
+ const { turnId } = parsed.data;
4810
+ const startedAt = turnId === void 0 ? void 0 : starts.get(turnId);
4811
+ if (turnId !== void 0) {
4812
+ starts.delete(turnId);
4813
+ }
5187
4814
  entries.push({
5188
4815
  id: event.id,
5189
4816
  ts: event.ts,
@@ -5418,7 +5045,7 @@ ${DASHBOARD_LINE}
5418
5045
  const starts = [
5419
5046
  ...scopes.flatMap((each) => conversation === void 0 ? [
5420
5047
  cursorOf(cursors, each),
5421
- threadStart(cursors, each),
5048
+ cursors.digest,
5422
5049
  channelStart(cursors, each),
5423
5050
  placeStart(cursors, each)
5424
5051
  ] : [cursorOf(cursors, each, conversation.part)]),
@@ -6034,11 +5661,12 @@ ${current.body}`);
6034
5661
  const args = VerbInputs.open_thread.parse(input);
6035
5662
  await this.authorize(actor, "open_thread");
6036
5663
  return this.mutex.run(async () => {
6037
- const opening = await this.threadToOpen(args);
6038
- if (await this.tryFindThread(opening.id) !== null) {
6039
- throw new BoardError("INVALID_STATE", `${opening.subject?.kind ?? "thread"} ${opening.id} already has a thread`);
6040
- }
6041
- return this.openThreadUnlocked(actor.name, opening);
5664
+ await this.assertChannelOpen(args.channel);
5665
+ return this.openThreadUnlocked(actor.name, {
5666
+ id: this.newId(),
5667
+ channel: args.channel,
5668
+ title: args.title
5669
+ });
6042
5670
  });
6043
5671
  }
6044
5672
  /** Writes a new open thread and its event. Callers hold the mutex and chose where it hangs. */
@@ -6073,28 +5701,13 @@ ${current.body}`);
6073
5701
  subject: { kind: "proposal", id: proposal.id }
6074
5702
  };
6075
5703
  }
6076
- /**
6077
- * The open thread of a proposal being decided. A proposal from before proposals opened their
6078
- * threads gets one now, opened as its proposer, and a thread closed before the decision opens
6079
- * again.
6080
- */
6081
- async proposalThreadUnlocked(by, proposal) {
5704
+ /** The open thread of a proposal being decided, which `propose` opened with it. */
5705
+ async proposalThreadUnlocked(proposal) {
6082
5706
  const found = await this.tryFindThread(proposal.id);
6083
- if (found?.thread.state === "open") {
6084
- return found.thread;
6085
- }
6086
- if (found === null) {
6087
- return this.openThreadUnlocked(proposal.proposedBy, this.proposalThreadOpening(proposal));
5707
+ if (found?.thread.state !== "open") {
5708
+ throw new BoardError("INVALID_STATE", `proposal ${proposal.id} has no open thread`);
6088
5709
  }
6089
- const { closedBy: _by, closedAt: _at, ...kept } = found.thread;
6090
- const reopened = await this.writeThread(found.threads, { ...kept, state: "open" });
6091
- await this.events.append("thread.opened", by, {
6092
- threadId: proposal.id,
6093
- channel: reopened.channel,
6094
- subject: reopened.subject ?? null,
6095
- reopened: true
6096
- });
6097
- return reopened;
5710
+ return found.thread;
6098
5711
  }
6099
5712
  /** Where a task's thread hangs, what it is called, and what it is about. */
6100
5713
  taskThreadOpening(task) {
@@ -6106,79 +5719,21 @@ ${current.body}`);
6106
5719
  };
6107
5720
  }
6108
5721
  /**
6109
- * The open thread of a task in play, for a verb about to post into it. A task from before tasks
6110
- * opened their threads gets one now, and a thread closed early while its task is in play opens
6111
- * again; an ended task's thread stays closed.
5722
+ * The open thread of a task in play, for a verb about to post into it, which `createTask` opened
5723
+ * with the task; an ended task's thread closed with it.
6112
5724
  */
6113
- async taskThreadUnlocked(by, task) {
5725
+ async taskThreadUnlocked(task) {
6114
5726
  const found = await this.tryFindThread(task.id);
6115
5727
  if (found?.thread.state === "open") {
6116
5728
  return found.thread;
6117
5729
  }
6118
- if (task.status === "done" || task.status === "abandoned") {
6119
- throw new BoardError("INVALID_STATE", `task ${task.id} is ${task.status} and its thread closed with it`);
6120
- }
6121
- if (found === null) {
6122
- return this.openThreadUnlocked(by, this.taskThreadOpening(task));
6123
- }
6124
- const { closedBy: _by, closedAt: _at, ...kept } = found.thread;
6125
- const reopened = await this.writeThread(found.threads, { ...kept, state: "open" });
6126
- await this.events.append("thread.opened", by, {
6127
- threadId: task.id,
6128
- channel: reopened.channel,
6129
- subject: reopened.subject ?? null,
6130
- reopened: true
6131
- });
6132
- return reopened;
5730
+ throw new BoardError("INVALID_STATE", task.status === "done" || task.status === "abandoned" ? `task ${task.id} is ${task.status} and its thread closed with it` : `task ${task.id} has no open thread`);
6133
5731
  }
6134
5732
  /** Posts what a task verb was told to say into the task's thread, as its author, with the step it recorded. */
6135
5733
  async postTaskNote(author, task, body, step) {
6136
- const thread = await this.taskThreadUnlocked(author, task);
5734
+ const thread = await this.taskThreadUnlocked(task);
6137
5735
  return this.appendThreadMessage(author, thread.id, body, step === void 0 ? {} : { step });
6138
5736
  }
6139
- /** Where a new thread hangs, what it is called, and what it is about. */
6140
- async threadToOpen(args) {
6141
- if (args.task_id !== void 0 && args.proposal_id !== void 0) {
6142
- throw new BoardError("VALIDATION", "a thread is about one task or one proposal, not both");
6143
- }
6144
- if (args.task_id !== void 0) {
6145
- const { task, project } = await this.findTask(args.task_id);
6146
- if (task.status === "done" || task.status === "abandoned") {
6147
- throw new BoardError("INVALID_STATE", `task ${task.id} is ${task.status}`);
6148
- }
6149
- const channel = args.channel ?? channelRef(project, task.channel);
6150
- if (parseChannelRef(channel).project !== project) {
6151
- throw new BoardError("VALIDATION", `a task's thread hangs off a channel of its project, ${project}`);
6152
- }
6153
- await this.assertChannelOpen(channel);
6154
- return {
6155
- id: task.id,
6156
- channel,
6157
- title: args.title ?? task.title,
6158
- subject: { kind: "task", id: task.id }
6159
- };
6160
- }
6161
- if (args.proposal_id !== void 0) {
6162
- const proposal = await this.readProposal(args.proposal_id);
6163
- if (proposal.status !== "proposed") {
6164
- throw new BoardError("INVALID_STATE", `proposal ${proposal.id} is already ${proposal.status}`);
6165
- }
6166
- const channel = args.channel ?? "governance";
6167
- await this.assertChannelOpen(channel);
6168
- const described = `${proposal.kind} proposal: ${describeCharter(proposal.kind, proposal.charter)}`;
6169
- return {
6170
- id: proposal.id,
6171
- channel,
6172
- title: args.title ?? described.slice(0, 200),
6173
- subject: { kind: "proposal", id: proposal.id }
6174
- };
6175
- }
6176
- if (args.channel === void 0 || args.title === void 0) {
6177
- throw new BoardError("VALIDATION", "a thread needs a task_id, a proposal_id, or a channel and a title");
6178
- }
6179
- await this.assertChannelOpen(args.channel);
6180
- return { id: this.newId(), channel: args.channel, title: args.title };
6181
- }
6182
5737
  /**
6183
5738
  * Where the board learns which turns are in flight, from the server that hosts them, so it
6184
5739
  * refuses to end a conversation under someone else's turn.
@@ -7312,6 +6867,7 @@ ${rationale}`}`);
7312
6867
  needsUserDecision: parsed.status?.needsUserDecision ?? false,
7313
6868
  error: parsed.error,
7314
6869
  ...parsed.resumeFailed === void 0 ? {} : { resumeFailed: parsed.resumeFailed },
6870
+ ...parsed.transient === true ? { transient: true } : {},
7315
6871
  ...parsed.sessionDropped === true ? { sessionDropped: true } : {}
7316
6872
  });
7317
6873
  });
@@ -8019,15 +7575,9 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
8019
7575
  case "archive":
8020
7576
  await this.validateArchive(ArchiveProposalSchema.parse(charter).project);
8021
7577
  return;
8022
- case "reallocation": {
8023
- const parsed = ReallocationProposalSchema.safeParse(charter);
8024
- if (parsed.success) {
8025
- await this.reallocatedRole(parsed.data);
8026
- } else {
8027
- LegacyReallocationSchema.parse(charter);
8028
- }
7578
+ case "reallocation":
7579
+ await this.reallocatedRole(ReallocationProposalSchema.parse(charter));
8029
7580
  return;
8030
- }
8031
7581
  default:
8032
7582
  return;
8033
7583
  }
@@ -8087,11 +7637,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
8087
7637
  return { project: input.project, members };
8088
7638
  }
8089
7639
  case "reallocation": {
8090
- const parsed = ReallocationProposalSchema.safeParse(proposal.charter);
8091
- if (!parsed.success) {
8092
- return void 0;
8093
- }
8094
- const { next, changed } = await this.reallocatedRole(parsed.data);
7640
+ const { next, changed } = await this.reallocatedRole(ReallocationProposalSchema.parse(proposal.charter));
8095
7641
  await this.writeRoleUnlocked(by, next, meta);
8096
7642
  return { role: next.name, changed };
8097
7643
  }
@@ -8101,8 +7647,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
8101
7647
  }
8102
7648
  /**
8103
7649
  * Publishes a promoted skill under the society's skills, where every citizen's skills index lists
8104
- * it: the folder the board took from its proposer's home, or the text proposed alone. A
8105
- * replacement is the whole skill, so files the new version dropped go with the old one.
7650
+ * it: the folder the board took from its proposer's home. A replacement is the whole skill, so
7651
+ * files the new version dropped go with the old one.
8106
7652
  */
8107
7653
  async promoteSkillUnlocked(by, proposal, skill, meta) {
8108
7654
  const dir = this.paths.societySkill(skill.name);
@@ -8110,16 +7656,12 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
8110
7656
  const replaced = await exists(file);
8111
7657
  const taken = proposal.skillFiles;
8112
7658
  const staged = path7.join(this.paths.proposalFiles(proposal.id), "SKILL.md");
8113
- if (skill.body === void 0 && (taken === void 0 || !await exists(staged))) {
7659
+ if (taken === void 0 || !await exists(staged)) {
8114
7660
  throw new BoardError("INVALID_STATE", `proposal ${proposal.id} holds no skill files`);
8115
7661
  }
8116
7662
  await rm2(dir, { recursive: true, force: true });
8117
- if (skill.body === void 0) {
8118
- await cp(this.paths.proposalFiles(proposal.id), dir, { recursive: true });
8119
- } else {
8120
- await ensureDir(dir);
8121
- }
8122
- const body = skill.body ?? (await readLooseMarkdown(file)).body;
7663
+ await cp(this.paths.proposalFiles(proposal.id), dir, { recursive: true });
7664
+ const { body } = await readLooseMarkdown(file);
8123
7665
  await writeMarkdown(file, {
8124
7666
  name: skill.name,
8125
7667
  description: skill.summary,
@@ -8127,7 +7669,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
8127
7669
  proposedBy: proposal.proposedBy,
8128
7670
  promotedBy: by,
8129
7671
  promotedAt: this.now().toISOString(),
8130
- ...taken === void 0 ? {} : { commit: taken.commit }
7672
+ commit: taken.commit
8131
7673
  }
8132
7674
  }, body);
8133
7675
  await this.events.append("skill.promoted", by, {
@@ -8135,7 +7677,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
8135
7677
  summary: skill.summary,
8136
7678
  proposedBy: proposal.proposedBy,
8137
7679
  replaced,
8138
- ...taken === void 0 ? {} : { files: taken.files.length },
7680
+ files: taken.files.length,
8139
7681
  ...meta
8140
7682
  });
8141
7683
  await this.appendMessage(SYSTEM_ACTOR.name, "general", `Skill ${skill.name} ${replaced ? "updated in" : "promoted to"} the society: ${sentence(skill.summary)}. Every citizen's skills index now lists it.`);
@@ -8824,6 +8366,7 @@ ${charter.purpose}
8824
8366
  await this.validateProvision(filed.kind, filed.charter);
8825
8367
  }
8826
8368
  this.assertNoTurnCut(actor, `proposal ${proposalId} cannot be decided`, (turn) => turn.thread === proposalId);
8369
+ const thread = await this.proposalThreadUnlocked(filed);
8827
8370
  if (outcome === "approved" && filed.kind === "archive") {
8828
8371
  await this.assertProjectQuiet(actor, ArchiveProposalSchema.parse(filed.charter).project);
8829
8372
  }
@@ -8865,7 +8408,6 @@ ${charter.purpose}
8865
8408
  const verdict = outcome === "approved" ? "Approved" : "Rejected";
8866
8409
  const why = reason === void 0 ? "" : ` Reason: ${reason}.`;
8867
8410
  const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
8868
- const thread = await this.proposalThreadUnlocked(actor.name, proposal);
8869
8411
  const post = `${verdict}: ${describeCharter(proposal.kind, proposal.charter)}.${why}${result}`;
8870
8412
  await this.appendThreadMessage(actor.name, thread.id, actor.name === SYSTEM_ACTOR.name ? withoutMentions(post) : post, { step: { action: outcome } });
8871
8413
  await this.endThreadOf(proposalId, actor.name, outcome);
@@ -9085,12 +8627,14 @@ var DEFAULT_TIMINGS = Object.freeze({
9085
8627
  leaseSweepMs: 6e4,
9086
8628
  opsIntervalMs: 5 * 6e4,
9087
8629
  signalRepeatMs: 6 * 36e5,
8630
+ signalWakeMs: 24 * 36e5,
9088
8631
  costReportMs: 36e5,
9089
8632
  staleThreadMs: 24 * 36e5,
9090
8633
  idleMemberMs: 3 * 24 * 36e5,
9091
8634
  homeConflictMs: 24 * 36e5,
9092
8635
  scaleCooldownMs: 36e5,
9093
- reflectionMs: 24 * 36e5
8636
+ reflectionMs: 24 * 36e5,
8637
+ transientRetryMs: [6e4, 3 * 6e4, 10 * 6e4]
9094
8638
  });
9095
8639
  var SchedulerTimingsSchema = z17.object({
9096
8640
  pollMs: z17.number().positive(),
@@ -9101,18 +8645,20 @@ var SchedulerTimingsSchema = z17.object({
9101
8645
  leaseSweepMs: z17.number().positive(),
9102
8646
  opsIntervalMs: z17.number().positive(),
9103
8647
  signalRepeatMs: z17.number().positive(),
8648
+ signalWakeMs: z17.number().positive(),
9104
8649
  costReportMs: z17.number().positive(),
9105
8650
  staleThreadMs: z17.number().positive(),
9106
8651
  idleMemberMs: z17.number().positive(),
9107
8652
  homeConflictMs: z17.number().positive(),
9108
8653
  scaleCooldownMs: z17.number().positive(),
9109
- reflectionMs: z17.number().positive()
8654
+ reflectionMs: z17.number().positive(),
8655
+ transientRetryMs: z17.array(z17.number().positive())
9110
8656
  }).partial();
9111
8657
  function parseTimings(input) {
9112
8658
  const parsed = SchedulerTimingsSchema.parse(input);
9113
8659
  const overrides = {};
9114
8660
  for (const [key, value] of Object.entries(parsed)) {
9115
- if (typeof value === "number") {
8661
+ if (value !== void 0) {
9116
8662
  Object.assign(overrides, { [key]: value });
9117
8663
  }
9118
8664
  }
@@ -9136,6 +8682,11 @@ var StateSchema = z17.object({
9136
8682
  /** Queued dispatches survive a restart; the cursor has already moved past the events that made them. */
9137
8683
  pending: z17.record(z17.string(), PendingSchema).default({}),
9138
8684
  opsReported: z17.record(z17.string(), z17.string()).default({}),
8685
+ /**
8686
+ * When each condition of a waking kind that still holds last woke the readers of signals, and
8687
+ * the level it woke at for the kinds that have one (`wakeLevel`); forgotten with the condition.
8688
+ */
8689
+ opsWoken: z17.record(z17.string(), z17.object({ at: z17.string(), level: z17.number().optional() })).default({}),
9139
8690
  releaseCounts: z17.record(z17.string(), z17.number().int().nonnegative()).default({}),
9140
8691
  lastOps: z17.string().nullable().default(null),
9141
8692
  lastScaled: z17.record(z17.string(), z17.string()).default({}),
@@ -9160,6 +8711,22 @@ var SILENT_LOG = { info() {
9160
8711
  }, error() {
9161
8712
  } };
9162
8713
  var WAKING_SIGNALS = new Set(WAKING_SIGNAL_KINDS);
8714
+ var DAY_MS = 24 * 36e5;
8715
+ var IDLE_WAKE_STEPS_MS = [7 * DAY_MS, 14 * DAY_MS, 30 * DAY_MS];
8716
+ function wakeLevel(signal) {
8717
+ switch (signal.kind) {
8718
+ case "idle_member":
8719
+ return IDLE_WAKE_STEPS_MS.filter((step) => signal.value >= step).length;
8720
+ case "churn":
8721
+ return signal.value;
8722
+ default:
8723
+ return void 0;
8724
+ }
8725
+ }
8726
+ function wokenAt(signal, now) {
8727
+ const level = wakeLevel(signal);
8728
+ return { at: iso(now), ...level === void 0 ? {} : { level } };
8729
+ }
9163
8730
  var HEARTBEAT_TRIGGER = "heartbeat";
9164
8731
  var OPS_TRIGGER = "ops_event";
9165
8732
  var USER_POST_TRIGGER = "user_post";
@@ -9216,6 +8783,8 @@ var Scheduler = class {
9216
8783
  sweeping = /* @__PURE__ */ new Set();
9217
8784
  /** Crons whose fire's turn is running; a cron's next time passes while one is. */
9218
8785
  cronsInFlight = /* @__PURE__ */ new Set();
8786
+ /** Turns in a row the model service turned away, by session key, which `retryRefused` counts. */
8787
+ refused = /* @__PURE__ */ new Map();
9219
8788
  state = StateSchema.parse({
9220
8789
  cursor: null,
9221
8790
  lastHeartbeat: {}
@@ -9428,10 +8997,6 @@ var Scheduler = class {
9428
8997
  await this.wakeCreator(taskId, project, creator, event.actor, now);
9429
8998
  }
9430
8999
  await this.wakeDependents(taskId, event.actor, now);
9431
- const threaded = await this.board.readThread(taskId).then(() => true, () => false);
9432
- if (!threaded) {
9433
- this.sweep({ kind: "thread", id: taskId });
9434
- }
9435
9000
  return;
9436
9001
  }
9437
9002
  case "thread.closed": {
@@ -9591,7 +9156,7 @@ var Scheduler = class {
9591
9156
  }
9592
9157
  case "ops.signal": {
9593
9158
  const parsed = OpsSignalSchema.safeParse(payload);
9594
- if (!parsed.success || !WAKING_SIGNALS.has(parsed.data.kind)) {
9159
+ if (!parsed.success || !signalWakes(parsed.data)) {
9595
9160
  return;
9596
9161
  }
9597
9162
  await this.wakeSignalReaders(parsed.data, event.actor, now);
@@ -10302,25 +9867,56 @@ var Scheduler = class {
10302
9867
  // ---------------------------------------------------------------------------------------------
10303
9868
  // Operations: signals from state, the scaling rule, and the spend report
10304
9869
  // ---------------------------------------------------------------------------------------------
9870
+ /**
9871
+ * Logs each condition when it is new and again every `signalRepeatMs` while it holds, and marks
9872
+ * each log with whether it wakes the readers of signals (`wakesNow`), so a repeat only logs.
9873
+ */
10305
9874
  async runOperations(now) {
10306
9875
  const signals = await this.collectSignals(now);
10307
9876
  const current = new Set(signals.map((signal) => signal.key));
10308
- for (const key of Object.keys(this.state.opsReported)) {
10309
- if (!current.has(key)) {
10310
- delete this.state.opsReported[key];
9877
+ for (const record of [this.state.opsReported, this.state.opsWoken]) {
9878
+ for (const key of Object.keys(record)) {
9879
+ if (!current.has(key)) {
9880
+ delete record[key];
9881
+ }
10311
9882
  }
10312
9883
  }
10313
9884
  for (const signal of signals) {
10314
9885
  const reported = this.state.opsReported[signal.key];
10315
- if (reported !== void 0 && now - Date.parse(reported) < this.timings.signalRepeatMs) {
9886
+ const wakes = this.wakesNow(signal, reported !== void 0, now);
9887
+ if (!wakes && reported !== void 0 && now - Date.parse(reported) < this.timings.signalRepeatMs) {
10316
9888
  continue;
10317
9889
  }
10318
- await this.board.publishSignal(signal);
9890
+ await this.board.publishSignal({ ...signal, wakes });
10319
9891
  this.state.opsReported[signal.key] = iso(now);
9892
+ if (wakes) {
9893
+ this.state.opsWoken[signal.key] = wokenAt(signal, now);
9894
+ }
10320
9895
  }
10321
9896
  await this.scaleRoles(now);
10322
9897
  await this.reportCost(now);
10323
9898
  }
9899
+ /**
9900
+ * Whether a condition wakes the readers of signals at this pass, which only a waking kind does:
9901
+ * when it is new; for an idle member or churn, when it has risen past the level it last woke at
9902
+ * (`wakeLevel`); for the other kinds, once its last wake is `signalWakeMs` old. Checked on every
9903
+ * pass, so a step wakes when it is crossed, between repeats.
9904
+ */
9905
+ wakesNow(signal, held, now) {
9906
+ if (!WAKING_SIGNALS.has(signal.kind)) {
9907
+ return false;
9908
+ }
9909
+ const last = this.state.opsWoken[signal.key];
9910
+ if (last === void 0) {
9911
+ if (held) {
9912
+ this.state.opsWoken[signal.key] = wokenAt(signal, now);
9913
+ return false;
9914
+ }
9915
+ return true;
9916
+ }
9917
+ const level = wakeLevel(signal);
9918
+ return level === void 0 ? now - Date.parse(last.at) >= this.timings.signalWakeMs : level > (last.level ?? 0);
9919
+ }
10324
9920
  /** Every operations condition that holds right now, from counters and timers only. */
10325
9921
  async collectSignals(now) {
10326
9922
  const signals = [];
@@ -10637,6 +10233,7 @@ var Scheduler = class {
10637
10233
  exitReason: record.exitReason,
10638
10234
  costUsd: record.costUsd
10639
10235
  }, "turn finished");
10236
+ await this.retryRefused(key, { ...item, dispatch }, record);
10640
10237
  } catch (error) {
10641
10238
  this.log.error({ agent: dispatch.agent, project: dispatch.project, error: String(error) }, "turn crashed");
10642
10239
  } finally {
@@ -10655,6 +10252,48 @@ var Scheduler = class {
10655
10252
  }
10656
10253
  }
10657
10254
  }
10255
+ /**
10256
+ * A turn the model service turned away for now, overloaded or failing on its side, is sent again:
10257
+ * the same wake in the same conversation, after the next delay of `transientRetryMs`, with every
10258
+ * check a queued wake gets at dispatch. A wake already queued there, as a heartbeat's or a new
10259
+ * post's, runs the conversation instead. Once the delays are spent the heartbeat takes over, and
10260
+ * a `turn_refused` signal, which wakes nobody, says so. A turn that ends any other way ends the run.
10261
+ */
10262
+ async retryRefused(key, item, record) {
10263
+ if (record.transient !== true || record.exitReason !== "error") {
10264
+ this.refused.delete(key);
10265
+ return;
10266
+ }
10267
+ const refused = (this.refused.get(key) ?? 0) + 1;
10268
+ this.refused.set(key, refused);
10269
+ const { dispatch } = item;
10270
+ const delay = this.timings.transientRetryMs[refused - 1];
10271
+ if (delay !== void 0) {
10272
+ if (this.pending.has(key)) {
10273
+ return;
10274
+ }
10275
+ const { steeredThrough: _steered, ...retry } = item;
10276
+ this.pending.set(key, { ...retry, readyAt: this.now().getTime() + delay });
10277
+ this.log.info({ agent: dispatch.agent, project: dispatch.project, refused, delayMs: delay }, "the model service turned the turn away, sending its wake again");
10278
+ return;
10279
+ }
10280
+ if (refused !== this.timings.transientRetryMs.length + 1) {
10281
+ return;
10282
+ }
10283
+ const agent = await this.tryReadAgent(dispatch.agent);
10284
+ const where = dispatch.thread !== void 0 ? `thread ${dispatch.thread.id}` : dispatch.channel !== void 0 ? `channel ${dispatch.channel}` : dispatch.place !== void 0 ? `project ${dispatch.place}'s channels` : "its home";
10285
+ await this.board.publishSignal({
10286
+ kind: "turn_refused",
10287
+ key: `turn_refused:${key}`,
10288
+ summary: `${dispatch.agent}'s turns in ${where} were turned away by the model service ${refused} times in a row${record.model === null ? "" : ` on ${record.model}`}, the last with "${record.error ?? "no error"}"; its heartbeat takes over`,
10289
+ value: refused,
10290
+ threshold: refused,
10291
+ agent: dispatch.agent,
10292
+ ...agent === null ? {} : { role: agent.role },
10293
+ ...dispatch.project === SOCIETY_SCOPE ? {} : { project: dispatch.project },
10294
+ ...dispatch.thread?.task === true ? { taskId: dispatch.thread.id } : {}
10295
+ });
10296
+ }
10658
10297
  /**
10659
10298
  * Whether a ready turn waits for another of its citizen's turns that must run alone: its
10660
10299
  * onboarding in the project, queued or running, which comes before any other work of its there;
@@ -10773,7 +10412,7 @@ var GOVERNANCE = [
10773
10412
  ' - retirement: {"agent", "reason"}',
10774
10413
  ' - 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',
10775
10414
  ` - 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`,
10776
- ' - 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',
10415
+ ' - 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.',
10777
10416
  "- 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.",
10778
10417
  "- 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.",
10779
10418
  "- 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."
@@ -11083,7 +10722,7 @@ function buildTurnPrompt(input) {
11083
10722
  if (signals.length === 0) {
11084
10723
  lines.push("None since your last turn here.");
11085
10724
  } else {
11086
- lines.push("Counters and timers the board logged since your last turn here, oldest first. A condition that persists is logged again every few hours; one that clears is not logged.", "");
10725
+ lines.push("Counters and timers the board logged since your last turn here, each condition once at its latest log, oldest first, with the hourly spend reports summed into one line. A condition that persists is logged again every few hours; one that clears is not logged.", "");
11087
10726
  for (const { ts, signal } of signals) {
11088
10727
  lines.push(signalLine(ts, signal));
11089
10728
  }
@@ -11147,6 +10786,43 @@ function stuckNotice(stuck) {
11147
10786
  return `${stuck.agent}'s worktree for this task holds commits that \`${stuck.branch}\` lacks and that the runner cannot fast-forward it to, because they no longer build on its tip or another worktree's commits build on it too: ${commits}. Until someone puts them on the branch, it lacks them, and so does what lands from it; any worktree of the project's clone can cherry-pick or merge them by id.`;
11148
10787
  }
11149
10788
  var MAX_SIGNALS = 30;
10789
+ var SIGNAL_READ = 500;
10790
+ var REPORT_TURNS = /^(\d+) turn\(s\) completed /;
10791
+ function sumSpend(records, partial) {
10792
+ const reports = records.filter((record) => record.signal.kind === "turn_cost");
10793
+ const last = reports.at(-1);
10794
+ if (last === void 0 || reports.length === 1 && !partial) {
10795
+ return [...records];
10796
+ }
10797
+ let cost = 0;
10798
+ let turns2 = 0;
10799
+ let dearest = last;
10800
+ for (const report of reports) {
10801
+ cost += report.signal.value;
10802
+ const counted = REPORT_TURNS.exec(report.signal.summary)?.[1];
10803
+ turns2 = turns2 === null || counted === void 0 ? null : turns2 + Number(counted);
10804
+ if (report.signal.value > dearest.signal.value) {
10805
+ dearest = report;
10806
+ }
10807
+ }
10808
+ const first = reports[0] ?? last;
10809
+ const one = reports.length === 1;
10810
+ const summary = [
10811
+ `${turns2 === null ? "turns" : `${turns2} turn(s)`} completed for $${cost.toFixed(2)} in ${reports.length} hourly report(s) logged ${one ? `at ${last.ts}` : `from ${first.ts} to ${last.ts}`}`,
10812
+ ...one ? [] : [`the dearest, logged at ${dearest.ts}, was $${dearest.signal.value.toFixed(2)}`],
10813
+ ...partial ? [`the read holds the newest ${SIGNAL_READ} signals, so these are lower bounds`] : []
10814
+ ].join("; ");
10815
+ const summed = { ...last, signal: { ...last.signal, summary, value: cost } };
10816
+ return records.flatMap((record) => record === last ? [summed] : record.signal.kind === "turn_cost" ? [] : [record]);
10817
+ }
10818
+ function latestPerKey(records) {
10819
+ const latest = /* @__PURE__ */ new Map();
10820
+ for (const record of records) {
10821
+ latest.delete(record.signal.key);
10822
+ latest.set(record.signal.key, record);
10823
+ }
10824
+ return [...latest.values()];
10825
+ }
11150
10826
  var TurnHost = class {
11151
10827
  board;
11152
10828
  mcpUrl;
@@ -11284,7 +10960,7 @@ var TurnHost = class {
11284
10960
  dir: `${PATH_TOKENS.board}/projects/${dispatch.project}/knowledge`,
11285
10961
  topics: await this.board.listKnowledge(dispatch.project)
11286
10962
  };
11287
- const signals = part === void 0 && charter.wakeTriggers.includes(OPS_TRIGGER2) ? (await this.board.listSignals(500)).filter((record2) => wakeScope(agent, record2.signal.project ?? null) === dispatch.project && (lastTurn === null || record2.ts > lastTurn.startedAt)).slice(-MAX_SIGNALS) : null;
10963
+ const signals = part === void 0 && charter.wakeTriggers.includes(OPS_TRIGGER2) ? await this.signalsSince(agent, dispatch.project, lastTurn) : null;
11288
10964
  const waiting = task !== null && task.status === "open" && !task.completing && mayHoldStage(actor, task) ? [task] : [];
11289
10965
  const threads = /* @__PURE__ */ new Map();
11290
10966
  for (const id of new Set(messages.flatMap((message) => message.thread ?? []))) {
@@ -11559,6 +11235,7 @@ var TurnHost = class {
11559
11235
  ...outcome.work === null ? {} : { work: outcome.work },
11560
11236
  ...outcome.exitReason === "stopped" ? { stoppedBy: turn.stopRequestedBy ?? USER_NAME } : {},
11561
11237
  ...outcome.resumeFailed === void 0 ? {} : { resumeFailed: outcome.resumeFailed },
11238
+ ...outcome.transient === true ? { transient: true } : {},
11562
11239
  ...resumeRetries === void 0 ? {} : { resumeRetries },
11563
11240
  ...dropSession ? { sessionDropped: true } : {}
11564
11241
  };
@@ -11738,6 +11415,17 @@ var TurnHost = class {
11738
11415
  this.residentTokens.set(slot, token);
11739
11416
  return token;
11740
11417
  }
11418
+ /**
11419
+ * The signals a reader's prompt lists: those logged for its scope since its last turn there,
11420
+ * the spend reports summed, then each condition once at its latest log, the newest kept.
11421
+ */
11422
+ async signalsSince(agent, scope, lastTurn) {
11423
+ const read2 = await this.board.listSignals(SIGNAL_READ + 1);
11424
+ const past = read2.length > SIGNAL_READ ? read2[0] : void 0;
11425
+ const partial = past !== void 0 && (lastTurn === null || past.ts > lastTurn.startedAt);
11426
+ const since = read2.slice(-SIGNAL_READ).filter((record) => wakeScope(agent, record.signal.project ?? null) === scope && (lastTurn === null || record.ts > lastTurn.startedAt));
11427
+ return latestPerKey(sumSpend(since, partial)).slice(-MAX_SIGNALS);
11428
+ }
11741
11429
  /**
11742
11430
  * Where a conversation works: a task's on its branch, which its stage's holder has to itself,
11743
11431
  * any other thread's, a channel's, and a project's channels' seen from the society scope in a
@@ -12625,6 +12313,8 @@ import pino from "pino";
12625
12313
  // packages/board-mcp/dist/index.js
12626
12314
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12627
12315
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
12316
+ var MAX_TOOL_INPUT_ELEMENTS = 1e3;
12317
+ var MAX_REQUEST_BODY_SIZE = 1024 * 1024;
12628
12318
  var BOARD_TOOLS = VERB_NAMES.map((name) => ({
12629
12319
  name,
12630
12320
  description: VERB_DESCRIPTIONS[name],
@@ -12647,7 +12337,7 @@ function describeError(error) {
12647
12337
  return String(error);
12648
12338
  }
12649
12339
  async function createBoardMcpServer(board2, actor, version) {
12650
- const server2 = new McpServer({ name: "stellaris-board", version });
12340
+ const server2 = new McpServer({ name: "stellaris-board", version }, { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS });
12651
12341
  const charter = await board2.readRole(actor.role);
12652
12342
  for (const tool of toolsForRole(charter)) {
12653
12343
  server2.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, async (args) => {
@@ -12668,7 +12358,10 @@ async function createBoardMcpServer(board2, actor, version) {
12668
12358
  }
12669
12359
  async function handleMcpRequest(board2, actor, request, version) {
12670
12360
  const server2 = await createBoardMcpServer(board2, actor, version);
12671
- const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
12361
+ const transport = new WebStandardStreamableHTTPServerTransport({
12362
+ enableJsonResponse: true,
12363
+ maxRequestBodySize: MAX_REQUEST_BODY_SIZE
12364
+ });
12672
12365
  await server2.connect(transport);
12673
12366
  try {
12674
12367
  return await transport.handleRequest(request, {
@@ -12685,6 +12378,25 @@ import { compress } from "hono/compress";
12685
12378
  import { streamSSE as streamSSE2 } from "hono/streaming";
12686
12379
  import { z as z19, ZodError } from "zod";
12687
12380
 
12381
+ // apps/server/dist/body-limit.js
12382
+ import { bodyLimit } from "hono/body-limit";
12383
+ var ENROLL_BODY_LIMIT_BYTES = 16 * 1024;
12384
+ var API_BODY_LIMIT_BYTES = 1024 * 1024;
12385
+ var RUNNER_BODY_LIMIT_BYTES = 8 * 1024 * 1024;
12386
+ var RUNNER_ANSWER_LIMIT_BYTES = 16 * 1024 * 1024;
12387
+ function limitBody(maxSize) {
12388
+ return bodyLimit({
12389
+ maxSize,
12390
+ onError: (c) => {
12391
+ c.header("connection", "close");
12392
+ return c.json({
12393
+ error: "PAYLOAD_TOO_LARGE",
12394
+ message: `the request body is over the ${maxSize} bytes this route takes`
12395
+ }, 413);
12396
+ }
12397
+ });
12398
+ }
12399
+
12688
12400
  // apps/server/dist/github-auth.js
12689
12401
  import { randomBytes as randomBytes2 } from "node:crypto";
12690
12402
  import { Hono } from "hono";
@@ -12706,9 +12418,21 @@ function githubAvatar(url) {
12706
12418
  return void 0;
12707
12419
  }
12708
12420
  }
12421
+ var BOARD_ORIGIN = "https://board.invalid";
12709
12422
  function safeNext(next) {
12710
- return next !== void 0 && /^\/(?![/\\])/.test(next) ? next : "/";
12423
+ if (next === void 0 || !next.startsWith("/")) {
12424
+ return "/";
12425
+ }
12426
+ let url;
12427
+ try {
12428
+ url = new URL(next, BOARD_ORIGIN);
12429
+ } catch {
12430
+ return "/";
12431
+ }
12432
+ const path11 = `${url.pathname}${url.search}`;
12433
+ return url.origin === BOARD_ORIGIN && /^\/(?![/\\])/.test(path11) ? path11 : "/";
12711
12434
  }
12435
+ var APP_LANDING = "stellaris://signin";
12712
12436
  function land(c, next, fragment) {
12713
12437
  return c.redirect(`${next}#${fragment}`, 302);
12714
12438
  }
@@ -12729,7 +12453,8 @@ function githubAuthRoutes(options) {
12729
12453
  }
12730
12454
  }
12731
12455
  const state = randomBytes2(24).toString("base64url");
12732
- states.set(state, { next: safeNext(c.req.query("next")), expiresAt: now() + STATE_TTL_MS });
12456
+ const next = c.req.query("landing") === "app" ? APP_LANDING : safeNext(c.req.query("next"));
12457
+ states.set(state, { next, expiresAt: now() + STATE_TTL_MS });
12733
12458
  const authorize = new URL("https://github.com/login/oauth/authorize");
12734
12459
  authorize.searchParams.set("client_id", github.clientId);
12735
12460
  authorize.searchParams.set("state", state);
@@ -12992,8 +12717,9 @@ function bearer(c) {
12992
12717
  function runnerRoutes(board2, hub, enrollments, enrollLimit) {
12993
12718
  const routes = new Hono2();
12994
12719
  if (enrollments !== void 0) {
12995
- routes.post("/enroll", enrollLimit ?? ((_c, next) => next()), async (c) => c.json(enrollments.open(RunnerEnrollRequestSchema.parse(await c.req.json()))));
12996
- routes.post("/enroll/poll", async (c) => {
12720
+ const enrollBody = limitBody(ENROLL_BODY_LIMIT_BYTES);
12721
+ routes.post("/enroll", enrollLimit ?? ((_c, next) => next()), enrollBody, async (c) => c.json(enrollments.open(RunnerEnrollRequestSchema.parse(await c.req.json()))));
12722
+ routes.post("/enroll/poll", enrollBody, async (c) => {
12997
12723
  const { deviceCode } = RunnerEnrollPollSchema.parse(await c.req.json());
12998
12724
  const status = enrollments.poll(deviceCode);
12999
12725
  return status === null ? c.json({ error: "NOT_FOUND", message: "no such enrollment; it may have expired" }, 404) : c.json(status);
@@ -13008,7 +12734,8 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
13008
12734
  c.set("runner", runner);
13009
12735
  await next();
13010
12736
  });
13011
- routes.post("/hello", async (c) => c.json(await hub.register(c.get("runner"), RunnerHelloSchema.parse(await c.req.json()))));
12737
+ const body = limitBody(RUNNER_BODY_LIMIT_BYTES);
12738
+ routes.post("/hello", body, async (c) => c.json(await hub.register(c.get("runner"), RunnerHelloSchema.parse(await c.req.json()))));
13012
12739
  routes.get("/stream", (c) => {
13013
12740
  const runner = c.get("runner");
13014
12741
  return streamSSE(c, async (stream) => {
@@ -13046,7 +12773,7 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
13046
12773
  }
13047
12774
  });
13048
12775
  });
13049
- routes.post("/turns/:id/events", async (c) => {
12776
+ routes.post("/turns/:id/events", body, async (c) => {
13050
12777
  const { entries } = TurnEventsSchema.parse(await c.req.json());
13051
12778
  await hub.turnEvents(c.get("runner"), UlidSchema.parse(c.req.param("id")), entries);
13052
12779
  return c.json({ ok: true });
@@ -13055,21 +12782,21 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
13055
12782
  hub.received(c.get("runner"), UlidSchema.parse(c.req.param("id")));
13056
12783
  return c.json({ ok: true });
13057
12784
  });
13058
- routes.post("/turns/:id/outcome", async (c) => c.json(await hub.turnOutcome(c.get("runner"), UlidSchema.parse(c.req.param("id")), TurnOutcomeSchema.parse(await c.req.json()))));
13059
- routes.post("/requests/:id", async (c) => {
12785
+ routes.post("/turns/:id/outcome", body, async (c) => c.json(await hub.turnOutcome(c.get("runner"), UlidSchema.parse(c.req.param("id")), TurnOutcomeSchema.parse(await c.req.json()))));
12786
+ routes.post("/requests/:id", limitBody(RUNNER_ANSWER_LIMIT_BYTES), async (c) => {
13060
12787
  hub.answer(c.get("runner"), c.req.param("id"), RunnerAnswerSchema.parse(await c.req.json()));
13061
12788
  return c.json({ ok: true });
13062
12789
  });
13063
- routes.post("/workspaces", async (c) => {
12790
+ routes.post("/workspaces", body, async (c) => {
13064
12791
  hub.heldWorkspaces(c.get("runner"), HeldWorkspacesSchema.parse(await c.req.json()).workspaces);
13065
12792
  return c.json({ ok: true });
13066
12793
  });
13067
- routes.post("/warm", async (c) => {
12794
+ routes.post("/warm", body, async (c) => {
13068
12795
  hub.warmChanged(c.get("runner"), WarmSessionsSchema.parse(await c.req.json()).warm);
13069
12796
  return c.json({ ok: true });
13070
12797
  });
13071
12798
  routes.get("/board/manifest", async (c) => c.json({ files: await board2.boardManifest() }));
13072
- routes.post("/board/read", async (c) => c.json({ files: await board2.readBoardFiles(FileReadSchema.parse(await c.req.json()).paths) }));
12799
+ routes.post("/board/read", body, async (c) => c.json({ files: await board2.readBoardFiles(FileReadSchema.parse(await c.req.json()).paths) }));
13073
12800
  const home = new Hono2();
13074
12801
  home.use("*", async (c, next) => {
13075
12802
  const agent = NameSchema.parse(c.req.param("agent"));
@@ -13221,6 +12948,7 @@ function createApp(deps) {
13221
12948
  };
13222
12949
  const api = new Hono3();
13223
12950
  api.use("*", authenticate);
12951
+ api.on(["POST", "PUT", "PATCH", "DELETE"], "*", limitBody(API_BODY_LIMIT_BYTES));
13224
12952
  api.get("/me", (c) => {
13225
12953
  const signIn = board2.signInOf(bearer2(c) ?? "");
13226
12954
  return c.json(signIn === null ? c.get("actor") : {
@@ -13370,14 +13098,23 @@ function createApp(deps) {
13370
13098
  api.get("/projects/:slug/tasks", async (c) => c.json(await board2.listTasks(c.req.param("slug"))));
13371
13099
  api.get("/projects/:slug/dashboard", async (c) => c.json(await board2.readDashboard(c.req.param("slug"))));
13372
13100
  api.get("/projects/:slug/knowledge", async (c) => c.json(await board2.listKnowledge(c.req.param("slug"))));
13101
+ const branchFile = async (c, project, branch, file) => {
13102
+ if (deps.runners === void 0) {
13103
+ throw new BoardError("NOT_FOUND", "this board has no runners to read files from");
13104
+ }
13105
+ return c.json(await deps.runners.readBranch(project, branch, BranchPathSchema.parse(file)));
13106
+ };
13107
+ const projectFile = async (c, file) => {
13108
+ const project = await board2.readProject(NameSchema.parse(c.req.param("slug")));
13109
+ return branchFile(c, project.slug, project.defaultBranch, file);
13110
+ };
13111
+ api.get("/projects/:slug/files", (c) => projectFile(c, ""));
13112
+ api.get("/projects/:slug/files/:path{.+}", (c) => projectFile(c, c.req.param("path")));
13373
13113
  api.get("/tasks/:id", async (c) => c.json((await board2.findTask(c.req.param("id"))).task));
13374
13114
  api.get("/tasks/:id/thread", async (c) => c.json(await board2.listThread(c.req.param("id"))));
13375
13115
  const taskFile = async (c, file) => {
13376
13116
  const { task } = await board2.findTask(UlidSchema.parse(c.req.param("id")));
13377
- if (deps.runners === void 0) {
13378
- throw new BoardError("NOT_FOUND", "this board has no runners to read a task's files from");
13379
- }
13380
- return c.json(await deps.runners.readBranch(task.project, `task/${task.id}`, BranchPathSchema.parse(file)));
13117
+ return branchFile(c, task.project, `task/${task.id}`, file);
13381
13118
  };
13382
13119
  api.get("/tasks/:id/files", (c) => taskFile(c, ""));
13383
13120
  api.get("/tasks/:id/changes", async (c) => {