@kubitnodes/stellaris 0.3.24 → 0.3.25

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 +303 -579
  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
package/dist/cli/index.js CHANGED
@@ -715,7 +715,6 @@ var ReallocationProposalSchema = z5.object({
715
715
  changes: RoleChangesSchema,
716
716
  description: z5.string().min(1)
717
717
  });
718
- var LegacyReallocationSchema = z5.object({ description: z5.string().min(1) });
719
718
  function applyRoleChanges(role2, changes) {
720
719
  return {
721
720
  ...role2,
@@ -760,8 +759,7 @@ var ArchiveProposalSchema = z5.object({
760
759
  });
761
760
  var SkillProposalSchema = z5.object({
762
761
  name: NameSchema,
763
- summary: z5.string().min(1).max(200),
764
- body: z5.string().min(1).optional()
762
+ summary: z5.string().min(1).max(200)
765
763
  });
766
764
  var ProposalCharterSchemas = {
767
765
  role: RoleCharterSchema,
@@ -838,14 +836,10 @@ function describeCharter(kind, charter) {
838
836
  }
839
837
  case "reallocation": {
840
838
  const parsed = ReallocationProposalSchema.safeParse(charter);
841
- if (parsed.success) {
842
- const { role: role2, changes, description } = parsed.data;
843
- return `reallocation of role ${role2} (${describeRoleChanges(changes)}): ${description}`;
844
- }
845
- const legacy = LegacyReallocationSchema.safeParse(charter);
846
- if (!legacy.success)
839
+ if (!parsed.success)
847
840
  break;
848
- return `reallocation: ${legacy.data.description}`;
841
+ const { role: role2, changes, description } = parsed.data;
842
+ return `reallocation of role ${role2} (${describeRoleChanges(changes)}): ${description}`;
849
843
  }
850
844
  case "skill": {
851
845
  const parsed = SkillProposalSchema.safeParse(charter);
@@ -1050,12 +1044,10 @@ var VerbInputs = {
1050
1044
  channel: ChannelRefSchema.optional(),
1051
1045
  limit: z7.number().int().positive().max(100).default(20)
1052
1046
  }),
1053
- /** On a task or a proposal, whose id the thread takes, or on a channel with a title. */
1047
+ /** On a channel with a title; a task's and a proposal's open with them. */
1054
1048
  open_thread: z7.object({
1055
- task_id: UlidSchema.optional(),
1056
- proposal_id: UlidSchema.optional(),
1057
- channel: ChannelRefSchema.optional(),
1058
- title: z7.string().min(1).max(200).optional()
1049
+ channel: ChannelRefSchema,
1050
+ title: z7.string().min(1).max(200)
1059
1051
  }),
1060
1052
  close_thread: z7.object({ thread_id: UlidSchema, summary: z7.string().min(1) }),
1061
1053
  create_task: z7.object({
@@ -1519,6 +1511,12 @@ var TurnOutcomeSchema = z9.object({
1519
1511
  * runner from before never sends it.
1520
1512
  */
1521
1513
  resumeFailed: ResumeFailureSchema.optional(),
1514
+ /**
1515
+ * Set when the model service turned the turn away for now, as when it was overloaded or failed
1516
+ * on its side, which the scheduler answers by sending the same wake again within minutes. A
1517
+ * runner from before never sends it.
1518
+ */
1519
+ transient: z9.boolean().optional(),
1522
1520
  /** The model the CLI reported, when it reported one. */
1523
1521
  model: z9.string().nullable(),
1524
1522
  work: TurnWorkSchema.nullable(),
@@ -1819,6 +1817,8 @@ var TurnRecordSchema = z10.object({
1819
1817
  stoppedBy: NameSchema.optional(),
1820
1818
  /** Why the CLI could not resume the turn's session, for a turn that failed so. */
1821
1819
  resumeFailed: ResumeFailureSchema.optional(),
1820
+ /** Whether the model service turned the turn away for now: overloaded, or failing on its side. */
1821
+ transient: z10.boolean().optional(),
1822
1822
  /** How many turns in a row, this one included, could not resume the session for a `retry`. */
1823
1823
  resumeRetries: z10.number().int().positive().optional(),
1824
1824
  /** Whether the server dropped the conversation's session after this turn, which the next starts anew. */
@@ -1893,9 +1893,19 @@ var OPS_SIGNAL_KINDS = [
1893
1893
  "runner",
1894
1894
  "home_conflict",
1895
1895
  "stuck_workspace",
1896
- "cron_failing"
1896
+ "cron_failing",
1897
+ "turn_refused"
1897
1898
  ];
1898
1899
  var OpsSignalKindSchema = z11.enum(OPS_SIGNAL_KINDS);
1900
+ var WAKING_SIGNAL_KINDS = [
1901
+ "backlog",
1902
+ "role_gap",
1903
+ "churn",
1904
+ "stale_thread",
1905
+ "idle_member",
1906
+ "blocked_capability",
1907
+ "scaled"
1908
+ ];
1899
1909
  var OpsSignalSchema = z11.object({
1900
1910
  kind: OpsSignalKindSchema,
1901
1911
  /** Identifies the condition, for example `backlog:demo:engineer`, so a persisting one is not logged again every pass. */
@@ -1908,8 +1918,15 @@ var OpsSignalSchema = z11.object({
1908
1918
  project: NameSchema.optional(),
1909
1919
  agent: NameSchema.optional(),
1910
1920
  role: NameSchema.optional(),
1911
- taskId: UlidSchema.optional()
1921
+ taskId: UlidSchema.optional(),
1922
+ /**
1923
+ * Whether this log of the condition wakes the roles charted for `ops_event`, as its publisher
1924
+ * decided: the operations pass wakes them when a condition is new, and again only at the steps
1925
+ * it names, not at every repeat. Absent, the kind decides.
1926
+ */
1927
+ wakes: z11.boolean().optional()
1912
1928
  });
1929
+ var WAKING = new Set(WAKING_SIGNAL_KINDS);
1913
1930
 
1914
1931
  // packages/shared/dist/metrics.js
1915
1932
  import { z as z12 } from "zod";
@@ -2067,7 +2084,7 @@ var RunnerConfigSchema = z13.object({
2067
2084
  });
2068
2085
 
2069
2086
  // packages/shared/dist/version.js
2070
- var STELLARIS_VERSION = "0.3.24";
2087
+ var STELLARIS_VERSION = "0.3.25";
2071
2088
 
2072
2089
  // packages/shared/dist/mentions.js
2073
2090
  import { parse, postprocess, preprocess } from "micromark";
@@ -3395,7 +3412,7 @@ var BoardPaths = class {
3395
3412
 
3396
3413
  // packages/board-core/dist/skill-folder.js
3397
3414
  import { constants } from "node:fs";
3398
- import { lstat, open as open2, readdir as readdir3, realpath } from "node:fs/promises";
3415
+ import { lstat, open as open2, readdir as readdir3, readlink, realpath } from "node:fs/promises";
3399
3416
  import path6 from "node:path";
3400
3417
  var SKILL_TEXT_LIMIT_CHARS = 1e5;
3401
3418
  var SKILL_TEXT_LIMIT_BYTES = SKILL_TEXT_LIMIT_CHARS * 4;
@@ -3410,6 +3427,13 @@ function inside(root, file) {
3410
3427
  const relative = path6.relative(root, file);
3411
3428
  return relative !== ".." && !relative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(relative);
3412
3429
  }
3430
+ async function handleInside(root, handle) {
3431
+ if (process.platform !== "linux") {
3432
+ return true;
3433
+ }
3434
+ const file = await readlink(`/proc/self/fd/${handle.fd}`).catch(() => null);
3435
+ return file !== null && inside(root, file);
3436
+ }
3413
3437
  async function walk(root, prefix, found) {
3414
3438
  const entries = await readdir3(path6.join(root, ...prefix === "" ? [] : prefix.split("/")), {
3415
3439
  withFileTypes: true
@@ -3444,6 +3468,9 @@ async function readEntry(root, relative) {
3444
3468
  return null;
3445
3469
  }
3446
3470
  try {
3471
+ if (!await handleInside(root, handle)) {
3472
+ return null;
3473
+ }
3447
3474
  const stats = await handle.stat();
3448
3475
  if (!stats.isFile()) {
3449
3476
  return null;
@@ -3514,17 +3541,16 @@ var SignInsSchema = z16.object({
3514
3541
  signIns: z16.array(z16.object({
3515
3542
  tokenHash: z16.string().min(1),
3516
3543
  login: z16.string().min(1),
3517
- /** The account's picture at the identity provider; sign-ins from before it have none. */
3544
+ /** The account's picture, when GitHub gave one on its own avatar host. */
3518
3545
  avatarUrl: z16.url().optional(),
3519
3546
  createdAt: IsoDateTimeSchema,
3520
3547
  expiresAt: IsoDateTimeSchema
3521
3548
  })).default([])
3522
3549
  });
3523
- var TurnStartPayloadSchema = z16.object({ project: NameSchema, turnId: UlidSchema.optional() });
3550
+ var TurnStartPayloadSchema = z16.object({ turnId: UlidSchema });
3524
3551
  var CursorsSchema = z16.object({
3525
3552
  digest: z16.string().nullable(),
3526
3553
  scopes: z16.record(z16.string(), z16.string()).default({}),
3527
- threadsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3528
3554
  channelsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3529
3555
  placesFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
3530
3556
  /** How far a work role has read news, which goes to whichever of its general conversations reads first. */
@@ -3533,7 +3559,6 @@ var CursorsSchema = z16.object({
3533
3559
  var NO_CURSORS = {
3534
3560
  digest: null,
3535
3561
  scopes: {},
3536
- threadsFrom: {},
3537
3562
  channelsFrom: {},
3538
3563
  placesFrom: {}
3539
3564
  };
@@ -3557,10 +3582,6 @@ function placeStart(cursors, scope) {
3557
3582
  const from = cursors.placesFrom[scope];
3558
3583
  return from === void 0 ? cursors.digest : from;
3559
3584
  }
3560
- function threadStart(cursors, scope) {
3561
- const from = cursors.threadsFrom[scope];
3562
- return from === void 0 ? cursors.digest : from;
3563
- }
3564
3585
  function newsStart(cursors) {
3565
3586
  if (cursors.news !== void 0) {
3566
3587
  return cursors.news;
@@ -3572,7 +3593,7 @@ function cursorOf(cursors, scope, part = null) {
3572
3593
  if (part === null) {
3573
3594
  return cursors.scopes[scope] ?? cursors.digest;
3574
3595
  }
3575
- return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : part.startsWith("~") ? placeStart(cursors, scope) : threadStart(cursors, scope));
3596
+ return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : part.startsWith("~") ? placeStart(cursors, scope) : cursors.digest);
3576
3597
  }
3577
3598
  function byId(a, b) {
3578
3599
  return a.message.id < b.message.id ? -1 : a.message.id > b.message.id ? 1 : 0;
@@ -3593,7 +3614,7 @@ function skillFolderOf(proposal2) {
3593
3614
  return null;
3594
3615
  }
3595
3616
  const parsed = SkillProposalSchema.safeParse(proposal2.charter);
3596
- return parsed.success && parsed.data.body === void 0 ? parsed.data.name : null;
3617
+ return parsed.success ? parsed.data.name : null;
3597
3618
  }
3598
3619
  function proposedFrom(origin, where) {
3599
3620
  return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel && origin.place === where.place;
@@ -3608,28 +3629,6 @@ var FRONT_DESK_TRIGGER = "user_post";
3608
3629
  var OPS_CHANNELS = ["governance"];
3609
3630
  var FRONT_DESK_CHANNELS = [ASK_CHANNEL];
3610
3631
  var AlignmentsSchema = z16.object({ applied: z16.array(z16.string()).default([]) });
3611
- var FOLLOW_DECISIONS = "ops-readers-follow-decisions";
3612
- var THREAD_RECORDS = "threads-as-records";
3613
- var DIGEST_CURSORS = "digest-replaces-inbox";
3614
- var TASK_RETURNS = "task-returns-recorded";
3615
- var OPS_CHANNEL_RETIRED = "ops-channel-retired";
3616
- var DECISIONS_CHANNEL_RETIRED = "decisions-channel-retired";
3617
- var THREAD_CONVERSATIONS = "thread-conversations";
3618
- var ASKS_CHANNEL_OPENED = "asks-channel-opened";
3619
- var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
3620
- var CHANNEL_CONVERSATIONS = "channel-conversations";
3621
- var PLACE_CONVERSATIONS = "place-conversations";
3622
- var DASHBOARD_UPDATES = "dashboard-updates-granted";
3623
- var CRON_VERBS = "cron-verbs-granted";
3624
- var KNOWLEDGE_NOTICES = "knowledge-notices";
3625
- var IMAGE_UPLOADS = "image-uploads-granted";
3626
- var KnowledgeEventSchema = z16.object({
3627
- topic: NameSchema,
3628
- project: NameSchema.nullable(),
3629
- replaced: z16.boolean().optional()
3630
- });
3631
- var CRON_VERB_NAMES = ["create_cron", "update_cron", "remove_cron"];
3632
- var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
3633
3632
  var DASHBOARD_LINE = "The project's members keep this page current with update_dashboard.";
3634
3633
  var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
3635
3634
  var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
@@ -3649,9 +3648,9 @@ function sentence(summary) {
3649
3648
  return summary.trim().replace(/\.+$/, "");
3650
3649
  }
3651
3650
  function skillSummary(data, body) {
3652
- const fromFrontmatter = data["description"] ?? data["summary"];
3653
- if (typeof fromFrontmatter === "string" && fromFrontmatter.trim().length > 0) {
3654
- return fromFrontmatter.trim();
3651
+ const description = data["description"];
3652
+ if (typeof description === "string" && description.trim().length > 0) {
3653
+ return description.trim();
3655
3654
  }
3656
3655
  return body.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#")) ?? "";
3657
3656
  }
@@ -3804,390 +3803,22 @@ var Board = class _Board {
3804
3803
  return board;
3805
3804
  }
3806
3805
  /**
3807
- * Applies each one-time alignment an existing society has not had yet. Once applied it is not
3808
- * repeated, so a member that later chooses otherwise keeps its choice.
3806
+ * Applies each one-time alignment an existing society has not had yet, and remembers it, so a
3807
+ * member that later chooses otherwise keeps its choice. A new alignment is a name and a method in
3808
+ * this list; once every board has applied it, both are deleted. None is pending.
3809
3809
  */
3810
3810
  async applyAlignments() {
3811
+ const alignments = [];
3811
3812
  const file = path7.join(this.paths.state(), "alignments.json");
3812
3813
  const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
3813
- const pending = [
3814
- FOLLOW_DECISIONS,
3815
- THREAD_RECORDS,
3816
- DIGEST_CURSORS,
3817
- TASK_RETURNS,
3818
- OPS_CHANNEL_RETIRED,
3819
- DECISIONS_CHANNEL_RETIRED,
3820
- THREAD_CONVERSATIONS,
3821
- ASKS_CHANNEL_OPENED,
3822
- KNOWLEDGE_REMOVAL,
3823
- CHANNEL_CONVERSATIONS,
3824
- DASHBOARD_UPDATES,
3825
- CRON_VERBS,
3826
- KNOWLEDGE_NOTICES,
3827
- IMAGE_UPLOADS,
3828
- PLACE_CONVERSATIONS
3829
- ].filter((name) => !applied.includes(name));
3814
+ const pending = alignments.filter(({ name }) => !applied.includes(name));
3830
3815
  if (pending.length === 0) {
3831
3816
  return;
3832
3817
  }
3833
- if (pending.includes(FOLLOW_DECISIONS)) {
3834
- await this.followDecisions();
3835
- }
3836
- if (pending.includes(THREAD_RECORDS)) {
3837
- await this.recordTaskThreads();
3838
- }
3839
- if (pending.includes(DIGEST_CURSORS)) {
3840
- await this.renameInboxCursors();
3841
- }
3842
- if (pending.includes(TASK_RETURNS)) {
3843
- await this.recordTaskReturns();
3844
- }
3845
- if (pending.includes(OPS_CHANNEL_RETIRED)) {
3846
- await this.retireSocietyChannel("ops");
3847
- }
3848
- if (pending.includes(DECISIONS_CHANNEL_RETIRED)) {
3849
- await this.retireSocietyChannel("decisions");
3850
- }
3851
- if (pending.includes(THREAD_CONVERSATIONS)) {
3852
- await this.startThreadCursors();
3853
- }
3854
- if (pending.includes(ASKS_CHANNEL_OPENED)) {
3855
- await this.openAsksChannel();
3856
- }
3857
- if (pending.includes(KNOWLEDGE_REMOVAL)) {
3858
- await this.grantBeside("write_knowledge", "remove_knowledge");
3859
- }
3860
- if (pending.includes(CHANNEL_CONVERSATIONS)) {
3861
- await this.startChannelCursors();
3862
- }
3863
- if (pending.includes(DASHBOARD_UPDATES)) {
3864
- await this.grantBeside("write_knowledge", "update_dashboard");
3865
- await this.rewordDashboards();
3866
- }
3867
- if (pending.includes(CRON_VERBS)) {
3868
- await this.grantToEveryRole(CRON_VERB_NAMES);
3869
- }
3870
- if (pending.includes(KNOWLEDGE_NOTICES)) {
3871
- await this.noticeKnowledgeNotes();
3872
- }
3873
- if (pending.includes(IMAGE_UPLOADS)) {
3874
- await this.grantBeside("post_message", "upload_image");
3875
- }
3876
- if (pending.includes(PLACE_CONVERSATIONS)) {
3877
- await this.startPlaceCursors();
3878
- }
3879
- await writeJson(file, { applied: [...applied, ...pending] });
3880
- }
3881
- /**
3882
- * Gives each knowledge note from before notes carried their notice the notice it would carry
3883
- * now. The event log, not the note's words, says which post is one: a knowledge verb appends
3884
- * its event and then its note under the mutex, so a note is the post right after such an event,
3885
- * by the same actor, at the top of the general channel of the topic's place. A post written to
3886
- * look like a note follows no such event and is left as it is.
3887
- */
3888
- async noticeKnowledgeNotes() {
3889
- let before;
3890
- for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
3891
- const verb = before;
3892
- before = event;
3893
- if (event.type !== "message.posted" || verb === void 0 || verb.actor !== event.actor || verb.type !== "knowledge.written" && verb.type !== "knowledge.removed") {
3894
- continue;
3895
- }
3896
- const knowledge2 = KnowledgeEventSchema.safeParse(verb.payload);
3897
- const id = UlidSchema.safeParse(event.payload["id"]);
3898
- if (!knowledge2.success || !id.success || (event.payload["thread"] ?? null) !== null || event.payload["channel"] !== channelRef(knowledge2.data.project, "general")) {
3899
- continue;
3900
- }
3901
- const { topic, project: project2, replaced } = knowledge2.data;
3902
- const file = this.paths.messageFile(this.paths.channelDir(channelRef(project2, "general")), id.data, event.actor);
3903
- if (!await exists(file)) {
3904
- continue;
3905
- }
3906
- const { data, body, error } = await readLooseMarkdown(file);
3907
- if (error !== null || data["notice"] !== void 0) {
3908
- continue;
3909
- }
3910
- const notice = {
3911
- kind: "knowledge",
3912
- action: verb.type === "knowledge.removed" ? "removed" : replaced === true ? "updated" : "written",
3913
- topic,
3914
- project: project2
3915
- };
3916
- await writeMarkdown(file, { ...data, notice }, body);
3917
- }
3918
- }
3919
- /**
3920
- * Grants `verb` once to every charter granting `beside`, the work roles' charters in practice;
3921
- * seed roles have it from their seed.
3922
- */
3923
- async grantBeside(beside, verb) {
3924
- for (const charter of await this.listRoles()) {
3925
- if (charter.verbs.includes(beside) && !charter.verbs.includes(verb)) {
3926
- await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
3927
- }
3928
- }
3929
- }
3930
- /** Grants verbs once to every charter but the user's, which holds every verb already. */
3931
- async grantToEveryRole(verbs) {
3932
- for (const charter of await this.listRoles()) {
3933
- const missing = verbs.filter((verb) => !charter.verbs.includes(verb));
3934
- if (charter.name !== USER_ROLE && missing.length > 0) {
3935
- await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, ...missing] }, { verbsAdded: missing });
3936
- }
3818
+ for (const { apply } of pending) {
3819
+ await apply();
3937
3820
  }
3938
- }
3939
- /** A dashboard still saying agents may edit its file says how it changes now. */
3940
- async rewordDashboards() {
3941
- for (const slug of await listDirs(this.paths.projects())) {
3942
- const file = this.paths.dashboard(slug);
3943
- if (!await exists(file)) {
3944
- continue;
3945
- }
3946
- const { data, body } = await readLooseMarkdown(file);
3947
- if (body.includes(OLD_DASHBOARD_LINE)) {
3948
- await writeMarkdown(file, data, body.replace(OLD_DASHBOARD_LINE, DASHBOARD_LINE));
3949
- }
3950
- }
3951
- }
3952
- /**
3953
- * A task sent back before tasks recorded it kept no trace outside the event log. A task still
3954
- * short of the stage its latest `task.moved` came from is in that rework, and records it.
3955
- */
3956
- async recordTaskReturns() {
3957
- const moves = /* @__PURE__ */ new Map();
3958
- for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
3959
- const taskId = event.payload["taskId"];
3960
- if (event.type === "task.moved" && typeof taskId === "string") {
3961
- moves.set(taskId, event);
3962
- }
3963
- }
3964
- for (const slug of await listDirs(this.paths.projects())) {
3965
- for (const task2 of await this.listTasks(slug)) {
3966
- const move = moves.get(task2.id);
3967
- const from = move?.payload["from"];
3968
- if (move === void 0 || typeof from !== "string" || task2.returned !== void 0 || task2.completing || task2.status !== "open" && task2.status !== "claimed") {
3969
- continue;
3970
- }
3971
- const back = stageIndex(task2, from);
3972
- if (back > stageIndex(task2, task2.stage)) {
3973
- await this.writeTask(slug, {
3974
- ...task2,
3975
- returned: { from, by: move.actor, at: move.ts }
3976
- });
3977
- }
3978
- }
3979
- }
3980
- }
3981
- /**
3982
- * A society channel the board no longer uses leaves the society's list and every member's
3983
- * subscriptions; its old posts stay on disk. `ops` carried operations signals, which are only
3984
- * logged now; `decisions` carried proposals' outcomes, which are posts in their threads now, and
3985
- * requests to the user, which are mentions of the user wherever the question belongs.
3986
- */
3987
- async retireSocietyChannel(name) {
3988
- const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
3989
- if (doc.data.channels.includes(name)) {
3990
- await writeMarkdown(this.paths.societyFile(), { ...doc.data, channels: doc.data.channels.filter((channel) => channel !== name) }, doc.body);
3991
- }
3992
- for (const agent2 of await this.listAgents()) {
3993
- if (agent2.subscriptions.includes(name)) {
3994
- await this.updateAgent(agent2.name, (a) => ({
3995
- ...a,
3996
- subscriptions: a.subscriptions.filter((ref) => ref !== name)
3997
- }));
3998
- await this.refreshMember(agent2.name);
3999
- }
4000
- }
4001
- }
4002
- /**
4003
- * Asks once opened their threads in general, where closing one posted its summary to every
4004
- * citizen and woke the steward's heartbeat for it. The society gains the asks channel, and the
4005
- * front desk follows it.
4006
- */
4007
- async openAsksChannel() {
4008
- const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
4009
- if (!doc.data.channels.includes(ASK_CHANNEL)) {
4010
- await writeMarkdown(this.paths.societyFile(), { ...doc.data, channels: [...doc.data.channels, ASK_CHANNEL] }, doc.body);
4011
- }
4012
- await ensureDir(this.paths.societyChannel(ASK_CHANNEL));
4013
- for (const agent2 of await this.listAgents()) {
4014
- if (agent2.status !== "active" || agent2.subscriptions.includes(ASK_CHANNEL)) {
4015
- continue;
4016
- }
4017
- if (!(await this.readRole(agent2.role)).wakeTriggers.includes(FRONT_DESK_TRIGGER)) {
4018
- continue;
4019
- }
4020
- await this.updateAgent(agent2.name, (a) => ({
4021
- ...a,
4022
- subscriptions: [...a.subscriptions, ASK_CHANNEL]
4023
- }));
4024
- await this.refreshMember(agent2.name);
4025
- await this.events.append("subscription.changed", agent2.name, {
4026
- channel: ASK_CHANNEL,
4027
- subscribed: true,
4028
- aligned: true
4029
- });
4030
- }
4031
- }
4032
- /** Members that predate `decisions` among the ops channels read proposals without their outcome. */
4033
- async followDecisions() {
4034
- if (!(await this.society()).channels.includes("decisions")) {
4035
- return;
4036
- }
4037
- for (const agent2 of await this.listAgents()) {
4038
- if (agent2.status !== "active" || agent2.subscriptions.includes("decisions")) {
4039
- continue;
4040
- }
4041
- if (!(await this.readRole(agent2.role)).wakeTriggers.includes(OPS_WAKE_TRIGGER)) {
4042
- continue;
4043
- }
4044
- await this.updateAgent(agent2.name, (a) => ({
4045
- ...a,
4046
- subscriptions: [...a.subscriptions, "decisions"]
4047
- }));
4048
- await this.refreshMember(agent2.name);
4049
- await this.events.append("subscription.changed", agent2.name, {
4050
- channel: "decisions",
4051
- subscribed: true,
4052
- aligned: true
4053
- });
4054
- }
4055
- }
4056
- /**
4057
- * Threads were once a state field on their task. Each becomes a record under the task's project,
4058
- * on its general channel, closed if its task has ended, and the field leaves the task file.
4059
- */
4060
- async recordTaskThreads() {
4061
- const opened = /* @__PURE__ */ new Map();
4062
- const closed = /* @__PURE__ */ new Map();
4063
- for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
4064
- const taskId = event.payload["taskId"];
4065
- if (typeof taskId !== "string") {
4066
- continue;
4067
- }
4068
- if (event.type === "thread.opened") {
4069
- opened.set(taskId, event);
4070
- } else if (event.type === "thread.closed") {
4071
- closed.set(taskId, event);
4072
- }
4073
- }
4074
- for (const slug of await listDirs(this.paths.projects())) {
4075
- const threads = this.paths.threads(slug);
4076
- const summaries = /* @__PURE__ */ new Map();
4077
- for (const message of await this.readMessagesIn(this.paths.projectChannel(slug, "general"), null)) {
4078
- if (message.task !== void 0) {
4079
- summaries.set(message.task, message.body.replace(/^Thread closed[^\n]*\n\n/, ""));
4080
- }
4081
- }
4082
- for (const file of await listFiles(this.paths.tasks(slug))) {
4083
- const raw = await readMarkdown(path7.join(this.paths.tasks(slug), file), z16.record(z16.string(), z16.unknown()));
4084
- const legacy = raw.data["thread"];
4085
- if (legacy === void 0) {
4086
- continue;
4087
- }
4088
- const task2 = { ...TaskFrontmatterSchema.parse(raw.data), body: raw.body };
4089
- if ((legacy === "open" || legacy === "closed") && !await exists(this.paths.threadFile(threads, task2.id))) {
4090
- const ended = task2.status === "done" || task2.status === "abandoned";
4091
- const open4 = legacy === "open" && !ended;
4092
- const openedBy = opened.get(task2.id);
4093
- const closedBy = closed.get(task2.id);
4094
- await this.writeThread(threads, {
4095
- id: task2.id,
4096
- channel: channelRef(slug, "general"),
4097
- title: task2.title,
4098
- subject: { kind: "task", id: task2.id },
4099
- state: open4 ? "open" : "closed",
4100
- openedBy: openedBy?.actor ?? task2.createdBy,
4101
- openedAt: openedBy?.ts ?? task2.createdAt,
4102
- ...open4 ? {} : {
4103
- closedBy: closedBy?.actor ?? SYSTEM_ACTOR.name,
4104
- closedAt: closedBy?.ts ?? this.now().toISOString()
4105
- },
4106
- body: summaries.get(task2.id) ?? ""
4107
- });
4108
- }
4109
- await this.writeTask(slug, task2);
4110
- }
4111
- }
4112
- }
4113
- /**
4114
- * Every channel of a scope was once read in its home conversation, so a member had read each up
4115
- * to its scope's cursor. Each channel beside general starts from that cursor now, as each takes a
4116
- * conversation of its own.
4117
- */
4118
- async startChannelCursors() {
4119
- for (const agent2 of await this.listAgents()) {
4120
- const file = this.paths.agentCursors(agent2.name);
4121
- if (!await exists(file)) {
4122
- continue;
4123
- }
4124
- const cursors = await readJson(file, CursorsSchema);
4125
- const scopes = /* @__PURE__ */ new Set([
4126
- ...Object.keys(cursors.scopes).filter((key) => !key.includes("/")),
4127
- ...agent2.memberships,
4128
- SOCIETY_SCOPE
4129
- ]);
4130
- const channelsFrom = Object.fromEntries([...scopes].map((scope) => [scope, cursorOf(cursors, scope)]));
4131
- await writeJson(file, { ...cursors, channelsFrom });
4132
- }
4133
- }
4134
- /**
4135
- * A project's channels were once read, from the society scope, in the society's home
4136
- * conversation, so a member had read each up to that cursor. Each project's conversation in the
4137
- * society scope starts from it now, as each takes a conversation of its own.
4138
- */
4139
- async startPlaceCursors() {
4140
- for (const agent2 of await this.listAgents()) {
4141
- const file = this.paths.agentCursors(agent2.name);
4142
- if (!await exists(file)) {
4143
- continue;
4144
- }
4145
- const cursors = await readJson(file, CursorsSchema);
4146
- await writeJson(file, {
4147
- ...cursors,
4148
- placesFrom: { [SOCIETY_SCOPE]: cursorOf(cursors, SOCIETY_SCOPE) }
4149
- });
4150
- }
4151
- }
4152
- /**
4153
- * Threads were once read in the scope's single conversation, so a member had read every thread
4154
- * up to its scope's cursor. Each scope's threads start from that cursor now, as each thread's
4155
- * conversation takes a cursor of its own.
4156
- */
4157
- async startThreadCursors() {
4158
- for (const agent2 of await this.listAgents()) {
4159
- const file = this.paths.agentCursors(agent2.name);
4160
- if (!await exists(file)) {
4161
- continue;
4162
- }
4163
- const cursors = await readJson(file, CursorsSchema);
4164
- const scopes = /* @__PURE__ */ new Set([
4165
- ...Object.keys(cursors.scopes).filter((key) => !key.includes("/")),
4166
- ...agent2.memberships,
4167
- SOCIETY_SCOPE
4168
- ]);
4169
- const threadsFrom = Object.fromEntries([...scopes].map((scope) => [scope, cursorOf(cursors, scope)]));
4170
- await writeJson(file, { ...cursors, threadsFrom });
4171
- }
4172
- }
4173
- /**
4174
- * The digest was once called the inbox: each cursor file keyed its cursor `inbox`, and the user
4175
- * followed every society channel and project general channel to read one. The user takes no
4176
- * turns, so it follows nothing now.
4177
- */
4178
- async renameInboxCursors() {
4179
- const Legacy = z16.object({ inbox: z16.string().nullable() });
4180
- for (const agent2 of await this.listAgents()) {
4181
- const file = this.paths.agentCursors(agent2.name);
4182
- if (await exists(file)) {
4183
- const legacy = Legacy.safeParse(JSON.parse(await readFile4(file, "utf8")));
4184
- if (legacy.success) {
4185
- await writeJson(file, { digest: legacy.data.inbox });
4186
- }
4187
- }
4188
- }
4189
- await this.updateAgent(USER_NAME, (user2) => ({ ...user2, subscriptions: [] }));
4190
- await this.refreshMember(USER_NAME);
3821
+ await writeJson(file, { applied: [...applied, ...pending.map(({ name }) => name)] });
4191
3822
  }
4192
3823
  /** Writes every missing seed charter and aligns the existing ones with their seed. */
4193
3824
  async ensureSeedRoles() {
@@ -4991,7 +4622,7 @@ ${DASHBOARD_LINE}
4991
4622
  if (event.type === "turn.started") {
4992
4623
  const start = TurnStartPayloadSchema.safeParse(event.payload);
4993
4624
  if (start.success)
4994
- starts.set(start.data.turnId ?? start.data.project, event.ts);
4625
+ starts.set(start.data.turnId, event.ts);
4995
4626
  continue;
4996
4627
  }
4997
4628
  if (event.type !== "turn.completed" && event.type !== "turn.failed") {
@@ -4999,9 +4630,11 @@ ${DASHBOARD_LINE}
4999
4630
  }
5000
4631
  const parsed = TurnHistoryPayloadSchema.safeParse(event.payload);
5001
4632
  if (parsed.success) {
5002
- const key = parsed.data.turnId ?? parsed.data.project;
5003
- const startedAt = starts.get(key);
5004
- starts.delete(key);
4633
+ const { turnId } = parsed.data;
4634
+ const startedAt = turnId === void 0 ? void 0 : starts.get(turnId);
4635
+ if (turnId !== void 0) {
4636
+ starts.delete(turnId);
4637
+ }
5005
4638
  entries.push({
5006
4639
  id: event.id,
5007
4640
  ts: event.ts,
@@ -5236,7 +4869,7 @@ ${DASHBOARD_LINE}
5236
4869
  const starts = [
5237
4870
  ...scopes.flatMap((each) => conversation === void 0 ? [
5238
4871
  cursorOf(cursors, each),
5239
- threadStart(cursors, each),
4872
+ cursors.digest,
5240
4873
  channelStart(cursors, each),
5241
4874
  placeStart(cursors, each)
5242
4875
  ] : [cursorOf(cursors, each, conversation.part)]),
@@ -5852,11 +5485,12 @@ ${current.body}`);
5852
5485
  const args = VerbInputs.open_thread.parse(input);
5853
5486
  await this.authorize(actor, "open_thread");
5854
5487
  return this.mutex.run(async () => {
5855
- const opening = await this.threadToOpen(args);
5856
- if (await this.tryFindThread(opening.id) !== null) {
5857
- throw new BoardError("INVALID_STATE", `${opening.subject?.kind ?? "thread"} ${opening.id} already has a thread`);
5858
- }
5859
- return this.openThreadUnlocked(actor.name, opening);
5488
+ await this.assertChannelOpen(args.channel);
5489
+ return this.openThreadUnlocked(actor.name, {
5490
+ id: this.newId(),
5491
+ channel: args.channel,
5492
+ title: args.title
5493
+ });
5860
5494
  });
5861
5495
  }
5862
5496
  /** Writes a new open thread and its event. Callers hold the mutex and chose where it hangs. */
@@ -5891,28 +5525,13 @@ ${current.body}`);
5891
5525
  subject: { kind: "proposal", id: proposal2.id }
5892
5526
  };
5893
5527
  }
5894
- /**
5895
- * The open thread of a proposal being decided. A proposal from before proposals opened their
5896
- * threads gets one now, opened as its proposer, and a thread closed before the decision opens
5897
- * again.
5898
- */
5899
- async proposalThreadUnlocked(by, proposal2) {
5528
+ /** The open thread of a proposal being decided, which `propose` opened with it. */
5529
+ async proposalThreadUnlocked(proposal2) {
5900
5530
  const found = await this.tryFindThread(proposal2.id);
5901
- if (found?.thread.state === "open") {
5902
- return found.thread;
5903
- }
5904
- if (found === null) {
5905
- return this.openThreadUnlocked(proposal2.proposedBy, this.proposalThreadOpening(proposal2));
5531
+ if (found?.thread.state !== "open") {
5532
+ throw new BoardError("INVALID_STATE", `proposal ${proposal2.id} has no open thread`);
5906
5533
  }
5907
- const { closedBy: _by, closedAt: _at, ...kept } = found.thread;
5908
- const reopened = await this.writeThread(found.threads, { ...kept, state: "open" });
5909
- await this.events.append("thread.opened", by, {
5910
- threadId: proposal2.id,
5911
- channel: reopened.channel,
5912
- subject: reopened.subject ?? null,
5913
- reopened: true
5914
- });
5915
- return reopened;
5534
+ return found.thread;
5916
5535
  }
5917
5536
  /** Where a task's thread hangs, what it is called, and what it is about. */
5918
5537
  taskThreadOpening(task2) {
@@ -5924,79 +5543,21 @@ ${current.body}`);
5924
5543
  };
5925
5544
  }
5926
5545
  /**
5927
- * The open thread of a task in play, for a verb about to post into it. A task from before tasks
5928
- * opened their threads gets one now, and a thread closed early while its task is in play opens
5929
- * again; an ended task's thread stays closed.
5546
+ * The open thread of a task in play, for a verb about to post into it, which `createTask` opened
5547
+ * with the task; an ended task's thread closed with it.
5930
5548
  */
5931
- async taskThreadUnlocked(by, task2) {
5549
+ async taskThreadUnlocked(task2) {
5932
5550
  const found = await this.tryFindThread(task2.id);
5933
5551
  if (found?.thread.state === "open") {
5934
5552
  return found.thread;
5935
5553
  }
5936
- if (task2.status === "done" || task2.status === "abandoned") {
5937
- throw new BoardError("INVALID_STATE", `task ${task2.id} is ${task2.status} and its thread closed with it`);
5938
- }
5939
- if (found === null) {
5940
- return this.openThreadUnlocked(by, this.taskThreadOpening(task2));
5941
- }
5942
- const { closedBy: _by, closedAt: _at, ...kept } = found.thread;
5943
- const reopened = await this.writeThread(found.threads, { ...kept, state: "open" });
5944
- await this.events.append("thread.opened", by, {
5945
- threadId: task2.id,
5946
- channel: reopened.channel,
5947
- subject: reopened.subject ?? null,
5948
- reopened: true
5949
- });
5950
- return reopened;
5554
+ throw new BoardError("INVALID_STATE", task2.status === "done" || task2.status === "abandoned" ? `task ${task2.id} is ${task2.status} and its thread closed with it` : `task ${task2.id} has no open thread`);
5951
5555
  }
5952
5556
  /** Posts what a task verb was told to say into the task's thread, as its author, with the step it recorded. */
5953
5557
  async postTaskNote(author, task2, body, step) {
5954
- const thread2 = await this.taskThreadUnlocked(author, task2);
5558
+ const thread2 = await this.taskThreadUnlocked(task2);
5955
5559
  return this.appendThreadMessage(author, thread2.id, body, step === void 0 ? {} : { step });
5956
5560
  }
5957
- /** Where a new thread hangs, what it is called, and what it is about. */
5958
- async threadToOpen(args) {
5959
- if (args.task_id !== void 0 && args.proposal_id !== void 0) {
5960
- throw new BoardError("VALIDATION", "a thread is about one task or one proposal, not both");
5961
- }
5962
- if (args.task_id !== void 0) {
5963
- const { task: task2, project: project2 } = await this.findTask(args.task_id);
5964
- if (task2.status === "done" || task2.status === "abandoned") {
5965
- throw new BoardError("INVALID_STATE", `task ${task2.id} is ${task2.status}`);
5966
- }
5967
- const channel = args.channel ?? channelRef(project2, task2.channel);
5968
- if (parseChannelRef(channel).project !== project2) {
5969
- throw new BoardError("VALIDATION", `a task's thread hangs off a channel of its project, ${project2}`);
5970
- }
5971
- await this.assertChannelOpen(channel);
5972
- return {
5973
- id: task2.id,
5974
- channel,
5975
- title: args.title ?? task2.title,
5976
- subject: { kind: "task", id: task2.id }
5977
- };
5978
- }
5979
- if (args.proposal_id !== void 0) {
5980
- const proposal2 = await this.readProposal(args.proposal_id);
5981
- if (proposal2.status !== "proposed") {
5982
- throw new BoardError("INVALID_STATE", `proposal ${proposal2.id} is already ${proposal2.status}`);
5983
- }
5984
- const channel = args.channel ?? "governance";
5985
- await this.assertChannelOpen(channel);
5986
- const described = `${proposal2.kind} proposal: ${describeCharter(proposal2.kind, proposal2.charter)}`;
5987
- return {
5988
- id: proposal2.id,
5989
- channel,
5990
- title: args.title ?? described.slice(0, 200),
5991
- subject: { kind: "proposal", id: proposal2.id }
5992
- };
5993
- }
5994
- if (args.channel === void 0 || args.title === void 0) {
5995
- throw new BoardError("VALIDATION", "a thread needs a task_id, a proposal_id, or a channel and a title");
5996
- }
5997
- await this.assertChannelOpen(args.channel);
5998
- return { id: this.newId(), channel: args.channel, title: args.title };
5999
- }
6000
5561
  /**
6001
5562
  * Where the board learns which turns are in flight, from the server that hosts them, so it
6002
5563
  * refuses to end a conversation under someone else's turn.
@@ -7130,6 +6691,7 @@ ${rationale}`}`);
7130
6691
  needsUserDecision: parsed.status?.needsUserDecision ?? false,
7131
6692
  error: parsed.error,
7132
6693
  ...parsed.resumeFailed === void 0 ? {} : { resumeFailed: parsed.resumeFailed },
6694
+ ...parsed.transient === true ? { transient: true } : {},
7133
6695
  ...parsed.sessionDropped === true ? { sessionDropped: true } : {}
7134
6696
  });
7135
6697
  });
@@ -7837,15 +7399,9 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7837
7399
  case "archive":
7838
7400
  await this.validateArchive(ArchiveProposalSchema.parse(charter).project);
7839
7401
  return;
7840
- case "reallocation": {
7841
- const parsed = ReallocationProposalSchema.safeParse(charter);
7842
- if (parsed.success) {
7843
- await this.reallocatedRole(parsed.data);
7844
- } else {
7845
- LegacyReallocationSchema.parse(charter);
7846
- }
7402
+ case "reallocation":
7403
+ await this.reallocatedRole(ReallocationProposalSchema.parse(charter));
7847
7404
  return;
7848
- }
7849
7405
  default:
7850
7406
  return;
7851
7407
  }
@@ -7905,11 +7461,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7905
7461
  return { project: input.project, members };
7906
7462
  }
7907
7463
  case "reallocation": {
7908
- const parsed = ReallocationProposalSchema.safeParse(proposal2.charter);
7909
- if (!parsed.success) {
7910
- return void 0;
7911
- }
7912
- const { next, changed } = await this.reallocatedRole(parsed.data);
7464
+ const { next, changed } = await this.reallocatedRole(ReallocationProposalSchema.parse(proposal2.charter));
7913
7465
  await this.writeRoleUnlocked(by, next, meta);
7914
7466
  return { role: next.name, changed };
7915
7467
  }
@@ -7919,8 +7471,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7919
7471
  }
7920
7472
  /**
7921
7473
  * Publishes a promoted skill under the society's skills, where every citizen's skills index lists
7922
- * it: the folder the board took from its proposer's home, or the text proposed alone. A
7923
- * replacement is the whole skill, so files the new version dropped go with the old one.
7474
+ * it: the folder the board took from its proposer's home. A replacement is the whole skill, so
7475
+ * files the new version dropped go with the old one.
7924
7476
  */
7925
7477
  async promoteSkillUnlocked(by, proposal2, skill2, meta) {
7926
7478
  const dir = this.paths.societySkill(skill2.name);
@@ -7928,16 +7480,12 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7928
7480
  const replaced = await exists(file);
7929
7481
  const taken = proposal2.skillFiles;
7930
7482
  const staged = path7.join(this.paths.proposalFiles(proposal2.id), "SKILL.md");
7931
- if (skill2.body === void 0 && (taken === void 0 || !await exists(staged))) {
7483
+ if (taken === void 0 || !await exists(staged)) {
7932
7484
  throw new BoardError("INVALID_STATE", `proposal ${proposal2.id} holds no skill files`);
7933
7485
  }
7934
7486
  await rm2(dir, { recursive: true, force: true });
7935
- if (skill2.body === void 0) {
7936
- await cp(this.paths.proposalFiles(proposal2.id), dir, { recursive: true });
7937
- } else {
7938
- await ensureDir(dir);
7939
- }
7940
- const body = skill2.body ?? (await readLooseMarkdown(file)).body;
7487
+ await cp(this.paths.proposalFiles(proposal2.id), dir, { recursive: true });
7488
+ const { body } = await readLooseMarkdown(file);
7941
7489
  await writeMarkdown(file, {
7942
7490
  name: skill2.name,
7943
7491
  description: skill2.summary,
@@ -7945,7 +7493,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7945
7493
  proposedBy: proposal2.proposedBy,
7946
7494
  promotedBy: by,
7947
7495
  promotedAt: this.now().toISOString(),
7948
- ...taken === void 0 ? {} : { commit: taken.commit }
7496
+ commit: taken.commit
7949
7497
  }
7950
7498
  }, body);
7951
7499
  await this.events.append("skill.promoted", by, {
@@ -7953,7 +7501,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7953
7501
  summary: skill2.summary,
7954
7502
  proposedBy: proposal2.proposedBy,
7955
7503
  replaced,
7956
- ...taken === void 0 ? {} : { files: taken.files.length },
7504
+ files: taken.files.length,
7957
7505
  ...meta
7958
7506
  });
7959
7507
  await this.appendMessage(SYSTEM_ACTOR.name, "general", `Skill ${skill2.name} ${replaced ? "updated in" : "promoted to"} the society: ${sentence(skill2.summary)}. Every citizen's skills index now lists it.`);
@@ -8642,6 +8190,7 @@ ${charter.purpose}
8642
8190
  await this.validateProvision(filed.kind, filed.charter);
8643
8191
  }
8644
8192
  this.assertNoTurnCut(actor, `proposal ${proposalId} cannot be decided`, (turn2) => turn2.thread === proposalId);
8193
+ const thread2 = await this.proposalThreadUnlocked(filed);
8645
8194
  if (outcome === "approved" && filed.kind === "archive") {
8646
8195
  await this.assertProjectQuiet(actor, ArchiveProposalSchema.parse(filed.charter).project);
8647
8196
  }
@@ -8683,7 +8232,6 @@ ${charter.purpose}
8683
8232
  const verdict = outcome === "approved" ? "Approved" : "Rejected";
8684
8233
  const why = reason === void 0 ? "" : ` Reason: ${reason}.`;
8685
8234
  const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
8686
- const thread2 = await this.proposalThreadUnlocked(actor.name, proposal2);
8687
8235
  const post = `${verdict}: ${describeCharter(proposal2.kind, proposal2.charter)}.${why}${result}`;
8688
8236
  await this.appendThreadMessage(actor.name, thread2.id, actor.name === SYSTEM_ACTOR.name ? withoutMentions(post) : post, { step: { action: outcome } });
8689
8237
  await this.endThreadOf(proposalId, actor.name, outcome);
@@ -9182,13 +8730,11 @@ task.command("list <project>").description("List a project's tasks with their cu
9182
8730
  }).join("\n"));
9183
8731
  });
9184
8732
  var thread = program.command("thread").description("Manage threads");
9185
- thread.command("open [taskId]").description("Open a thread on a task, on a proposal, or on a channel with a title").option("--proposal <id>", "open the proposal's thread").option("--channel <ref>", "the channel it hangs off; required for a topic").option("--title <text>", "the thread's title; required for a topic").option("--as <agent>", "act as this agent instead of the user").action(async (taskId, opts) => {
8733
+ thread.command("open").description("Open a thread on a channel with a title; a task's or a proposal's opens with it").requiredOption("--channel <ref>", "the channel it hangs off").requiredOption("--title <text>", "the thread's title").option("--as <agent>", "act as this agent instead of the user").action(async (opts) => {
9186
8734
  const board = await open3();
9187
8735
  const opened = await board.openThread(await actorFor(board, opts.as), {
9188
- ...taskId === void 0 ? {} : { task_id: taskId },
9189
- ...opts.proposal === void 0 ? {} : { proposal_id: opts.proposal },
9190
- ...opts.channel === void 0 ? {} : { channel: opts.channel },
9191
- ...opts.title === void 0 ? {} : { title: opts.title }
8736
+ channel: opts.channel,
8737
+ title: opts.title
9192
8738
  });
9193
8739
  print(opened, () => `Thread ${opened.id} opened on ${opened.channel}: ${opened.title}`);
9194
8740
  });