@kubitnodes/stellaris 0.3.6 → 0.3.8

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 (38) hide show
  1. package/dist/cli/index.js +108 -18
  2. package/dist/server/index.js +179 -36
  3. package/package.json +1 -1
  4. package/web/dist/assets/{BackLink-Cr1RWb4K.js → BackLink-DrYyD7II.js} +1 -1
  5. package/web/dist/assets/{ChannelView-iK3ug1lV.js → ChannelView-B2sufhRs.js} +1 -1
  6. package/web/dist/assets/CitizenView-B9u0X7ZY.js +2 -0
  7. package/web/dist/assets/{CitizensView-CFrD6f7m.js → CitizensView-B60Z7gmH.js} +1 -1
  8. package/web/dist/assets/KnowledgeView-Cppc2D7_.js +1 -0
  9. package/web/dist/assets/{Markdown-CYcTTxcR.js → Markdown-B390nfRS.js} +1 -1
  10. package/web/dist/assets/{MessageItem-DDC_1Jqn.js → MessageItem-D54BS9OS.js} +1 -1
  11. package/web/dist/assets/MetricsView-CsoRuj4g.js +1 -0
  12. package/web/dist/assets/{NeedsYouView-I7j_cnda.js → NeedsYouView-BPjjyZRx.js} +1 -1
  13. package/web/dist/assets/{Overview-DI7LPNEY.js → Overview-BWEXLQaq.js} +1 -1
  14. package/web/dist/assets/{Pane-BNjb76U4.js → Pane-DvocjNie.js} +1 -1
  15. package/web/dist/assets/ProjectView-BIC7kE4L.js +1 -0
  16. package/web/dist/assets/{ProposalView-DrP3Cv9y.js → ProposalView-cPi-EUXR.js} +1 -1
  17. package/web/dist/assets/{ProposalsView-90rufsQN.js → ProposalsView-Bq7iCMtk.js} +1 -1
  18. package/web/dist/assets/{RunnersView-iMbemuCe.js → RunnersView-BiJmbsoG.js} +1 -1
  19. package/web/dist/assets/{SocietyView-C9FdIFZY.js → SocietyView-6iPyxKze.js} +1 -1
  20. package/web/dist/assets/{SubjectThread-Bw6wEJpH.js → SubjectThread-D2BHnecl.js} +1 -1
  21. package/web/dist/assets/{TaskFileView-D0n7kzt2.js → TaskFileView-DDs6OHWY.js} +1 -1
  22. package/web/dist/assets/{TaskView-BZ6U_XpQ.js → TaskView-3_2vs2fy.js} +1 -1
  23. package/web/dist/assets/{TasksView-CtGF3i-n.js → TasksView-B7a0sWKu.js} +1 -1
  24. package/web/dist/assets/{ThreadCard-FPtAzvNM.js → ThreadCard-9h03aKCB.js} +1 -1
  25. package/web/dist/assets/{ThreadForms-LFw2EF6r.js → ThreadForms-uORhDqAo.js} +1 -1
  26. package/web/dist/assets/{ThreadView-DnDx2SCv.js → ThreadView-DVSe1HGK.js} +1 -1
  27. package/web/dist/assets/{citizen-Bm_r8vBY.js → citizen-CDTy3g7f.js} +1 -1
  28. package/web/dist/assets/format-C6tBTRKZ.js +55 -0
  29. package/web/dist/assets/index-CugUsWCm.js +23 -0
  30. package/web/dist/assets/{tasks-tFehkpRz.js → tasks-Bh3GaY7V.js} +1 -1
  31. package/web/dist/assets/{useStickyScroll-XrG6M-hT.js → useStickyScroll-C6bu36lV.js} +1 -1
  32. package/web/dist/index.html +3 -3
  33. package/web/dist/assets/CitizenView-BiaYYmCh.js +0 -2
  34. package/web/dist/assets/KnowledgeView-CR3cXdj0.js +0 -1
  35. package/web/dist/assets/MetricsView-xdvE7pFA.js +0 -1
  36. package/web/dist/assets/ProjectView-DYDcDsWg.js +0 -1
  37. package/web/dist/assets/format-Cj7NW9Y9.js +0 -55
  38. package/web/dist/assets/index-CthEgIzf.js +0 -23
package/dist/cli/index.js CHANGED
@@ -6,7 +6,7 @@ import path7 from "node:path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // packages/board-core/dist/board.js
9
- import { readdir as readdir3, readFile as readFile5, rename as rename3, stat as stat2 } from "node:fs/promises";
9
+ import { mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rename as rename3, stat as stat2 } from "node:fs/promises";
10
10
  import path6 from "node:path";
11
11
  import { decodeTime, monotonicFactory } from "ulid";
12
12
  import { z as z12 } from "zod";
@@ -56,7 +56,8 @@ var VERB_NAMES = [
56
56
  "plan_task",
57
57
  "advance_task",
58
58
  "configure_project",
59
- "archive_project"
59
+ "archive_project",
60
+ "remove_knowledge"
60
61
  ];
61
62
  var VerbNameSchema = z2.enum(VERB_NAMES);
62
63
  var CharterTriggerSchema = z2.enum(["user_post", "ops_event", "heartbeat"]);
@@ -100,6 +101,7 @@ var MEMBER_VERBS = [
100
101
  "join_project",
101
102
  "leave_project",
102
103
  "write_knowledge",
104
+ "remove_knowledge",
103
105
  "plan_task",
104
106
  "advance_task"
105
107
  ];
@@ -314,6 +316,11 @@ var ModelOptionSchema = z3.object({
314
316
  efforts: z3.array(EffortOptionSchema).default([]),
315
317
  defaultEffort: EffortSchema.optional()
316
318
  });
319
+ var RunnerModelsSchema = z3.object({
320
+ runner: NameSchema,
321
+ cli: CliKindSchema,
322
+ models: z3.array(ModelOptionSchema)
323
+ });
317
324
  var AgentStatusSchema = z3.enum(["active", "retired"]);
318
325
  var AgentSchema = z3.object({
319
326
  name: NameSchema,
@@ -534,6 +541,12 @@ var KnowledgeSchema = z3.object({
534
541
  updatedBy: NameSchema,
535
542
  updatedAt: IsoDateTimeSchema
536
543
  });
544
+ var RemovedKnowledgeSchema = z3.object({
545
+ topic: NameSchema,
546
+ project: NameSchema.nullable(),
547
+ removedBy: NameSchema,
548
+ removedAt: IsoDateTimeSchema
549
+ });
537
550
  var ProposalFrontmatterSchema = z3.object({
538
551
  id: UlidSchema,
539
552
  kind: ProposalKindSchema,
@@ -630,6 +643,11 @@ var VerbInputs = {
630
643
  topic: NameSchema,
631
644
  body: z4.string().min(1)
632
645
  }),
646
+ remove_knowledge: z4.object({
647
+ /** As for write_knowledge: a project you belong to, or null for society knowledge. */
648
+ project: NameSchema.nullable().default(null),
649
+ topic: NameSchema
650
+ }),
633
651
  plan_task: z4.object({
634
652
  task_id: UlidSchema,
635
653
  stages: z4.array(PlanEditStageSchema),
@@ -639,7 +657,9 @@ var VerbInputs = {
639
657
  advance_task: z4.object({ task_id: UlidSchema, note: z4.string().min(1).optional() }),
640
658
  configure_project: z4.object({
641
659
  project: NameSchema,
642
- on_done: CompletionEffectSchema
660
+ on_done: CompletionEffectSchema.optional(),
661
+ /** A new display name; the slug, which everything refers to the project by, never changes. */
662
+ name: z4.string().trim().min(1).max(120).optional()
643
663
  }),
644
664
  archive_project: z4.object({ project: NameSchema, reason: z4.string().min(1) })
645
665
  };
@@ -679,6 +699,7 @@ var BOARD_EVENT_TYPES = [
679
699
  "agent.joined",
680
700
  "agent.left",
681
701
  "knowledge.written",
702
+ "knowledge.removed",
682
703
  "skill.promoted",
683
704
  "runner.added",
684
705
  "user.signed_in",
@@ -1276,7 +1297,7 @@ var RunnerConfigSchema = z10.object({
1276
1297
  });
1277
1298
 
1278
1299
  // packages/shared/dist/version.js
1279
- var STELLARIS_VERSION = "0.3.6";
1300
+ var STELLARIS_VERSION = "0.3.8";
1280
1301
 
1281
1302
  // packages/board-core/dist/errors.js
1282
1303
  var BoardError = class extends Error {
@@ -2309,6 +2330,10 @@ var BoardPaths = class {
2309
2330
  pausedFile() {
2310
2331
  return path5.join(this.state(), "paused.json");
2311
2332
  }
2333
+ /** Removed knowledge topics, set aside outside the projection, by scope. */
2334
+ removedKnowledge(scope) {
2335
+ return path5.join(this.state(), "removed-knowledge", scope);
2336
+ }
2312
2337
  };
2313
2338
 
2314
2339
  // packages/board-core/dist/tokens.js
@@ -2402,6 +2427,7 @@ var OPS_CHANNEL_RETIRED = "ops-channel-retired";
2402
2427
  var DECISIONS_CHANNEL_RETIRED = "decisions-channel-retired";
2403
2428
  var THREAD_CONVERSATIONS = "thread-conversations";
2404
2429
  var ASKS_CHANNEL_OPENED = "asks-channel-opened";
2430
+ var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
2405
2431
  var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
2406
2432
  var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
2407
2433
  var PROFILE_TEMPLATE = "# Profile\n\nOne short paragraph, kept current: what I do well, what I am working on, and what to send my way. The board projects this into the roster the front desk reads.\n";
@@ -2591,7 +2617,8 @@ var Board = class _Board {
2591
2617
  OPS_CHANNEL_RETIRED,
2592
2618
  DECISIONS_CHANNEL_RETIRED,
2593
2619
  THREAD_CONVERSATIONS,
2594
- ASKS_CHANNEL_OPENED
2620
+ ASKS_CHANNEL_OPENED,
2621
+ KNOWLEDGE_REMOVAL
2595
2622
  ].filter((name) => !applied.includes(name));
2596
2623
  if (pending.length === 0) {
2597
2624
  return;
@@ -2620,8 +2647,22 @@ var Board = class _Board {
2620
2647
  if (pending.includes(ASKS_CHANNEL_OPENED)) {
2621
2648
  await this.openAsksChannel();
2622
2649
  }
2650
+ if (pending.includes(KNOWLEDGE_REMOVAL)) {
2651
+ await this.grantKnowledgeRemoval();
2652
+ }
2623
2653
  await writeJson(file, { applied: [...applied, ...pending] });
2624
2654
  }
2655
+ /**
2656
+ * Whoever may write a scope's knowledge may remove it, so every charter granting
2657
+ * `write_knowledge` is granted `remove_knowledge` once; seed roles have it from their seed.
2658
+ */
2659
+ async grantKnowledgeRemoval() {
2660
+ for (const charter of await this.listRoles()) {
2661
+ if (charter.verbs.includes("write_knowledge") && !charter.verbs.includes("remove_knowledge")) {
2662
+ await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, "remove_knowledge"] }, { verbsAdded: ["remove_knowledge"] });
2663
+ }
2664
+ }
2665
+ }
2625
2666
  /**
2626
2667
  * A task sent back before tasks recorded it kept no trace outside the event log. A task still
2627
2668
  * short of the stage its latest `task.moved` came from is in that rework, and records it.
@@ -3853,16 +3894,7 @@ ${task2.body}`;
3853
3894
  const args = VerbInputs.write_knowledge.parse(input);
3854
3895
  await this.authorize(actor, "write_knowledge");
3855
3896
  return this.mutex.run(async () => {
3856
- if (args.project === null) {
3857
- if (!CURATING_ROLES.includes(actor.role)) {
3858
- throw new BoardError("FORBIDDEN", "society knowledge is curated by the steward and the user");
3859
- }
3860
- } else {
3861
- const project2 = await this.readActiveProject(args.project);
3862
- if (!CURATING_ROLES.includes(actor.role) && !project2.members.includes(actor.name)) {
3863
- throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${args.project}`);
3864
- }
3865
- }
3897
+ await this.assertMayCurate(actor, args.project);
3866
3898
  const data = KnowledgeSchema.parse({
3867
3899
  topic: args.topic,
3868
3900
  project: args.project,
@@ -3882,6 +3914,52 @@ ${task2.body}`;
3882
3914
  return { ...data, body: args.body };
3883
3915
  });
3884
3916
  }
3917
+ /**
3918
+ * Retires a topic, by whoever may write it. Its file leaves the knowledge directory, so no turn,
3919
+ * search, or digest finds it again, and is set aside under `state/removed-knowledge`, outside
3920
+ * the projection, for the user to restore by hand. The note in the matching general channel
3921
+ * carries no mention, as a write's does.
3922
+ */
3923
+ async removeKnowledge(actor, input) {
3924
+ const args = VerbInputs.remove_knowledge.parse(input);
3925
+ await this.authorize(actor, "remove_knowledge");
3926
+ return this.mutex.run(async () => {
3927
+ await this.assertMayCurate(actor, args.project);
3928
+ const file = args.project === null ? this.paths.societyKnowledgeFile(args.topic) : this.paths.projectKnowledgeFile(args.project, args.topic);
3929
+ if (!await exists(file)) {
3930
+ throw new BoardError("NOT_FOUND", `${args.project ?? "the society"} has no knowledge topic ${args.topic}`);
3931
+ }
3932
+ const removed = RemovedKnowledgeSchema.parse({
3933
+ topic: args.topic,
3934
+ project: args.project,
3935
+ removedBy: actor.name,
3936
+ removedAt: this.now().toISOString()
3937
+ });
3938
+ const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
3939
+ await mkdir4(aside, { recursive: true });
3940
+ await rename3(file, path6.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
3941
+ await this.events.append("knowledge.removed", actor.name, {
3942
+ topic: args.topic,
3943
+ project: args.project
3944
+ });
3945
+ const where = args.project === null ? "general" : channelRef(args.project, "general");
3946
+ await this.appendMessage(actor.name, where, `Knowledge removed: ${args.topic}. It is no longer under knowledge/${args.topic}.md${args.project === null ? " of the society" : ""}.`);
3947
+ return removed;
3948
+ });
3949
+ }
3950
+ /** Society knowledge is the steward's and the user's; a project's, its members' too. */
3951
+ async assertMayCurate(actor, project2) {
3952
+ if (project2 === null) {
3953
+ if (!CURATING_ROLES.includes(actor.role)) {
3954
+ throw new BoardError("FORBIDDEN", "society knowledge is curated by the steward and the user");
3955
+ }
3956
+ return;
3957
+ }
3958
+ const active = await this.readActiveProject(project2);
3959
+ if (!CURATING_ROLES.includes(actor.role) && !active.members.includes(actor.name)) {
3960
+ throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${project2}`);
3961
+ }
3962
+ }
3885
3963
  async openThread(actor, input) {
3886
3964
  const args = VerbInputs.open_thread.parse(input);
3887
3965
  await this.authorize(actor, "open_thread");
@@ -4551,18 +4629,28 @@ ${rationale}`}`);
4551
4629
  }));
4552
4630
  }
4553
4631
  /** Sets a project's completion effect. */
4632
+ /**
4633
+ * Sets a project's completion effect, its display name, or both. The slug stays: addresses,
4634
+ * channel references, branches, and runners' directories all carry it.
4635
+ */
4554
4636
  async configureProject(actor, input) {
4555
4637
  const args = VerbInputs.configure_project.parse(input);
4556
4638
  await this.authorize(actor, "configure_project");
4639
+ if (args.on_done === void 0 && args.name === void 0) {
4640
+ throw new BoardError("VALIDATION", "configure_project needs on_done, name, or both");
4641
+ }
4557
4642
  return this.mutex.run(async () => {
4558
- await this.readActiveProject(args.project);
4643
+ const previous = await this.readActiveProject(args.project);
4559
4644
  const project2 = await this.updateProject(args.project, (current) => ({
4560
4645
  ...current,
4561
- onDone: args.on_done
4646
+ ...args.on_done === void 0 ? {} : { onDone: args.on_done },
4647
+ ...args.name === void 0 ? {} : { name: args.name }
4562
4648
  }));
4563
4649
  await this.events.append("project.configured", actor.name, {
4564
4650
  slug: project2.slug,
4565
- onDone: project2.onDone
4651
+ onDone: project2.onDone,
4652
+ name: project2.name,
4653
+ ...project2.name === previous.name ? {} : { previousName: previous.name }
4566
4654
  });
4567
4655
  return project2;
4568
4656
  });
@@ -4708,6 +4796,8 @@ ${rationale}`}`);
4708
4796
  return this.leaveProject(actor, VerbInputs.leave_project.parse(input));
4709
4797
  case "write_knowledge":
4710
4798
  return this.writeKnowledge(actor, VerbInputs.write_knowledge.parse(input));
4799
+ case "remove_knowledge":
4800
+ return this.removeKnowledge(actor, VerbInputs.remove_knowledge.parse(input));
4711
4801
  case "plan_task":
4712
4802
  return this.planTask(actor, VerbInputs.plan_task.parse(input));
4713
4803
  case "advance_task":
@@ -6,7 +6,7 @@ import path9 from "node:path";
6
6
  import { serve } from "@hono/node-server";
7
7
 
8
8
  // packages/board-core/dist/board.js
9
- import { readdir as readdir3, readFile as readFile5, rename as rename3, stat as stat2 } from "node:fs/promises";
9
+ import { mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rename as rename3, stat as stat2 } from "node:fs/promises";
10
10
  import path6 from "node:path";
11
11
  import { decodeTime, monotonicFactory } from "ulid";
12
12
  import { z as z12 } from "zod";
@@ -56,7 +56,8 @@ var VERB_NAMES = [
56
56
  "plan_task",
57
57
  "advance_task",
58
58
  "configure_project",
59
- "archive_project"
59
+ "archive_project",
60
+ "remove_knowledge"
60
61
  ];
61
62
  var VerbNameSchema = z2.enum(VERB_NAMES);
62
63
  var CharterTriggerSchema = z2.enum(["user_post", "ops_event", "heartbeat"]);
@@ -100,6 +101,7 @@ var MEMBER_VERBS = [
100
101
  "join_project",
101
102
  "leave_project",
102
103
  "write_knowledge",
104
+ "remove_knowledge",
103
105
  "plan_task",
104
106
  "advance_task"
105
107
  ];
@@ -314,6 +316,11 @@ var ModelOptionSchema = z3.object({
314
316
  efforts: z3.array(EffortOptionSchema).default([]),
315
317
  defaultEffort: EffortSchema.optional()
316
318
  });
319
+ var RunnerModelsSchema = z3.object({
320
+ runner: NameSchema,
321
+ cli: CliKindSchema,
322
+ models: z3.array(ModelOptionSchema)
323
+ });
317
324
  var AgentStatusSchema = z3.enum(["active", "retired"]);
318
325
  var AgentSchema = z3.object({
319
326
  name: NameSchema,
@@ -534,6 +541,12 @@ var KnowledgeSchema = z3.object({
534
541
  updatedBy: NameSchema,
535
542
  updatedAt: IsoDateTimeSchema
536
543
  });
544
+ var RemovedKnowledgeSchema = z3.object({
545
+ topic: NameSchema,
546
+ project: NameSchema.nullable(),
547
+ removedBy: NameSchema,
548
+ removedAt: IsoDateTimeSchema
549
+ });
537
550
  var ProposalFrontmatterSchema = z3.object({
538
551
  id: UlidSchema,
539
552
  kind: ProposalKindSchema,
@@ -630,6 +643,11 @@ var VerbInputs = {
630
643
  topic: NameSchema,
631
644
  body: z4.string().min(1)
632
645
  }),
646
+ remove_knowledge: z4.object({
647
+ /** As for write_knowledge: a project you belong to, or null for society knowledge. */
648
+ project: NameSchema.nullable().default(null),
649
+ topic: NameSchema
650
+ }),
633
651
  plan_task: z4.object({
634
652
  task_id: UlidSchema,
635
653
  stages: z4.array(PlanEditStageSchema),
@@ -639,7 +657,9 @@ var VerbInputs = {
639
657
  advance_task: z4.object({ task_id: UlidSchema, note: z4.string().min(1).optional() }),
640
658
  configure_project: z4.object({
641
659
  project: NameSchema,
642
- on_done: CompletionEffectSchema
660
+ on_done: CompletionEffectSchema.optional(),
661
+ /** A new display name; the slug, which everything refers to the project by, never changes. */
662
+ name: z4.string().trim().min(1).max(120).optional()
643
663
  }),
644
664
  archive_project: z4.object({ project: NameSchema, reason: z4.string().min(1) })
645
665
  };
@@ -663,9 +683,10 @@ var VERB_DESCRIPTIONS = {
663
683
  join_project: "Join a project, or add another citizen to one when your role allows it. Membership gives the pair a worktree and an onboarding turn.",
664
684
  leave_project: "Leave a project, or remove another citizen from one when your role allows it.",
665
685
  write_knowledge: "Write or replace a knowledge topic: durable facts every member of a project should know, or with project null the society's shared knowledge (steward and user). Not a message; use post_message for those.",
686
+ remove_knowledge: "Remove a knowledge topic that is wrong, superseded, or folded into another, so no turn or search finds it again. Whoever may write the topic may remove it; the board keeps its text aside for the user to restore.",
666
687
  plan_task: "Reshape a task's plan from the current stage onward, or after it while someone holds it: keep a stage by passing its id, drop it by leaving it out, add one without an id. Gated stages and on_done: user, steward, concierge.",
667
688
  advance_task: "Finish the stage you hold, with a note for the task's thread on what you did. The next stage becomes current; past the last one the task is done.",
668
- configure_project: "Set a project's completion effect: none, or merge to land each finished task's branch on the default branch.",
689
+ configure_project: "Set a project's completion effect (none, or merge to land each finished task's branch on the default branch), its display name, or both. The slug never changes.",
669
690
  archive_project: "Archive a project whose work is finished or has moved: 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."
670
691
  };
671
692
 
@@ -704,6 +725,7 @@ var BOARD_EVENT_TYPES = [
704
725
  "agent.joined",
705
726
  "agent.left",
706
727
  "knowledge.written",
728
+ "knowledge.removed",
707
729
  "skill.promoted",
708
730
  "runner.added",
709
731
  "user.signed_in",
@@ -1356,7 +1378,7 @@ var RunnerConfigSchema = z10.object({
1356
1378
  });
1357
1379
 
1358
1380
  // packages/shared/dist/version.js
1359
- var STELLARIS_VERSION = "0.3.6";
1381
+ var STELLARIS_VERSION = "0.3.8";
1360
1382
 
1361
1383
  // packages/board-core/dist/errors.js
1362
1384
  var BoardError = class extends Error {
@@ -2392,6 +2414,10 @@ var BoardPaths = class {
2392
2414
  pausedFile() {
2393
2415
  return path5.join(this.state(), "paused.json");
2394
2416
  }
2417
+ /** Removed knowledge topics, set aside outside the projection, by scope. */
2418
+ removedKnowledge(scope) {
2419
+ return path5.join(this.state(), "removed-knowledge", scope);
2420
+ }
2395
2421
  };
2396
2422
 
2397
2423
  // packages/board-core/dist/tokens.js
@@ -2485,6 +2511,7 @@ var OPS_CHANNEL_RETIRED = "ops-channel-retired";
2485
2511
  var DECISIONS_CHANNEL_RETIRED = "decisions-channel-retired";
2486
2512
  var THREAD_CONVERSATIONS = "thread-conversations";
2487
2513
  var ASKS_CHANNEL_OPENED = "asks-channel-opened";
2514
+ var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
2488
2515
  var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
2489
2516
  var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
2490
2517
  var PROFILE_TEMPLATE = "# Profile\n\nOne short paragraph, kept current: what I do well, what I am working on, and what to send my way. The board projects this into the roster the front desk reads.\n";
@@ -2674,7 +2701,8 @@ var Board = class _Board {
2674
2701
  OPS_CHANNEL_RETIRED,
2675
2702
  DECISIONS_CHANNEL_RETIRED,
2676
2703
  THREAD_CONVERSATIONS,
2677
- ASKS_CHANNEL_OPENED
2704
+ ASKS_CHANNEL_OPENED,
2705
+ KNOWLEDGE_REMOVAL
2678
2706
  ].filter((name) => !applied.includes(name));
2679
2707
  if (pending.length === 0) {
2680
2708
  return;
@@ -2703,8 +2731,22 @@ var Board = class _Board {
2703
2731
  if (pending.includes(ASKS_CHANNEL_OPENED)) {
2704
2732
  await this.openAsksChannel();
2705
2733
  }
2734
+ if (pending.includes(KNOWLEDGE_REMOVAL)) {
2735
+ await this.grantKnowledgeRemoval();
2736
+ }
2706
2737
  await writeJson(file, { applied: [...applied, ...pending] });
2707
2738
  }
2739
+ /**
2740
+ * Whoever may write a scope's knowledge may remove it, so every charter granting
2741
+ * `write_knowledge` is granted `remove_knowledge` once; seed roles have it from their seed.
2742
+ */
2743
+ async grantKnowledgeRemoval() {
2744
+ for (const charter of await this.listRoles()) {
2745
+ if (charter.verbs.includes("write_knowledge") && !charter.verbs.includes("remove_knowledge")) {
2746
+ await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, "remove_knowledge"] }, { verbsAdded: ["remove_knowledge"] });
2747
+ }
2748
+ }
2749
+ }
2708
2750
  /**
2709
2751
  * A task sent back before tasks recorded it kept no trace outside the event log. A task still
2710
2752
  * short of the stage its latest `task.moved` came from is in that rework, and records it.
@@ -3936,16 +3978,7 @@ ${task.body}`;
3936
3978
  const args = VerbInputs.write_knowledge.parse(input);
3937
3979
  await this.authorize(actor, "write_knowledge");
3938
3980
  return this.mutex.run(async () => {
3939
- if (args.project === null) {
3940
- if (!CURATING_ROLES.includes(actor.role)) {
3941
- throw new BoardError("FORBIDDEN", "society knowledge is curated by the steward and the user");
3942
- }
3943
- } else {
3944
- const project = await this.readActiveProject(args.project);
3945
- if (!CURATING_ROLES.includes(actor.role) && !project.members.includes(actor.name)) {
3946
- throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${args.project}`);
3947
- }
3948
- }
3981
+ await this.assertMayCurate(actor, args.project);
3949
3982
  const data = KnowledgeSchema.parse({
3950
3983
  topic: args.topic,
3951
3984
  project: args.project,
@@ -3965,6 +3998,52 @@ ${task.body}`;
3965
3998
  return { ...data, body: args.body };
3966
3999
  });
3967
4000
  }
4001
+ /**
4002
+ * Retires a topic, by whoever may write it. Its file leaves the knowledge directory, so no turn,
4003
+ * search, or digest finds it again, and is set aside under `state/removed-knowledge`, outside
4004
+ * the projection, for the user to restore by hand. The note in the matching general channel
4005
+ * carries no mention, as a write's does.
4006
+ */
4007
+ async removeKnowledge(actor, input) {
4008
+ const args = VerbInputs.remove_knowledge.parse(input);
4009
+ await this.authorize(actor, "remove_knowledge");
4010
+ return this.mutex.run(async () => {
4011
+ await this.assertMayCurate(actor, args.project);
4012
+ const file = args.project === null ? this.paths.societyKnowledgeFile(args.topic) : this.paths.projectKnowledgeFile(args.project, args.topic);
4013
+ if (!await exists(file)) {
4014
+ throw new BoardError("NOT_FOUND", `${args.project ?? "the society"} has no knowledge topic ${args.topic}`);
4015
+ }
4016
+ const removed = RemovedKnowledgeSchema.parse({
4017
+ topic: args.topic,
4018
+ project: args.project,
4019
+ removedBy: actor.name,
4020
+ removedAt: this.now().toISOString()
4021
+ });
4022
+ const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
4023
+ await mkdir4(aside, { recursive: true });
4024
+ await rename3(file, path6.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
4025
+ await this.events.append("knowledge.removed", actor.name, {
4026
+ topic: args.topic,
4027
+ project: args.project
4028
+ });
4029
+ const where = args.project === null ? "general" : channelRef(args.project, "general");
4030
+ await this.appendMessage(actor.name, where, `Knowledge removed: ${args.topic}. It is no longer under knowledge/${args.topic}.md${args.project === null ? " of the society" : ""}.`);
4031
+ return removed;
4032
+ });
4033
+ }
4034
+ /** Society knowledge is the steward's and the user's; a project's, its members' too. */
4035
+ async assertMayCurate(actor, project) {
4036
+ if (project === null) {
4037
+ if (!CURATING_ROLES.includes(actor.role)) {
4038
+ throw new BoardError("FORBIDDEN", "society knowledge is curated by the steward and the user");
4039
+ }
4040
+ return;
4041
+ }
4042
+ const active = await this.readActiveProject(project);
4043
+ if (!CURATING_ROLES.includes(actor.role) && !active.members.includes(actor.name)) {
4044
+ throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${project}`);
4045
+ }
4046
+ }
3968
4047
  async openThread(actor, input) {
3969
4048
  const args = VerbInputs.open_thread.parse(input);
3970
4049
  await this.authorize(actor, "open_thread");
@@ -4634,18 +4713,28 @@ ${rationale}`}`);
4634
4713
  }));
4635
4714
  }
4636
4715
  /** Sets a project's completion effect. */
4716
+ /**
4717
+ * Sets a project's completion effect, its display name, or both. The slug stays: addresses,
4718
+ * channel references, branches, and runners' directories all carry it.
4719
+ */
4637
4720
  async configureProject(actor, input) {
4638
4721
  const args = VerbInputs.configure_project.parse(input);
4639
4722
  await this.authorize(actor, "configure_project");
4723
+ if (args.on_done === void 0 && args.name === void 0) {
4724
+ throw new BoardError("VALIDATION", "configure_project needs on_done, name, or both");
4725
+ }
4640
4726
  return this.mutex.run(async () => {
4641
- await this.readActiveProject(args.project);
4727
+ const previous = await this.readActiveProject(args.project);
4642
4728
  const project = await this.updateProject(args.project, (current) => ({
4643
4729
  ...current,
4644
- onDone: args.on_done
4730
+ ...args.on_done === void 0 ? {} : { onDone: args.on_done },
4731
+ ...args.name === void 0 ? {} : { name: args.name }
4645
4732
  }));
4646
4733
  await this.events.append("project.configured", actor.name, {
4647
4734
  slug: project.slug,
4648
- onDone: project.onDone
4735
+ onDone: project.onDone,
4736
+ name: project.name,
4737
+ ...project.name === previous.name ? {} : { previousName: previous.name }
4649
4738
  });
4650
4739
  return project;
4651
4740
  });
@@ -4791,6 +4880,8 @@ ${rationale}`}`);
4791
4880
  return this.leaveProject(actor, VerbInputs.leave_project.parse(input));
4792
4881
  case "write_knowledge":
4793
4882
  return this.writeKnowledge(actor, VerbInputs.write_knowledge.parse(input));
4883
+ case "remove_knowledge":
4884
+ return this.removeKnowledge(actor, VerbInputs.remove_knowledge.parse(input));
4794
4885
  case "plan_task":
4795
4886
  return this.planTask(actor, VerbInputs.plan_task.parse(input));
4796
4887
  case "advance_task":
@@ -7601,7 +7692,7 @@ var GOVERNANCE = [
7601
7692
  ' - 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',
7602
7693
  ' - reallocation: {"description"}',
7603
7694
  ` - skill: {"name", "summary", "body"}: the SKILL.md text; approval publishes it under society/skills, where every citizen's skills index lists it`,
7604
- "- 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 (user, steward, concierge). 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.",
7695
+ "- 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 or its display name, never its slug (user, steward, concierge). 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.",
7605
7696
  "- 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.",
7606
7697
  "- 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."
7607
7698
  ].join("\n");
@@ -8473,6 +8564,7 @@ var RunnerHub = class {
8473
8564
  pending = /* @__PURE__ */ new Map();
8474
8565
  /** Disconnects being recorded, which `close` waits for. */
8475
8566
  detaching = /* @__PURE__ */ new Set();
8567
+ runnerListeners = /* @__PURE__ */ new Set();
8476
8568
  constructor(options) {
8477
8569
  this.board = options.board;
8478
8570
  this.host = options.host;
@@ -8528,6 +8620,7 @@ var RunnerHub = class {
8528
8620
  clis: hello.clis,
8529
8621
  capabilities: hello.capabilities
8530
8622
  });
8623
+ this.runnerChanged(name);
8531
8624
  this.log.info({ runner: name, clis: hello.clis, slots: hello.slots }, "runner registered");
8532
8625
  return { name, version: this.version };
8533
8626
  }
@@ -8544,6 +8637,7 @@ var RunnerHub = class {
8544
8637
  seat.grace = null;
8545
8638
  }
8546
8639
  await this.board.markRunner(name, { status: "connected" });
8640
+ this.runnerChanged(name);
8547
8641
  return async () => {
8548
8642
  if (seat.send !== send) {
8549
8643
  return;
@@ -8816,16 +8910,37 @@ var RunnerHub = class {
8816
8910
  return changes;
8817
8911
  }
8818
8912
  /** The models a CLI offers, asked of a connected runner that has it; none when no runner has it. */
8819
- async models(cli) {
8820
- const seat = this.byLoad().find((each) => each.send !== null && each.hello.clis.includes(cli));
8821
- if (seat === void 0) {
8822
- throw new Error(`no connected runner has ${cli}`);
8913
+ /**
8914
+ * The connected runner a model list for `cli` comes from: the first of `prefer` that has the CLI,
8915
+ * else the least busy that does; null when no connected runner has it.
8916
+ */
8917
+ modelRunner(cli, prefer = []) {
8918
+ const able = (seat) => seat !== void 0 && seat.send !== null && seat.hello.clis.includes(cli);
8919
+ const preferred = prefer.map((name) => this.seats.get(name)).find(able);
8920
+ return (preferred ?? this.byLoad().find(able))?.name ?? null;
8921
+ }
8922
+ /** The models `cli` lists on a runner, `modelRunner`'s choice when none is named. */
8923
+ async models(cli, runner) {
8924
+ const name = runner ?? this.modelRunner(cli);
8925
+ const seat = name === null ? void 0 : this.seats.get(name);
8926
+ if (seat === void 0 || seat.send === null || !seat.hello.clis.includes(cli)) {
8927
+ throw new RunnerAwayError(runner === void 0 ? `no connected runner has ${cli}` : `runner ${runner} is not connected with ${cli}`);
8823
8928
  }
8824
8929
  return ModelListSchema.parse(await this.request(seat, (request) => ({ type: "models", request, cli })));
8825
8930
  }
8931
+ /** Calls `listener` whenever a runner registers, connects, or goes away. */
8932
+ onRunnerChange(listener) {
8933
+ this.runnerListeners.add(listener);
8934
+ }
8935
+ runnerChanged(runner) {
8936
+ for (const listener of this.runnerListeners) {
8937
+ listener(runner);
8938
+ }
8939
+ }
8826
8940
  // -------------------------------------------------------------------------------------------
8827
8941
  async detach(seat) {
8828
8942
  seat.send = null;
8943
+ this.runnerChanged(seat.name);
8829
8944
  seat.disconnectedAt = this.now().getTime();
8830
8945
  for (const [id, pending] of this.pending) {
8831
8946
  if (pending.runner === seat.name) {
@@ -9632,7 +9747,19 @@ function createApp(deps) {
9632
9747
  });
9633
9748
  api.get("/models/:cli", async (c) => {
9634
9749
  const cli = CliKindSchema.parse(c.req.param("cli"));
9635
- return c.json(models === void 0 ? [] : await models.list(cli).catch(() => []));
9750
+ return c.json(models === void 0 ? [] : await models.list(cli).then((listed) => listed.models, () => []));
9751
+ });
9752
+ api.get("/agents/:name/models", async (c) => {
9753
+ const agent = await board2.readAgent(c.req.param("name"));
9754
+ if (agent.cli === null) {
9755
+ throw new BoardError("INVALID_STATE", `${agent.name} takes no turns, so it has no models`);
9756
+ }
9757
+ if (models === void 0) {
9758
+ throw new RunnerAwayError("this server asks no runner for models");
9759
+ }
9760
+ const placed = new Map((await board2.listProjects()).map((project) => [project.slug, project.runner]));
9761
+ const prefer = [agent.homeRunner, ...agent.memberships.map((slug) => placed.get(slug))].filter((runner) => runner !== void 0);
9762
+ return c.json(await models.list(agent.cli, prefer));
9636
9763
  });
9637
9764
  api.put("/roles/:name", async (c) => {
9638
9765
  const charter = RoleCharterSchema.parse({
@@ -9915,26 +10042,42 @@ var EnrollmentDesk = class {
9915
10042
  // apps/server/dist/models.js
9916
10043
  var HOUR = 60 * 6e4;
9917
10044
  var ModelCatalog = class {
9918
- lister;
10045
+ runners;
9919
10046
  ttlMs;
9920
10047
  now;
9921
10048
  kept = /* @__PURE__ */ new Map();
9922
- constructor(lister, ttlMs = HOUR, now = Date.now) {
9923
- this.lister = lister;
10049
+ constructor(runners2, ttlMs = HOUR, now = Date.now) {
10050
+ this.runners = runners2;
9924
10051
  this.ttlMs = ttlMs;
9925
10052
  this.now = now;
10053
+ runners2.onRunnerChange((runner) => this.forget(runner));
9926
10054
  }
9927
- list(cli) {
9928
- const kept = this.kept.get(cli);
10055
+ async list(cli, prefer = []) {
10056
+ const runner = this.runners.modelRunner(cli, prefer);
10057
+ if (runner === null) {
10058
+ throw new RunnerAwayError(`no connected runner has ${cli}`);
10059
+ }
10060
+ const key = `${runner}/${cli}`;
10061
+ const kept = this.kept.get(key);
9929
10062
  if (kept !== void 0 && this.now() - kept.at < this.ttlMs) {
9930
- return kept.models;
10063
+ return { runner, cli, models: await kept.models };
9931
10064
  }
9932
- const models = this.lister(cli);
9933
- this.kept.set(cli, { at: this.now(), models });
10065
+ const models = this.runners.models(cli, runner);
10066
+ this.kept.set(key, { at: this.now(), models });
9934
10067
  models.catch(() => {
9935
- this.kept.delete(cli);
10068
+ if (this.kept.get(key)?.models === models) {
10069
+ this.kept.delete(key);
10070
+ }
9936
10071
  });
9937
- return models;
10072
+ return { runner, cli, models: await models };
10073
+ }
10074
+ /** Drops every list a runner gave. */
10075
+ forget(runner) {
10076
+ for (const key of this.kept.keys()) {
10077
+ if (key.startsWith(`${runner}/`)) {
10078
+ this.kept.delete(key);
10079
+ }
10080
+ }
9938
10081
  }
9939
10082
  };
9940
10083
 
@@ -10042,7 +10185,7 @@ var app = createApp({
10042
10185
  version: VERSION,
10043
10186
  turns,
10044
10187
  scheduler: view,
10045
- models: new ModelCatalog((cli) => runners.models(cli)),
10188
+ models: new ModelCatalog(runners),
10046
10189
  runners,
10047
10190
  webDir,
10048
10191
  signIn: { github: config.github, log },
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "codex",
9
9
  "multi-agent"
10
10
  ],
11
- "version": "0.3.6",
11
+ "version": "0.3.8",
12
12
  "license": "MIT",
13
13
  "homepage": "https://github.com/kr4t0n/stellaris",
14
14
  "repository": {