@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
@@ -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,
@@ -631,9 +631,54 @@ var ChannelProposalSchema = z4.object({
631
631
  name: NameSchema,
632
632
  purpose: z4.string().min(1)
633
633
  });
634
+ var RoleChangesSchema = z4.strictObject({
635
+ maxReplicas: z4.number().int().min(1).optional(),
636
+ backlogThreshold: z4.number().positive().optional(),
637
+ wakeTriggers: z4.array(CharterTriggerSchema).optional(),
638
+ verbs: z4.array(VerbNameSchema).optional()
639
+ }).refine((changes) => Object.values(changes).some((value) => value !== void 0), {
640
+ message: "a reallocation changes at least one of maxReplicas, backlogThreshold, wakeTriggers, verbs"
641
+ });
634
642
  var ReallocationProposalSchema = z4.object({
643
+ role: NameSchema,
644
+ changes: RoleChangesSchema,
635
645
  description: z4.string().min(1)
636
646
  });
647
+ var LegacyReallocationSchema = z4.object({ description: z4.string().min(1) });
648
+ function applyRoleChanges(role, changes) {
649
+ return {
650
+ ...role,
651
+ ...changes.maxReplicas === void 0 ? {} : { maxReplicas: changes.maxReplicas },
652
+ ...changes.backlogThreshold === void 0 ? {} : { backlogThreshold: changes.backlogThreshold },
653
+ ...changes.wakeTriggers === void 0 ? {} : { wakeTriggers: [...changes.wakeTriggers] },
654
+ ...changes.verbs === void 0 ? {} : { verbs: [...changes.verbs] }
655
+ };
656
+ }
657
+ function listedOrNone(values) {
658
+ return values.length === 0 ? "none" : values.join(", ");
659
+ }
660
+ function roleChangeLines(role, changes) {
661
+ const lines = [];
662
+ if (changes.maxReplicas !== void 0 && changes.maxReplicas !== role.maxReplicas) {
663
+ lines.push(`maxReplicas ${role.maxReplicas} \u2192 ${changes.maxReplicas}`);
664
+ }
665
+ if (changes.backlogThreshold !== void 0 && changes.backlogThreshold !== role.backlogThreshold) {
666
+ lines.push(`backlogThreshold ${role.backlogThreshold} \u2192 ${changes.backlogThreshold}`);
667
+ }
668
+ const wakes = changes.wakeTriggers;
669
+ if (wakes !== void 0 && (wakes.length !== role.wakeTriggers.length || wakes.some((trigger) => !role.wakeTriggers.includes(trigger)))) {
670
+ lines.push(`wakeTriggers ${listedOrNone(role.wakeTriggers)} \u2192 ${listedOrNone(wakes)}`);
671
+ }
672
+ if (changes.verbs !== void 0) {
673
+ const next = changes.verbs;
674
+ const added = next.filter((verb) => !role.verbs.includes(verb)).map((verb) => `+${verb}`);
675
+ const removed = role.verbs.filter((verb) => !next.includes(verb)).map((verb) => `\u2212${verb}`);
676
+ if (added.length + removed.length > 0) {
677
+ lines.push(`verbs ${[...added, ...removed].join(" ")}`);
678
+ }
679
+ }
680
+ return lines;
681
+ }
637
682
  var RetirementProposalSchema = z4.object({
638
683
  agent: NameSchema,
639
684
  reason: z4.string().min(1)
@@ -665,6 +710,32 @@ var ROLE_KIND_APPROVERS = {
665
710
  reallocation: [USER_ROLE, "steward"],
666
711
  skill: [USER_ROLE, "steward"]
667
712
  };
713
+ var USER_DECIDED_ROLE_CHANGES = ["verbs", "maxReplicas"];
714
+ function proposalApprovers(kind, charter) {
715
+ if (kind === "reallocation") {
716
+ const parsed = ReallocationProposalSchema.safeParse(charter);
717
+ if (parsed.success && USER_DECIDED_ROLE_CHANGES.some((field) => parsed.data.changes[field] !== void 0)) {
718
+ return [USER_ROLE];
719
+ }
720
+ }
721
+ return ROLE_KIND_APPROVERS[kind];
722
+ }
723
+ function describeRoleChanges(changes) {
724
+ const parts = [];
725
+ if (changes.maxReplicas !== void 0) {
726
+ parts.push(`up to ${changes.maxReplicas} per project`);
727
+ }
728
+ if (changes.backlogThreshold !== void 0) {
729
+ parts.push(`backlog threshold ${changes.backlogThreshold}`);
730
+ }
731
+ if (changes.wakeTriggers !== void 0) {
732
+ parts.push(changes.wakeTriggers.length === 0 ? "no ambient wakes" : `wakes on ${changes.wakeTriggers.join(", ")}`);
733
+ }
734
+ if (changes.verbs !== void 0) {
735
+ parts.push(`${changes.verbs.length} verbs`);
736
+ }
737
+ return parts.join(", ");
738
+ }
668
739
  function describeCharter(kind, charter) {
669
740
  switch (kind) {
670
741
  case "member": {
@@ -696,9 +767,14 @@ function describeCharter(kind, charter) {
696
767
  }
697
768
  case "reallocation": {
698
769
  const parsed = ReallocationProposalSchema.safeParse(charter);
699
- if (!parsed.success)
770
+ if (parsed.success) {
771
+ const { role, changes, description } = parsed.data;
772
+ return `reallocation of role ${role} (${describeRoleChanges(changes)}): ${description}`;
773
+ }
774
+ const legacy = LegacyReallocationSchema.safeParse(charter);
775
+ if (!legacy.success)
700
776
  break;
701
- return `reallocation: ${parsed.data.description}`;
777
+ return `reallocation: ${legacy.data.description}`;
702
778
  }
703
779
  case "skill": {
704
780
  const parsed = SkillProposalSchema.safeParse(charter);
@@ -1007,7 +1083,7 @@ var VERB_DESCRIPTIONS = {
1007
1083
  create_task: "Create a task in a project with its plan: stages of {name, role or agent, gate}. Without stages it gets one stage, work, that anyone in the project may take. File it in a channel of the project, such as a release's, or general by default; its thread opens there with it, under the task's id, for everything said about the work.",
1008
1084
  claim_task: "Hold the task's current stage. Claims are leases renewed by every turn that touches the task.",
1009
1085
  release_task: "Let go of the stage you hold so someone else can take it.",
1010
- update_task: "Move a task back to an earlier stage, abandon it, set what it is blocked by, or link its pull request (pull_request: url, with the merge commit's merge_subject and merge_body), which a ghpr task needs before its last stage completes. A note is posted to the task's thread.",
1086
+ update_task: "Move a task back to an earlier stage, abandon it, set the tasks it is blocked by (blocked_by: its stage wakes nobody until they have all ended), or link its pull request (pull_request: url, with the merge commit's merge_subject and merge_body), which a ghpr task needs before its last stage completes. A note is posted to the task's thread.",
1011
1087
  get_task: "Read a task with its plan, its brief, and the messages of its thread.",
1012
1088
  subscribe: "Subscribe to a channel. Subscriptions feed your digest; they never wake you.",
1013
1089
  unsubscribe: "Unsubscribe from a channel.",
@@ -1869,7 +1945,7 @@ var RunnerConfigSchema = z11.object({
1869
1945
  });
1870
1946
 
1871
1947
  // packages/shared/dist/version.js
1872
- var STELLARIS_VERSION = "0.3.20";
1948
+ var STELLARIS_VERSION = "0.3.21";
1873
1949
 
1874
1950
  // packages/board-core/dist/errors.js
1875
1951
  var BoardError = class extends Error {
@@ -4606,18 +4682,16 @@ ${DASHBOARD_LINE}
4606
4682
  async readDigest(actor, input = {}) {
4607
4683
  const args = VerbInputs.read_inbox.parse(input);
4608
4684
  await this.authorize(actor, "read_inbox");
4609
- return this.mutex.run(async () => {
4610
- const stored = await this.readCursors(actor.name);
4611
- const from = args.since_cursor === void 0 ? stored : { ...NO_CURSORS, digest: args.since_cursor };
4612
- const conversation = actor.scope === void 0 ? void 0 : { scope: actor.scope, part: conversationPart(actor) ?? null };
4613
- const filed = (await this.digestSince(actor, from, conversation)).slice(0, args.limit);
4614
- if (args.advance) {
4615
- await this.advanceCursors(actor.name, filed);
4616
- }
4617
- const last = filed.at(-1)?.message.id;
4618
- const cursor = last ?? (conversation === void 0 ? from.digest : cursorOf(from, conversation.scope, conversation.part));
4619
- return { messages: filed.map((entry) => entry.message), cursor };
4620
- });
4685
+ const { cursors: stored, upto } = await this.digestSnapshot(actor.name);
4686
+ const from = args.since_cursor === void 0 ? stored : { ...NO_CURSORS, digest: args.since_cursor };
4687
+ const conversation = actor.scope === void 0 ? void 0 : { scope: actor.scope, part: conversationPart(actor) ?? null };
4688
+ const filed = (await this.digestSince(actor, from, upto, conversation)).slice(0, args.limit);
4689
+ if (args.advance) {
4690
+ await this.mutex.run(() => this.advanceCursors(actor.name, filed));
4691
+ }
4692
+ const last = filed.at(-1)?.message.id;
4693
+ const cursor = last ?? (conversation === void 0 ? from.digest : cursorOf(from, conversation.scope, conversation.part));
4694
+ return { messages: filed.map((entry) => entry.message), cursor };
4621
4695
  }
4622
4696
  /**
4623
4697
  * How many unread digest messages a member has in each conversation: a scope's home, for its
@@ -4629,28 +4703,39 @@ ${DASHBOARD_LINE}
4629
4703
  * whose signals that call for action wake their readers directly.
4630
4704
  */
4631
4705
  async unreadByConversation(actor) {
4632
- return this.mutex.run(async () => {
4633
- const counts = /* @__PURE__ */ new Map();
4634
- const unscoped = { name: actor.name, role: actor.role };
4635
- for (const { scope, part, message, news } of await this.digestSince(unscoped, await this.readCursors(actor.name))) {
4636
- if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
4637
- continue;
4638
- }
4639
- const posted = parseChannelRef(message.channel).project ?? SOCIETY_SCOPE;
4640
- if (posted !== scope && scope !== SOCIETY_SCOPE) {
4641
- continue;
4642
- }
4643
- const key = conversationKey(scope, part);
4644
- const entry = counts.get(key) ?? {
4645
- scope,
4646
- thread: threadOfPart(part) ?? null,
4647
- channel: channelOfPart(part) ?? null,
4648
- count: 0
4649
- };
4650
- counts.set(key, { ...entry, count: entry.count + 1 });
4706
+ const { cursors, upto } = await this.digestSnapshot(actor.name);
4707
+ const counts = /* @__PURE__ */ new Map();
4708
+ const unscoped = { name: actor.name, role: actor.role };
4709
+ for (const { scope, part, message, news } of await this.digestSince(unscoped, cursors, upto)) {
4710
+ if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
4711
+ continue;
4651
4712
  }
4652
- return [...counts.values()];
4653
- });
4713
+ const posted = parseChannelRef(message.channel).project ?? SOCIETY_SCOPE;
4714
+ if (posted !== scope && scope !== SOCIETY_SCOPE) {
4715
+ continue;
4716
+ }
4717
+ const key = conversationKey(scope, part);
4718
+ const entry = counts.get(key) ?? {
4719
+ scope,
4720
+ thread: threadOfPart(part) ?? null,
4721
+ channel: channelOfPart(part) ?? null,
4722
+ count: 0
4723
+ };
4724
+ counts.set(key, { ...entry, count: entry.count + 1 });
4725
+ }
4726
+ return [...counts.values()];
4727
+ }
4728
+ /**
4729
+ * A reader's cursors and the id below which every message is already on disk, taken under the
4730
+ * mutex, which a digest scan then leaves: messages carry ids from the same monotonic source and are
4731
+ * written under the mutex, so the scan, which counts nothing newer than the mark, cannot pass over
4732
+ * one written while it reads, and a post never waits for a scan.
4733
+ */
4734
+ digestSnapshot(agent) {
4735
+ return this.mutex.run(async () => ({
4736
+ cursors: await this.readCursors(agent),
4737
+ upto: this.newId()
4738
+ }));
4654
4739
  }
4655
4740
  async readCursors(agent) {
4656
4741
  const file = this.paths.agentCursors(agent);
@@ -4692,9 +4777,10 @@ ${DASHBOARD_LINE}
4692
4777
  * by the user wherever it is. With a conversation, only what is filed there: for a thread, every
4693
4778
  * message of the thread, since the conversation is about it; for a channel's, that channel's
4694
4779
  * top-level messages; for a scope's home, general's and those of a project the member is not in,
4695
- * since each thread and each other channel is a conversation of its own.
4780
+ * since each thread and each other channel is a conversation of its own. Nothing newer than
4781
+ * `upto` counts (see `digestSnapshot`).
4696
4782
  */
4697
- async digestSince(actor, cursors, conversation) {
4783
+ async digestSince(actor, cursors, upto, conversation) {
4698
4784
  const conversationThread = threadOfPart(conversation?.part);
4699
4785
  if (conversation !== void 0 && conversationThread !== void 0) {
4700
4786
  const { scope, part } = conversation;
@@ -4702,7 +4788,7 @@ ${DASHBOARD_LINE}
4702
4788
  if (found === null) {
4703
4789
  return [];
4704
4790
  }
4705
- const messages = await this.readMessagesIn(this.paths.threadMessages(found.threads, conversationThread), cursorOf(cursors, scope, part));
4791
+ const messages = await this.readMessagesIn(this.paths.threadMessages(found.threads, conversationThread), cursorOf(cursors, scope, part), upto);
4706
4792
  return messages.filter((message) => message.author !== actor.name).map((message) => ({ scope, part, message })).toSorted(byId);
4707
4793
  }
4708
4794
  const agent = await this.readAgent(actor.name);
@@ -4720,7 +4806,7 @@ ${DASHBOARD_LINE}
4720
4806
  ];
4721
4807
  const known = starts.filter((start) => start !== null);
4722
4808
  const since = known.length < starts.length ? null : known.toSorted((a, b) => a < b ? -1 : a > b ? 1 : 0)[0] ?? null;
4723
- for await (const message of this.iterateMessages(since)) {
4809
+ for await (const message of this.iterateMessages(since, void 0, upto)) {
4724
4810
  const filedIn = wakeScope(agent, parseChannelRef(message.channel).project);
4725
4811
  const thread = message.thread ?? null;
4726
4812
  const asked = message.mentions.includes(actor.name) || frontDesk && message.author === USER_NAME;
@@ -5844,6 +5930,9 @@ ${args.summary}`);
5844
5930
  }
5845
5931
  if (args.blocked_by !== void 0) {
5846
5932
  for (const id of args.blocked_by) {
5933
+ if (id === current.id) {
5934
+ throw new BoardError("VALIDATION", `task ${current.id} cannot be blocked by itself`);
5935
+ }
5847
5936
  await this.findTask(id);
5848
5937
  }
5849
5938
  next = { ...next, blockedBy: [...args.blocked_by] };
@@ -5880,6 +5969,7 @@ ${args.summary}`);
5880
5969
  from: current.status,
5881
5970
  to: task.status,
5882
5971
  note: args.note ?? null,
5972
+ ...args.blocked_by === void 0 ? {} : { blockedBy: task.blockedBy, previousBlockedBy: current.blockedBy },
5883
5973
  ...args.pull_request === void 0 ? {} : { pullRequest: task.pullRequest?.url ?? null }
5884
5974
  });
5885
5975
  }
@@ -7178,12 +7268,37 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7178
7268
  case "archive":
7179
7269
  await this.validateArchive(ArchiveProposalSchema.parse(charter).project);
7180
7270
  return;
7181
- case "reallocation":
7271
+ case "reallocation": {
7272
+ const parsed = ReallocationProposalSchema.safeParse(charter);
7273
+ if (parsed.success) {
7274
+ await this.reallocatedRole(parsed.data);
7275
+ } else {
7276
+ LegacyReallocationSchema.parse(charter);
7277
+ }
7182
7278
  return;
7279
+ }
7183
7280
  default:
7184
7281
  return;
7185
7282
  }
7186
7283
  }
7284
+ /**
7285
+ * The role as a reallocation would leave it, with what changes: refused for the user's charter,
7286
+ * a role that does not exist, a charter the changes would make invalid, and changes the role
7287
+ * has already.
7288
+ */
7289
+ async reallocatedRole(reallocation) {
7290
+ if (reallocation.role === USER_ROLE) {
7291
+ throw new BoardError("FORBIDDEN", "the user charter is not subject to proposals");
7292
+ }
7293
+ const current = await this.readRole(reallocation.role);
7294
+ const next = RoleCharterSchema.parse(applyRoleChanges(current, reallocation.changes));
7295
+ await this.validateRole(next);
7296
+ const changed = roleChangeLines(current, reallocation.changes);
7297
+ if (changed.length === 0) {
7298
+ throw new BoardError("VALIDATION", `role ${current.name} has these values already; the reallocation changes nothing`);
7299
+ }
7300
+ return { next, changed };
7301
+ }
7187
7302
  async executeProvision(by, proposal) {
7188
7303
  const meta = { proposalId: proposal.id };
7189
7304
  switch (proposal.kind) {
@@ -7220,8 +7335,15 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7220
7335
  const { members } = await this.archiveUnlocked(by, input.project, input.reason, meta);
7221
7336
  return { project: input.project, members };
7222
7337
  }
7223
- case "reallocation":
7224
- return void 0;
7338
+ case "reallocation": {
7339
+ const parsed = ReallocationProposalSchema.safeParse(proposal.charter);
7340
+ if (!parsed.success) {
7341
+ return void 0;
7342
+ }
7343
+ const { next, changed } = await this.reallocatedRole(parsed.data);
7344
+ await this.writeRoleUnlocked(by, next, meta);
7345
+ return { role: next.name, changed };
7346
+ }
7225
7347
  default:
7226
7348
  return void 0;
7227
7349
  }
@@ -7921,7 +8043,7 @@ ${charter.purpose}
7921
8043
  if (filed.proposedBy === actor.name) {
7922
8044
  throw new BoardError("FORBIDDEN", "a proposer never decides its own proposal");
7923
8045
  }
7924
- const approvers = ROLE_KIND_APPROVERS[filed.kind];
8046
+ const approvers = proposalApprovers(filed.kind, filed.charter);
7925
8047
  if (!approvers.includes(actor.role)) {
7926
8048
  throw new BoardError("FORBIDDEN", `a ${filed.kind} proposal requires one of: ${approvers.join(", ")}`);
7927
8049
  }
@@ -8004,7 +8126,7 @@ ${charter.purpose}
8004
8126
  }
8005
8127
  if (subject?.kind === "proposal") {
8006
8128
  const proposal = await this.readProposal(subject.id);
8007
- if (proposal.proposedBy === actor.name || proposal.decidedBy === actor.name || ROLE_KIND_APPROVERS[proposal.kind].includes(actor.role)) {
8129
+ if (proposal.proposedBy === actor.name || proposal.decidedBy === actor.name || proposalApprovers(proposal.kind, proposal.charter).includes(actor.role)) {
8008
8130
  return true;
8009
8131
  }
8010
8132
  return (await this.readRole(actor.role)).wakeTriggers.includes(OPS_WAKE_TRIGGER);
@@ -8061,11 +8183,12 @@ ${charter.purpose}
8061
8183
  });
8062
8184
  await this.endCronsUnlocked(by, `its thread closed: its ${found.thread.subject?.kind ?? "subject"} was ${ended}`, (cron) => cron.thread === subjectId);
8063
8185
  }
8064
- async readMessagesIn(dir, since) {
8186
+ /** The messages in a folder after `since` and, when given, up to `upto`, read by name first. */
8187
+ async readMessagesIn(dir, since, upto) {
8065
8188
  const messages = [];
8066
8189
  for (const file of await listFiles(dir)) {
8067
8190
  const id = file.slice(0, 26);
8068
- if (since !== null && id <= since) {
8191
+ if (since !== null && id <= since || upto !== void 0 && id > upto) {
8069
8192
  continue;
8070
8193
  }
8071
8194
  const doc = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
@@ -8073,20 +8196,23 @@ ${charter.purpose}
8073
8196
  }
8074
8197
  return messages;
8075
8198
  }
8076
- /** Every message newer than `since` across society channels, project channels, and threads. */
8077
- async *iterateMessages(since, project) {
8199
+ /**
8200
+ * Every message newer than `since`, and no newer than `upto` when given, across society
8201
+ * channels, project channels, and threads.
8202
+ */
8203
+ async *iterateMessages(since, project, upto) {
8078
8204
  if (project === void 0) {
8079
8205
  for (const channel of await listDirs(this.paths.societyChannels())) {
8080
- yield* await this.readMessagesIn(this.paths.societyChannel(channel), since);
8206
+ yield* await this.readMessagesIn(this.paths.societyChannel(channel), since, upto);
8081
8207
  }
8082
- yield* this.threadMessagesIn(this.paths.societyThreads(), since);
8208
+ yield* this.threadMessagesIn(this.paths.societyThreads(), since, upto);
8083
8209
  }
8084
8210
  const projects = project === void 0 ? await listDirs(this.paths.projects()) : [project];
8085
8211
  for (const slug of projects) {
8086
8212
  for (const channel of await listDirs(this.paths.projectChannels(slug))) {
8087
- yield* await this.readMessagesIn(this.paths.projectChannel(slug, channel), since);
8213
+ yield* await this.readMessagesIn(this.paths.projectChannel(slug, channel), since, upto);
8088
8214
  }
8089
- yield* this.threadMessagesIn(this.paths.threads(slug), since);
8215
+ yield* this.threadMessagesIn(this.paths.threads(slug), since, upto);
8090
8216
  }
8091
8217
  }
8092
8218
  /** Every folder that holds messages: each channel's and each thread's, in the society and every project. */
@@ -8106,9 +8232,9 @@ ${charter.purpose}
8106
8232
  }
8107
8233
  }
8108
8234
  }
8109
- async *threadMessagesIn(threads, since) {
8235
+ async *threadMessagesIn(threads, since, upto) {
8110
8236
  for (const id of await listDirs(threads)) {
8111
- yield* await this.readMessagesIn(this.paths.threadMessages(threads, id), since);
8237
+ yield* await this.readMessagesIn(this.paths.threadMessages(threads, id), since, upto);
8112
8238
  }
8113
8239
  }
8114
8240
  };
@@ -8492,7 +8618,7 @@ var Scheduler = class {
8492
8618
  return;
8493
8619
  }
8494
8620
  if (stringOf(payload["thread"]) !== taskId) {
8495
- await this.wakeHolder(taskId, event.actor, now);
8621
+ await this.wakeHolder(taskId, event.actor, event.actor, now);
8496
8622
  }
8497
8623
  await this.pruneStageWakes(taskId);
8498
8624
  return;
@@ -8518,6 +8644,7 @@ var Scheduler = class {
8518
8644
  if (creator !== null && creator !== event.actor) {
8519
8645
  await this.wakeCreator(taskId, project, creator, event.actor, now);
8520
8646
  }
8647
+ await this.wakeDependents(taskId, event.actor, now);
8521
8648
  const threaded = await this.board.readThread(taskId).then(() => true, () => false);
8522
8649
  if (!threaded) {
8523
8650
  this.sweep({ kind: "thread", id: taskId });
@@ -8537,12 +8664,17 @@ var Scheduler = class {
8537
8664
  }
8538
8665
  case "task.updated": {
8539
8666
  const taskId = stringOf(payload["taskId"]);
8540
- if (taskId !== null && payload["to"] === "abandoned") {
8667
+ if (taskId === null) {
8668
+ return;
8669
+ }
8670
+ if (payload["to"] === "abandoned") {
8541
8671
  delete this.state.releaseCounts[taskId];
8542
8672
  await this.queueClosing(taskId);
8673
+ await this.wakeDependents(taskId, event.actor, now);
8543
8674
  }
8544
- if (taskId !== null) {
8545
- await this.pruneStageWakes(taskId);
8675
+ await this.pruneStageWakes(taskId);
8676
+ if (Array.isArray(payload["blockedBy"]) && (await this.blockersInPlay(stringArray(payload["previousBlockedBy"]))).length > 0) {
8677
+ await this.wakeUnblocked(taskId, event.actor, now);
8546
8678
  }
8547
8679
  return;
8548
8680
  }
@@ -8713,9 +8845,10 @@ var Scheduler = class {
8713
8845
  }
8714
8846
  /**
8715
8847
  * Entering a stage wakes its named citizen, else its last holder when work returns to it, else
8716
- * the project's members of its role, else every member of the project who may hold it.
8848
+ * the project's members of its role, else every member of the project who may hold it. A stage
8849
+ * of a blocked task wakes nobody until its last blocker ends (`wakeUnblocked`).
8717
8850
  */
8718
- async wakeStage(taskId, from, now, only) {
8851
+ async wakeStage(taskId, from, now, options = {}) {
8719
8852
  let task;
8720
8853
  try {
8721
8854
  task = (await this.board.findTask(taskId)).task;
@@ -8726,6 +8859,10 @@ var Scheduler = class {
8726
8859
  if (stage === void 0 || task.status !== "open" || task.completing) {
8727
8860
  return;
8728
8861
  }
8862
+ if ((await this.blockersInPlay(task.blockedBy)).length > 0) {
8863
+ return;
8864
+ }
8865
+ const { only, state = "is waiting for you" } = options;
8729
8866
  const lastHolder = stage.holders.at(-1);
8730
8867
  const targets = stage.agent !== void 0 ? [stage.agent] : lastHolder !== void 0 ? [lastHolder] : stage.role !== void 0 ? (await this.board.membersWithRole(task.project, stage.role)).map((a) => a.name) : (await this.board.projectMembers(task.project)).map((a) => a.name);
8731
8868
  for (const name of targets) {
@@ -8736,7 +8873,7 @@ var Scheduler = class {
8736
8873
  if (agent === null || !mayHoldStage(agent, task)) {
8737
8874
  continue;
8738
8875
  }
8739
- await this.enqueueStage(agent, task, from, now, "is waiting for you");
8876
+ await this.enqueueStage(agent, task, from, now, state);
8740
8877
  }
8741
8878
  }
8742
8879
  /**
@@ -8745,7 +8882,7 @@ var Scheduler = class {
8745
8882
  */
8746
8883
  async wakeNewcomer(name, project, now) {
8747
8884
  for (const task of await this.board.openTasks(project).catch(() => [])) {
8748
- await this.wakeStage(task.id, name, now, name);
8885
+ await this.wakeStage(task.id, name, now, { only: name });
8749
8886
  }
8750
8887
  }
8751
8888
  /**
@@ -8753,7 +8890,7 @@ var Scheduler = class {
8753
8890
  * worked in that conversation, which nothing else would start before the next heartbeat. Turns
8754
8891
  * of one conversation run one at a time, so the turn this queues is the one that sees the claim.
8755
8892
  */
8756
- async wakeHolder(taskId, holder, now) {
8893
+ async wakeHolder(taskId, holder, from, now, state = "is yours; its work happens here") {
8757
8894
  let task;
8758
8895
  try {
8759
8896
  task = (await this.board.findTask(taskId)).task;
@@ -8764,12 +8901,66 @@ var Scheduler = class {
8764
8901
  if (agent === null || task.status !== "claimed" || task.claimedBy !== holder) {
8765
8902
  return;
8766
8903
  }
8767
- const key = await this.enqueueStage(agent, task, holder, now, "is yours; its work happens here");
8904
+ const key = await this.enqueueStage(agent, task, from, now, state);
8768
8905
  const item = key === null ? void 0 : this.pending.get(key);
8769
8906
  if (item?.stageWakes !== void 0) {
8770
8907
  item.holder = true;
8771
8908
  }
8772
8909
  }
8910
+ /**
8911
+ * The ids among these that name a task still open or claimed, a landing one included. A task
8912
+ * blocked by any of them waits for them rather than for a holder: its stage wakes nobody, by
8913
+ * coming up or on a heartbeat, until the last one ends.
8914
+ */
8915
+ async blockersInPlay(ids) {
8916
+ const blocking = [];
8917
+ for (const id of ids) {
8918
+ try {
8919
+ const { task } = await this.board.findTask(id);
8920
+ if (task.status === "open" || task.status === "claimed") {
8921
+ blocking.push(id);
8922
+ }
8923
+ } catch {
8924
+ }
8925
+ }
8926
+ return blocking;
8927
+ }
8928
+ /**
8929
+ * A task's last blocker has ended, or its blockers were changed to none in play: its stage wakes
8930
+ * as on coming up, or its holder in the task's conversation, since nothing woke either for it
8931
+ * while it was blocked.
8932
+ */
8933
+ async wakeUnblocked(taskId, from, now) {
8934
+ let task;
8935
+ try {
8936
+ task = (await this.board.findTask(taskId)).task;
8937
+ } catch {
8938
+ return;
8939
+ }
8940
+ if (task.completing || (await this.blockersInPlay(task.blockedBy)).length > 0) {
8941
+ return;
8942
+ }
8943
+ if (task.status === "open") {
8944
+ await this.wakeStage(task.id, from, now, {
8945
+ state: "is waiting for you, now that nothing blocks it"
8946
+ });
8947
+ } else if (task.status === "claimed" && task.claimedBy !== void 0) {
8948
+ await this.wakeHolder(task.id, task.claimedBy, from, now, "is yours, and nothing blocks it any more");
8949
+ }
8950
+ }
8951
+ /** A task that ended unblocks every task in play that names it, once nothing else blocks them. */
8952
+ async wakeDependents(taskId, from, now) {
8953
+ for (const project of await this.board.listProjects()) {
8954
+ if (project.archived !== void 0) {
8955
+ continue;
8956
+ }
8957
+ for (const task of await this.board.listTasks(project.slug)) {
8958
+ if (task.blockedBy.includes(taskId) && (task.status === "open" || task.status === "claimed")) {
8959
+ await this.wakeUnblocked(task.id, from, now);
8960
+ }
8961
+ }
8962
+ }
8963
+ }
8773
8964
  /** Queues a stage wake in the task's conversation and returns its key, or null for no wake. */
8774
8965
  async enqueueStage(agent, task, from, now, state) {
8775
8966
  if (agent.status !== "active" || agent.cli === null) {
@@ -8805,9 +8996,9 @@ var Scheduler = class {
8805
8996
  }
8806
8997
  }
8807
8998
  /**
8808
- * The first of these stage wakes still due: its task waits, unheld, on a stage the agent may
8809
- * hold, or, with `holder`, the agent holds it, having claimed it outside the task's conversation.
8810
- * A wake for a stage the agent took in the turn the wake waited on is dropped.
8999
+ * The first of these stage wakes still due: its task waits, unheld and unblocked, on a stage the
9000
+ * agent may hold, or, with `holder`, the agent holds it, having claimed it outside the task's
9001
+ * conversation. A wake for a stage the agent took in the turn the wake waited on is dropped.
8811
9002
  */
8812
9003
  async stillWaiting(name, wakes, holder) {
8813
9004
  const agent = await this.tryReadAgent(name);
@@ -8823,7 +9014,7 @@ var Scheduler = class {
8823
9014
  if (task.completing || currentStage(task) === void 0) {
8824
9015
  continue;
8825
9016
  }
8826
- if (task.status === "open" && mayHoldStage(agent, task)) {
9017
+ if (task.status === "open" && mayHoldStage(agent, task) && (await this.blockersInPlay(task.blockedBy)).length === 0) {
8827
9018
  return wake;
8828
9019
  }
8829
9020
  if (holder && task.status === "claimed" && task.claimedBy === agent.name) {
@@ -9046,9 +9237,9 @@ var Scheduler = class {
9046
9237
  /**
9047
9238
  * A heartbeat per member and scope on the cadence, which then looks at every conversation of the
9048
9239
  * scope: the unread messages of its home, of each other channel, and of each thread, a stage the
9049
- * member holds, and a stage
9050
- * waiting that it may take, each in its task's thread. Each conversation with something wakes on
9051
- * its own, so a heartbeat never brings one task's business into another's turn.
9240
+ * member holds, and a stage of an unblocked task waiting that it may take, each in its task's
9241
+ * thread. Each conversation with something wakes on its own, so a heartbeat never brings one
9242
+ * task's business into another's turn.
9052
9243
  */
9053
9244
  async checkHeartbeats(now) {
9054
9245
  for (const agent of await this.board.listAgents()) {
@@ -9100,7 +9291,7 @@ var Scheduler = class {
9100
9291
  }
9101
9292
  if (project !== SOCIETY_SCOPE) {
9102
9293
  for (const task of await this.board.openTasks(project)) {
9103
- if (mayHoldStage(agent, task)) {
9294
+ if (mayHoldStage(agent, task) && (await this.blockersInPlay(task.blockedBy)).length === 0) {
9104
9295
  reasonsIn({ thread: { id: task.id, task: true } }).waiting += 1;
9105
9296
  }
9106
9297
  }
@@ -9252,9 +9443,10 @@ var Scheduler = class {
9252
9443
  return latest;
9253
9444
  }
9254
9445
  /**
9255
- * A current stage without a holder past the threshold is logged as a signal once while it waits.
9256
- * It wakes nobody by itself: its assignees see the stage on their heartbeat, and the steward reads
9257
- * the signal at its next turn and decides whether to replan or to mention someone.
9446
+ * A current stage without a holder past the threshold is logged as a signal once while it waits,
9447
+ * naming the tasks that still block it. It wakes nobody by itself: its assignees see the stage on
9448
+ * their heartbeat, or a blocked one when its last blocker ends, and the steward reads the signal
9449
+ * at its next turn and decides whether to replan or to mention someone.
9258
9450
  */
9259
9451
  async checkWaitingStages(now) {
9260
9452
  const stillWaiting = /* @__PURE__ */ new Set();
@@ -9275,10 +9467,12 @@ var Scheduler = class {
9275
9467
  }
9276
9468
  this.state.waitingSeen[key] = iso(now);
9277
9469
  const assignee2 = stage.agent ?? stage.role ?? "anyone in the project";
9470
+ const blockers = await this.blockersInPlay(task.blockedBy);
9471
+ const waited = blockers.length === 0 ? "for a holder" : `blocked by ${blockers.map((id) => `task ${id}`).join(", ")}`;
9278
9472
  await this.board.publishSignal({
9279
9473
  kind: "waiting_stage",
9280
9474
  key: `waiting_stage:${key}`,
9281
- summary: `stage "${stage.name}" of task ${task.id} "${task.title}" in ${project.slug}, for ${assignee2}, has waited ${describeDuration(age)} for a holder`,
9475
+ summary: `stage "${stage.name}" of task ${task.id} "${task.title}" in ${project.slug}, for ${assignee2}, has waited ${describeDuration(age)} ${waited}`,
9282
9476
  value: age,
9283
9477
  threshold: this.timings.waitingStageMs,
9284
9478
  project: project.slug,
@@ -9741,6 +9935,7 @@ var TURN_CONTRACT = [
9741
9935
  "- To come back to something later, set a cron with create_cron: a note on what to do then, and a five-field cron expression or one time with at. It wakes you in this conversation unless you name another, at most every fifteen minutes, and every fire is a paid turn, so end a cron with remove_cron once it has done its job. Never wait by holding a stage, sleeping, or leaving a process running: a turn's board token ends with the turn, so nothing it leaves behind can act on the board. The society's crons are under society/crons/ in the board projection.",
9742
9936
  "- An @mention wakes the citizen named, and every wake costs a turn. Address someone with @ only when you need them to act; when you merely refer to citizens, write their names plainly. @user wakes nobody, since the user takes no turns; it is what puts a question before the user, so use it only when you need the user to answer or decide.",
9743
9937
  "- A thread is a conversation off a channel that reaches only its participants and whoever is mentioned in it. Every task has one, opened with it under the task's id on the channel of its project it was filed in, general unless it was filed in another: talk about a task there, with post_message and its thread_id, and the notes you give advance_task and update_task are posted there as you. Its participants are the task's creator, its stage holders and assignees, whoever may take the stage that waits, and anyone who posted; it closes when the task ends. Every proposal has one too, in governance, opened with its pitch and closed by its decision. Open other threads with open_thread on a channel with a title, for any other topic; close_thread posts your summary to its channel.",
9938
+ "- To show an image in a post, such as a screenshot or a chart, commit the file to a task's branch and link it by its path in the task's worktree, as `![what it shows](<path>)`: the board shows a file on a task's branch in place once it is committed, at the latest when your turn ends. An image at an https address shows too. Never put an image or any other file into a message as base64 or a data: URL: the board does not show it, and every reader of the conversation gets it in full in their prompt.",
9744
9939
  "- Route every lesson: about you, your craft, or the user, write it to memory/core.md in your home directory, and keep that file short by moving detail to memory/<topic>.md, your archive; a durable fact about a project's codebase or process goes through `write_knowledge` on that project; something the whole project should know now, post it to the project channel, and anything about one task, in the task's thread.",
9745
9940
  '- A procedure you have followed twice is a skill: write it to skills/<name>/SKILL.md in your home, frontmatter with `name` and a one-line `description` and then the steps, and your skills index lists it from the next turn. Files the steps use, such as scripts/ and references/, go in the same folder, named in SKILL.md by paths relative to it. Propose it with kind "skill" when the whole society would use it.',
9746
9941
  "- To take a published skill for yourself, install it in a temporary folder and copy it into your home: in a folder from `mktemp -d`, `npx -y skills add <source> --skill <name> -a universal --copy -y` writes `.agents/skills/<name>/` and a lock file there, and you copy that skill folder to skills/<name>/ in your home, named in lowercase letters, digits, and hyphens. Never install one inside a worktree, where it would be committed into the project, or with -g, which writes to the runner's own home, shared by every citizen on it and listed in no skills index.",
@@ -9754,21 +9949,22 @@ var PLANNING = [
9754
9949
  "- Name stages by what gets done. Plan the next few steps rather than everything, and reshape with plan_task as the work teaches you.",
9755
9950
  "- Assign a stage to a role when anyone in it could do it, to a citizen only when it must be them, and to nobody when anyone in the project could. A citizen named on a stage must be a member of the task's project; add them first. A stage wakes everyone it is open to when it comes up, so the narrower the assignee, the fewer turns it costs.",
9756
9951
  "- Reshape rather than force: another round is an inserted stage, and a wait on something outside the society is a stage named for what it waits on. When a check finds work unfinished, send the task back to an earlier stage with update_task.",
9952
+ "- A task that cannot start until other tasks finish names them with update_task blocked_by. While any of them is in play, its stage wakes nobody, neither when it comes up nor on a heartbeat; once the last has ended, done or abandoned, the stage wakes as if it had just come up, or wakes its holder. Nobody needs to clear the block or mention anyone when they end; change blocked_by only when the plan changes.",
9757
9953
  "- A gated stage is an independent check: nobody who held an earlier stage of the task may hold it, and only the user, the steward, and the concierge may add, remove, move, reassign, or ungate it. Ask them in the task's thread when a gate should change.",
9758
9954
  "- Commit your work on the task's branch before you advance, and say what you did in the advance note; it is the handover the next holder reads in the task's thread."
9759
9955
  ].join("\n");
9760
9956
  var GOVERNANCE = [
9761
- "- Anything the society lacks is a proposal: call `propose` with a kind and a charter. It opens the proposal's thread in governance with your rationale, where it is discussed and where the decision is posted. Approval provisions it on the spot, and either decision wakes the proposer. The user decides members, roles, retirements, and archives; the steward may also decide channels, reallocations, and skills. Nobody decides their own proposal.",
9957
+ "- Anything the society lacks is a proposal: call `propose` with a kind and a charter. It opens the proposal's thread in governance with your rationale, where it is discussed and where the decision is posted. Approval provisions it on the spot, and either decision wakes the proposer. The user decides members, roles, retirements, archives, and reallocations that change a role's verbs or replicas; the steward may also decide channels, skills, and other reallocations. Nobody decides their own proposal.",
9762
9958
  "- Charter shapes, as JSON objects:",
9763
9959
  ' - member: {"name", "role", "cli": "claude" or "codex", "memberships": [project slugs], "model"?, "homeRunner"?, "subscriptions"?, "seedInstructions"?}',
9764
9960
  ' - role: {"name", "purpose", "verbs": [board verbs], "wakeTriggers"?: any of "user_post", "ops_event", "heartbeat" (heartbeat when omitted), "maxReplicas"?, "backlogThreshold"?, "societyScope"?: true for a society role, whose members are never in a project and keep watch over the whole society}. Mentions, stages that become yours, and finished tasks you created wake every role; a heartbeat wakes you when something is unread, held, or waiting for you.',
9765
9961
  ' - channel: {"project": slug or null, "name", "purpose"}',
9766
9962
  ' - retirement: {"agent", "reason"}',
9767
9963
  ' - archive: {"project", "reason"}: approval archives the project: its members leave, its open threads close, and nothing more is posted, filed, or joined there, while its files and history stay; refused while a task there is in play',
9768
- ' - reallocation: {"description"}',
9769
- ' - skill: {"name", "summary"}: the folder skills/<name>/ in your home, SKILL.md and every file beside it, which the board takes as it stands when this turn ends into society/proposals/<id>/, where whoever decides reads it; a proposal whose folder has no SKILL.md then is rejected by the board. Approval publishes it under society/skills/<name>/, where every citizen\'s skills index lists it until the steward or the user removes it with `remove_skill`; ask the steward to remove one that is wrong or superseded. A proposal from before folders carries "body", its SKILL.md text alone',
9964
+ ` - 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`,
9965
+ ' - skill: {"name", "summary"}: the folder skills/<name>/ in your home, SKILL.md and every file beside it, which the board takes as it stands when this turn ends into society/proposals/<id>/, where whoever decides reads it; a proposal whose folder has no SKILL.md then is rejected by the board. Approval publishes it under society/skills/<name>/, where every citizen\'s skills index lists it until the steward or the user removes it with `remove_skill`; ask the steward to remove one that is wrong or superseded. Once yours is approved, delete skills/<name>/ from your home and report memoryUpdated: the board leaves your copy there, and your index would list it beside the published one and follow it as it drifts from what was reviewed. To revise a published skill, copy its folder home, edit it, and propose it again. A proposal from before folders carries "body", its SKILL.md text alone',
9770
9966
  "- Projects and membership: a project is a lasting area of work, not one request; `create_project` opens one with its general channel (front desk and user), and `configure_project` sets its completion effect, its display name, or its default branch, never its slug (user, steward, concierge). A workstream within a project or the society, such as a release, gets a channel of its own: `create_channel` opens it, which its opener follows, and announces it in the place's general, where members subscribe when it concerns them, `create_task` with `channel` files the workstream's tasks there, and `archive_channel` ends it once none of its tasks is in play, leaving its history readable (user, steward, concierge; others propose a channel). A project whose work is finished or has moved to another project is archived by proposal, once no task there is in play. `join_project` and `leave_project` move yourself, or another citizen when you are the concierge, the steward, or the user; joining gives the pair a worktree and an onboarding turn. A society role, one that keeps watch over the whole society as the concierge and the steward do, is never in a project: it moves others, never itself.",
9771
- "- Prefer scaling an existing role over inventing one; a new role is justified by work a project needs that no existing role covers. A role that needs a tool the board lacks is an engineering task, not a hiring request.",
9967
+ "- Prefer scaling an existing role over inventing one, with a reallocation of its replicas, backlog threshold, wakes, or verbs; a new role is justified by work a project needs that no existing role covers. A role that needs a tool the board lacks is an engineering task, not a hiring request.",
9772
9968
  "- Operations signals are counters and timers the board logs, listed in the prompt of a role that reads them: stages waiting for a holder, backlog per role, stages waiting on a role nobody fills, churn, stale threads, idle members, missing capabilities, replicas added, spend. They are not posts in any channel; interpreting them is your judgment."
9773
9969
  ].join("\n");
9774
9970
  function renderOnboardingPreamble(context) {
@@ -9951,7 +10147,7 @@ function buildTurnPrompt(input) {
9951
10147
  lines.push("A task you created is done. Tell whoever asked for it, if anyone did.");
9952
10148
  }
9953
10149
  if (dispatch.trigger.kind === "proposal_decided") {
9954
- lines.push("A proposal you made was decided; the decision is the last post in the proposal's thread. A decision wakes you in the conversation you filed the proposal from, or at home once that has ended. Carry on with what an approval enables or a rejection asks, and stay silent if nothing follows.");
10150
+ lines.push("A proposal you made was decided; the decision is the last post in the proposal's thread. A decision wakes you in the conversation you filed the proposal from, or at home once that has ended. Carry on with what an approval enables or a rejection asks, and stay silent if nothing follows. An approved skill is published under society/skills/<name>/: delete your own skills/<name>/ from your home and report memoryUpdated, so your index lists the published copy alone.");
9955
10151
  }
9956
10152
  if (dispatch.trigger.kind === "cron") {
9957
10153
  lines.push("A cron came due: what it asks is under Crons below.");
@@ -10737,7 +10933,7 @@ var TurnHost = class {
10737
10933
  if (await this.board.hasMentioned(actor.name, USER_NAME, since)) {
10738
10934
  return;
10739
10935
  }
10740
- const proposed = (await this.board.listProposals()).some((proposal) => proposal.proposedBy === actor.name && proposal.createdAt >= since && ROLE_KIND_APPROVERS[proposal.kind].includes(USER_ROLE));
10936
+ const proposed = (await this.board.listProposals()).some((proposal) => proposal.proposedBy === actor.name && proposal.createdAt >= since && proposalApprovers(proposal.kind, proposal.charter).includes(USER_ROLE));
10741
10937
  if (proposed) {
10742
10938
  return;
10743
10939
  }