@kubitnodes/stellaris 0.3.20 → 0.3.21

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 (46) hide show
  1. package/dist/cli/index.js +182 -56
  2. package/dist/server/index.js +281 -85
  3. package/package.json +1 -1
  4. package/web/dist/assets/BackLink-23Q1JvmZ.js +1 -0
  5. package/web/dist/assets/{ChannelForms-BYrcUldk.js → ChannelForms-0Cwh59E7.js} +1 -1
  6. package/web/dist/assets/{ChannelView-GJ_DusLQ.js → ChannelView-CqWV1zza.js} +2 -2
  7. package/web/dist/assets/{CitizenView-Cx7IYSRr.js → CitizenView-s5OdjXmB.js} +2 -2
  8. package/web/dist/assets/{CitizensView-IKH7_yNQ.js → CitizensView-r6SZLnVl.js} +1 -1
  9. package/web/dist/assets/{CliIcon-vSuwLv2K.js → CliIcon-BOSygXeI.js} +1 -1
  10. package/web/dist/assets/{CronsView-DJbi8Qhx.js → CronsView-Y-bAE0l2.js} +1 -1
  11. package/web/dist/assets/{KnowledgeView-CoYOG9tI.js → KnowledgeView-CI4PJWwm.js} +1 -1
  12. package/web/dist/assets/{Markdown-YC-fbHiE.js → Markdown-DyckB74r.js} +4 -4
  13. package/web/dist/assets/{MessageItem-D7MUCA_T.js → MessageItem-DI05jbrI.js} +1 -1
  14. package/web/dist/assets/{MetricsView-CE94VW0I.js → MetricsView-DKh-lEW5.js} +1 -1
  15. package/web/dist/assets/{NeedsYouView-Otnm3lcG.js → NeedsYouView-C6FfiTsg.js} +1 -1
  16. package/web/dist/assets/{Overview-B8f6rLb2.js → Overview-D5RvWBg5.js} +1 -1
  17. package/web/dist/assets/{Pane-_D7n5uKH.js → Pane-BendOaje.js} +1 -1
  18. package/web/dist/assets/{Picker-DUiiQMFN.js → Picker-BuVnakvi.js} +1 -1
  19. package/web/dist/assets/{ProjectView-D_ACOmEd.js → ProjectView-blBEh70W.js} +1 -1
  20. package/web/dist/assets/ProposalView-CMntpKTc.js +2 -0
  21. package/web/dist/assets/{ProposalsView-BovF6CMD.js → ProposalsView-CVUAV2Mm.js} +1 -1
  22. package/web/dist/assets/{RunnersView-B2_uCVHC.js → RunnersView-B-wuWVgB.js} +1 -1
  23. package/web/dist/assets/{SocietyView-Bf0iX8AY.js → SocietyView-CaVbfJWz.js} +1 -1
  24. package/web/dist/assets/{SubjectThread-B_Vmgs3d.js → SubjectThread-Bd_-sDhJ.js} +1 -1
  25. package/web/dist/assets/{TaskFileView-BzO0gjE6.js → TaskFileView-CO4Y97OT.js} +2 -2
  26. package/web/dist/assets/{TaskView-DB_Nji99.js → TaskView-DRZoHALo.js} +2 -2
  27. package/web/dist/assets/{TasksView-Bz36YYWq.js → TasksView-CQVQo3zF.js} +1 -1
  28. package/web/dist/assets/{ThreadForms-1mbMZsbv.js → ThreadForms-CUSeWu0c.js} +1 -1
  29. package/web/dist/assets/{ThreadItem-QJSA92C4.js → ThreadItem-BPYYXh3V.js} +1 -1
  30. package/web/dist/assets/{ThreadView-BECLqt5B.js → ThreadView-BcI2i6FI.js} +1 -1
  31. package/web/dist/assets/{citizen-DVFr5T6R.js → citizen-Eff1h9rA.js} +1 -1
  32. package/web/dist/assets/{compose-_6feqP1t.js → compose-D73Agshx.js} +1 -1
  33. package/web/dist/assets/{format-DxXtbdNM.js → format-fw0G_Vff.js} +1 -1
  34. package/web/dist/assets/governance-sRsfOw52.js +1 -0
  35. package/web/dist/assets/index-CYiok0pT.js +23 -0
  36. package/web/dist/assets/link-Cl7vKbOz.js +1 -0
  37. package/web/dist/assets/{tasks-B6I9DJ8O.js → tasks-CyXIxoAy.js} +1 -1
  38. package/web/dist/assets/useMatch-D-tEHQEw.js +1 -0
  39. package/web/dist/assets/{useStickyScroll-Ca7sWbGH.js → useStickyScroll-Bn4YyEXL.js} +1 -1
  40. package/web/dist/index.html +8 -8
  41. package/web/dist/assets/BackLink-BjfX2KZl.js +0 -1
  42. package/web/dist/assets/ProposalView-4d0U3nEb.js +0 -2
  43. package/web/dist/assets/governance-C9xH4QTm.js +0 -1
  44. package/web/dist/assets/index-BEVCPm4C.js +0 -23
  45. package/web/dist/assets/link-CTjFMNoY.js +0 -1
  46. package/web/dist/assets/useMatch-B3RzAe8y.js +0 -1
package/dist/cli/index.js CHANGED
@@ -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,
@@ -615,9 +615,54 @@ var ChannelProposalSchema = z4.object({
615
615
  name: NameSchema,
616
616
  purpose: z4.string().min(1)
617
617
  });
618
+ var RoleChangesSchema = z4.strictObject({
619
+ maxReplicas: z4.number().int().min(1).optional(),
620
+ backlogThreshold: z4.number().positive().optional(),
621
+ wakeTriggers: z4.array(CharterTriggerSchema).optional(),
622
+ verbs: z4.array(VerbNameSchema).optional()
623
+ }).refine((changes) => Object.values(changes).some((value) => value !== void 0), {
624
+ message: "a reallocation changes at least one of maxReplicas, backlogThreshold, wakeTriggers, verbs"
625
+ });
618
626
  var ReallocationProposalSchema = z4.object({
627
+ role: NameSchema,
628
+ changes: RoleChangesSchema,
619
629
  description: z4.string().min(1)
620
630
  });
631
+ var LegacyReallocationSchema = z4.object({ description: z4.string().min(1) });
632
+ function applyRoleChanges(role2, changes) {
633
+ return {
634
+ ...role2,
635
+ ...changes.maxReplicas === void 0 ? {} : { maxReplicas: changes.maxReplicas },
636
+ ...changes.backlogThreshold === void 0 ? {} : { backlogThreshold: changes.backlogThreshold },
637
+ ...changes.wakeTriggers === void 0 ? {} : { wakeTriggers: [...changes.wakeTriggers] },
638
+ ...changes.verbs === void 0 ? {} : { verbs: [...changes.verbs] }
639
+ };
640
+ }
641
+ function listedOrNone(values) {
642
+ return values.length === 0 ? "none" : values.join(", ");
643
+ }
644
+ function roleChangeLines(role2, changes) {
645
+ const lines = [];
646
+ if (changes.maxReplicas !== void 0 && changes.maxReplicas !== role2.maxReplicas) {
647
+ lines.push(`maxReplicas ${role2.maxReplicas} \u2192 ${changes.maxReplicas}`);
648
+ }
649
+ if (changes.backlogThreshold !== void 0 && changes.backlogThreshold !== role2.backlogThreshold) {
650
+ lines.push(`backlogThreshold ${role2.backlogThreshold} \u2192 ${changes.backlogThreshold}`);
651
+ }
652
+ const wakes = changes.wakeTriggers;
653
+ if (wakes !== void 0 && (wakes.length !== role2.wakeTriggers.length || wakes.some((trigger) => !role2.wakeTriggers.includes(trigger)))) {
654
+ lines.push(`wakeTriggers ${listedOrNone(role2.wakeTriggers)} \u2192 ${listedOrNone(wakes)}`);
655
+ }
656
+ if (changes.verbs !== void 0) {
657
+ const next = changes.verbs;
658
+ const added = next.filter((verb) => !role2.verbs.includes(verb)).map((verb) => `+${verb}`);
659
+ const removed = role2.verbs.filter((verb) => !next.includes(verb)).map((verb) => `\u2212${verb}`);
660
+ if (added.length + removed.length > 0) {
661
+ lines.push(`verbs ${[...added, ...removed].join(" ")}`);
662
+ }
663
+ }
664
+ return lines;
665
+ }
621
666
  var RetirementProposalSchema = z4.object({
622
667
  agent: NameSchema,
623
668
  reason: z4.string().min(1)
@@ -649,6 +694,32 @@ var ROLE_KIND_APPROVERS = {
649
694
  reallocation: [USER_ROLE, "steward"],
650
695
  skill: [USER_ROLE, "steward"]
651
696
  };
697
+ var USER_DECIDED_ROLE_CHANGES = ["verbs", "maxReplicas"];
698
+ function proposalApprovers(kind, charter) {
699
+ if (kind === "reallocation") {
700
+ const parsed = ReallocationProposalSchema.safeParse(charter);
701
+ if (parsed.success && USER_DECIDED_ROLE_CHANGES.some((field) => parsed.data.changes[field] !== void 0)) {
702
+ return [USER_ROLE];
703
+ }
704
+ }
705
+ return ROLE_KIND_APPROVERS[kind];
706
+ }
707
+ function describeRoleChanges(changes) {
708
+ const parts = [];
709
+ if (changes.maxReplicas !== void 0) {
710
+ parts.push(`up to ${changes.maxReplicas} per project`);
711
+ }
712
+ if (changes.backlogThreshold !== void 0) {
713
+ parts.push(`backlog threshold ${changes.backlogThreshold}`);
714
+ }
715
+ if (changes.wakeTriggers !== void 0) {
716
+ parts.push(changes.wakeTriggers.length === 0 ? "no ambient wakes" : `wakes on ${changes.wakeTriggers.join(", ")}`);
717
+ }
718
+ if (changes.verbs !== void 0) {
719
+ parts.push(`${changes.verbs.length} verbs`);
720
+ }
721
+ return parts.join(", ");
722
+ }
652
723
  function describeCharter(kind, charter) {
653
724
  switch (kind) {
654
725
  case "member": {
@@ -680,9 +751,14 @@ function describeCharter(kind, charter) {
680
751
  }
681
752
  case "reallocation": {
682
753
  const parsed = ReallocationProposalSchema.safeParse(charter);
683
- if (!parsed.success)
754
+ if (parsed.success) {
755
+ const { role: role2, changes, description } = parsed.data;
756
+ return `reallocation of role ${role2} (${describeRoleChanges(changes)}): ${description}`;
757
+ }
758
+ const legacy = LegacyReallocationSchema.safeParse(charter);
759
+ if (!legacy.success)
684
760
  break;
685
- return `reallocation: ${parsed.data.description}`;
761
+ return `reallocation: ${legacy.data.description}`;
686
762
  }
687
763
  case "skill": {
688
764
  const parsed = SkillProposalSchema.safeParse(charter);
@@ -1740,7 +1816,7 @@ var RunnerConfigSchema = z11.object({
1740
1816
  });
1741
1817
 
1742
1818
  // packages/shared/dist/version.js
1743
- var STELLARIS_VERSION = "0.3.20";
1819
+ var STELLARIS_VERSION = "0.3.21";
1744
1820
 
1745
1821
  // packages/board-core/dist/errors.js
1746
1822
  var BoardError = class extends Error {
@@ -4474,18 +4550,16 @@ ${DASHBOARD_LINE}
4474
4550
  async readDigest(actor, input = {}) {
4475
4551
  const args = VerbInputs.read_inbox.parse(input);
4476
4552
  await this.authorize(actor, "read_inbox");
4477
- return this.mutex.run(async () => {
4478
- const stored = await this.readCursors(actor.name);
4479
- const from = args.since_cursor === void 0 ? stored : { ...NO_CURSORS, digest: args.since_cursor };
4480
- const conversation = actor.scope === void 0 ? void 0 : { scope: actor.scope, part: conversationPart(actor) ?? null };
4481
- const filed = (await this.digestSince(actor, from, conversation)).slice(0, args.limit);
4482
- if (args.advance) {
4483
- await this.advanceCursors(actor.name, filed);
4484
- }
4485
- const last = filed.at(-1)?.message.id;
4486
- const cursor = last ?? (conversation === void 0 ? from.digest : cursorOf(from, conversation.scope, conversation.part));
4487
- return { messages: filed.map((entry) => entry.message), cursor };
4488
- });
4553
+ const { cursors: stored, upto } = await this.digestSnapshot(actor.name);
4554
+ const from = args.since_cursor === void 0 ? stored : { ...NO_CURSORS, digest: args.since_cursor };
4555
+ const conversation = actor.scope === void 0 ? void 0 : { scope: actor.scope, part: conversationPart(actor) ?? null };
4556
+ const filed = (await this.digestSince(actor, from, upto, conversation)).slice(0, args.limit);
4557
+ if (args.advance) {
4558
+ await this.mutex.run(() => this.advanceCursors(actor.name, filed));
4559
+ }
4560
+ const last = filed.at(-1)?.message.id;
4561
+ const cursor = last ?? (conversation === void 0 ? from.digest : cursorOf(from, conversation.scope, conversation.part));
4562
+ return { messages: filed.map((entry) => entry.message), cursor };
4489
4563
  }
4490
4564
  /**
4491
4565
  * How many unread digest messages a member has in each conversation: a scope's home, for its
@@ -4497,28 +4571,39 @@ ${DASHBOARD_LINE}
4497
4571
  * whose signals that call for action wake their readers directly.
4498
4572
  */
4499
4573
  async unreadByConversation(actor) {
4500
- return this.mutex.run(async () => {
4501
- const counts = /* @__PURE__ */ new Map();
4502
- const unscoped = { name: actor.name, role: actor.role };
4503
- for (const { scope, part, message, news } of await this.digestSince(unscoped, await this.readCursors(actor.name))) {
4504
- if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
4505
- continue;
4506
- }
4507
- const posted = parseChannelRef(message.channel).project ?? SOCIETY_SCOPE;
4508
- if (posted !== scope && scope !== SOCIETY_SCOPE) {
4509
- continue;
4510
- }
4511
- const key = conversationKey(scope, part);
4512
- const entry = counts.get(key) ?? {
4513
- scope,
4514
- thread: threadOfPart(part) ?? null,
4515
- channel: channelOfPart(part) ?? null,
4516
- count: 0
4517
- };
4518
- counts.set(key, { ...entry, count: entry.count + 1 });
4574
+ const { cursors, upto } = await this.digestSnapshot(actor.name);
4575
+ const counts = /* @__PURE__ */ new Map();
4576
+ const unscoped = { name: actor.name, role: actor.role };
4577
+ for (const { scope, part, message, news } of await this.digestSince(unscoped, cursors, upto)) {
4578
+ if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
4579
+ continue;
4519
4580
  }
4520
- return [...counts.values()];
4521
- });
4581
+ const posted = parseChannelRef(message.channel).project ?? SOCIETY_SCOPE;
4582
+ if (posted !== scope && scope !== SOCIETY_SCOPE) {
4583
+ continue;
4584
+ }
4585
+ const key = conversationKey(scope, part);
4586
+ const entry = counts.get(key) ?? {
4587
+ scope,
4588
+ thread: threadOfPart(part) ?? null,
4589
+ channel: channelOfPart(part) ?? null,
4590
+ count: 0
4591
+ };
4592
+ counts.set(key, { ...entry, count: entry.count + 1 });
4593
+ }
4594
+ return [...counts.values()];
4595
+ }
4596
+ /**
4597
+ * A reader's cursors and the id below which every message is already on disk, taken under the
4598
+ * mutex, which a digest scan then leaves: messages carry ids from the same monotonic source and are
4599
+ * written under the mutex, so the scan, which counts nothing newer than the mark, cannot pass over
4600
+ * one written while it reads, and a post never waits for a scan.
4601
+ */
4602
+ digestSnapshot(agent2) {
4603
+ return this.mutex.run(async () => ({
4604
+ cursors: await this.readCursors(agent2),
4605
+ upto: this.newId()
4606
+ }));
4522
4607
  }
4523
4608
  async readCursors(agent2) {
4524
4609
  const file = this.paths.agentCursors(agent2);
@@ -4560,9 +4645,10 @@ ${DASHBOARD_LINE}
4560
4645
  * by the user wherever it is. With a conversation, only what is filed there: for a thread, every
4561
4646
  * message of the thread, since the conversation is about it; for a channel's, that channel's
4562
4647
  * top-level messages; for a scope's home, general's and those of a project the member is not in,
4563
- * since each thread and each other channel is a conversation of its own.
4648
+ * since each thread and each other channel is a conversation of its own. Nothing newer than
4649
+ * `upto` counts (see `digestSnapshot`).
4564
4650
  */
4565
- async digestSince(actor, cursors, conversation) {
4651
+ async digestSince(actor, cursors, upto, conversation) {
4566
4652
  const conversationThread = threadOfPart(conversation?.part);
4567
4653
  if (conversation !== void 0 && conversationThread !== void 0) {
4568
4654
  const { scope, part } = conversation;
@@ -4570,7 +4656,7 @@ ${DASHBOARD_LINE}
4570
4656
  if (found === null) {
4571
4657
  return [];
4572
4658
  }
4573
- const messages = await this.readMessagesIn(this.paths.threadMessages(found.threads, conversationThread), cursorOf(cursors, scope, part));
4659
+ const messages = await this.readMessagesIn(this.paths.threadMessages(found.threads, conversationThread), cursorOf(cursors, scope, part), upto);
4574
4660
  return messages.filter((message) => message.author !== actor.name).map((message) => ({ scope, part, message })).toSorted(byId);
4575
4661
  }
4576
4662
  const agent2 = await this.readAgent(actor.name);
@@ -4588,7 +4674,7 @@ ${DASHBOARD_LINE}
4588
4674
  ];
4589
4675
  const known = starts.filter((start) => start !== null);
4590
4676
  const since = known.length < starts.length ? null : known.toSorted((a, b) => a < b ? -1 : a > b ? 1 : 0)[0] ?? null;
4591
- for await (const message of this.iterateMessages(since)) {
4677
+ for await (const message of this.iterateMessages(since, void 0, upto)) {
4592
4678
  const filedIn = wakeScope(agent2, parseChannelRef(message.channel).project);
4593
4679
  const thread2 = message.thread ?? null;
4594
4680
  const asked = message.mentions.includes(actor.name) || frontDesk && message.author === USER_NAME;
@@ -5712,6 +5798,9 @@ ${args.summary}`);
5712
5798
  }
5713
5799
  if (args.blocked_by !== void 0) {
5714
5800
  for (const id of args.blocked_by) {
5801
+ if (id === current.id) {
5802
+ throw new BoardError("VALIDATION", `task ${current.id} cannot be blocked by itself`);
5803
+ }
5715
5804
  await this.findTask(id);
5716
5805
  }
5717
5806
  next = { ...next, blockedBy: [...args.blocked_by] };
@@ -5748,6 +5837,7 @@ ${args.summary}`);
5748
5837
  from: current.status,
5749
5838
  to: task2.status,
5750
5839
  note: args.note ?? null,
5840
+ ...args.blocked_by === void 0 ? {} : { blockedBy: task2.blockedBy, previousBlockedBy: current.blockedBy },
5751
5841
  ...args.pull_request === void 0 ? {} : { pullRequest: task2.pullRequest?.url ?? null }
5752
5842
  });
5753
5843
  }
@@ -7046,12 +7136,37 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7046
7136
  case "archive":
7047
7137
  await this.validateArchive(ArchiveProposalSchema.parse(charter).project);
7048
7138
  return;
7049
- case "reallocation":
7139
+ case "reallocation": {
7140
+ const parsed = ReallocationProposalSchema.safeParse(charter);
7141
+ if (parsed.success) {
7142
+ await this.reallocatedRole(parsed.data);
7143
+ } else {
7144
+ LegacyReallocationSchema.parse(charter);
7145
+ }
7050
7146
  return;
7147
+ }
7051
7148
  default:
7052
7149
  return;
7053
7150
  }
7054
7151
  }
7152
+ /**
7153
+ * The role as a reallocation would leave it, with what changes: refused for the user's charter,
7154
+ * a role that does not exist, a charter the changes would make invalid, and changes the role
7155
+ * has already.
7156
+ */
7157
+ async reallocatedRole(reallocation) {
7158
+ if (reallocation.role === USER_ROLE) {
7159
+ throw new BoardError("FORBIDDEN", "the user charter is not subject to proposals");
7160
+ }
7161
+ const current = await this.readRole(reallocation.role);
7162
+ const next = RoleCharterSchema.parse(applyRoleChanges(current, reallocation.changes));
7163
+ await this.validateRole(next);
7164
+ const changed = roleChangeLines(current, reallocation.changes);
7165
+ if (changed.length === 0) {
7166
+ throw new BoardError("VALIDATION", `role ${current.name} has these values already; the reallocation changes nothing`);
7167
+ }
7168
+ return { next, changed };
7169
+ }
7055
7170
  async executeProvision(by, proposal2) {
7056
7171
  const meta = { proposalId: proposal2.id };
7057
7172
  switch (proposal2.kind) {
@@ -7088,8 +7203,15 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7088
7203
  const { members } = await this.archiveUnlocked(by, input.project, input.reason, meta);
7089
7204
  return { project: input.project, members };
7090
7205
  }
7091
- case "reallocation":
7092
- return void 0;
7206
+ case "reallocation": {
7207
+ const parsed = ReallocationProposalSchema.safeParse(proposal2.charter);
7208
+ if (!parsed.success) {
7209
+ return void 0;
7210
+ }
7211
+ const { next, changed } = await this.reallocatedRole(parsed.data);
7212
+ await this.writeRoleUnlocked(by, next, meta);
7213
+ return { role: next.name, changed };
7214
+ }
7093
7215
  default:
7094
7216
  return void 0;
7095
7217
  }
@@ -7789,7 +7911,7 @@ ${charter.purpose}
7789
7911
  if (filed.proposedBy === actor.name) {
7790
7912
  throw new BoardError("FORBIDDEN", "a proposer never decides its own proposal");
7791
7913
  }
7792
- const approvers = ROLE_KIND_APPROVERS[filed.kind];
7914
+ const approvers = proposalApprovers(filed.kind, filed.charter);
7793
7915
  if (!approvers.includes(actor.role)) {
7794
7916
  throw new BoardError("FORBIDDEN", `a ${filed.kind} proposal requires one of: ${approvers.join(", ")}`);
7795
7917
  }
@@ -7872,7 +7994,7 @@ ${charter.purpose}
7872
7994
  }
7873
7995
  if (subject?.kind === "proposal") {
7874
7996
  const proposal2 = await this.readProposal(subject.id);
7875
- if (proposal2.proposedBy === actor.name || proposal2.decidedBy === actor.name || ROLE_KIND_APPROVERS[proposal2.kind].includes(actor.role)) {
7997
+ if (proposal2.proposedBy === actor.name || proposal2.decidedBy === actor.name || proposalApprovers(proposal2.kind, proposal2.charter).includes(actor.role)) {
7876
7998
  return true;
7877
7999
  }
7878
8000
  return (await this.readRole(actor.role)).wakeTriggers.includes(OPS_WAKE_TRIGGER);
@@ -7929,11 +8051,12 @@ ${charter.purpose}
7929
8051
  });
7930
8052
  await this.endCronsUnlocked(by, `its thread closed: its ${found.thread.subject?.kind ?? "subject"} was ${ended}`, (cron2) => cron2.thread === subjectId);
7931
8053
  }
7932
- async readMessagesIn(dir, since) {
8054
+ /** The messages in a folder after `since` and, when given, up to `upto`, read by name first. */
8055
+ async readMessagesIn(dir, since, upto) {
7933
8056
  const messages = [];
7934
8057
  for (const file of await listFiles(dir)) {
7935
8058
  const id = file.slice(0, 26);
7936
- if (since !== null && id <= since) {
8059
+ if (since !== null && id <= since || upto !== void 0 && id > upto) {
7937
8060
  continue;
7938
8061
  }
7939
8062
  const doc = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
@@ -7941,20 +8064,23 @@ ${charter.purpose}
7941
8064
  }
7942
8065
  return messages;
7943
8066
  }
7944
- /** Every message newer than `since` across society channels, project channels, and threads. */
7945
- async *iterateMessages(since, project2) {
8067
+ /**
8068
+ * Every message newer than `since`, and no newer than `upto` when given, across society
8069
+ * channels, project channels, and threads.
8070
+ */
8071
+ async *iterateMessages(since, project2, upto) {
7946
8072
  if (project2 === void 0) {
7947
8073
  for (const channel of await listDirs(this.paths.societyChannels())) {
7948
- yield* await this.readMessagesIn(this.paths.societyChannel(channel), since);
8074
+ yield* await this.readMessagesIn(this.paths.societyChannel(channel), since, upto);
7949
8075
  }
7950
- yield* this.threadMessagesIn(this.paths.societyThreads(), since);
8076
+ yield* this.threadMessagesIn(this.paths.societyThreads(), since, upto);
7951
8077
  }
7952
8078
  const projects = project2 === void 0 ? await listDirs(this.paths.projects()) : [project2];
7953
8079
  for (const slug of projects) {
7954
8080
  for (const channel of await listDirs(this.paths.projectChannels(slug))) {
7955
- yield* await this.readMessagesIn(this.paths.projectChannel(slug, channel), since);
8081
+ yield* await this.readMessagesIn(this.paths.projectChannel(slug, channel), since, upto);
7956
8082
  }
7957
- yield* this.threadMessagesIn(this.paths.threads(slug), since);
8083
+ yield* this.threadMessagesIn(this.paths.threads(slug), since, upto);
7958
8084
  }
7959
8085
  }
7960
8086
  /** Every folder that holds messages: each channel's and each thread's, in the society and every project. */
@@ -7974,9 +8100,9 @@ ${charter.purpose}
7974
8100
  }
7975
8101
  }
7976
8102
  }
7977
- async *threadMessagesIn(threads, since) {
8103
+ async *threadMessagesIn(threads, since, upto) {
7978
8104
  for (const id of await listDirs(threads)) {
7979
- yield* await this.readMessagesIn(this.paths.threadMessages(threads, id), since);
8105
+ yield* await this.readMessagesIn(this.paths.threadMessages(threads, id), since, upto);
7980
8106
  }
7981
8107
  }
7982
8108
  };