@kubitnodes/stellaris 0.3.6 → 0.3.7

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 (36) hide show
  1. package/dist/cli/index.js +92 -14
  2. package/dist/server/index.js +161 -30
  3. package/package.json +1 -1
  4. package/web/dist/assets/{BackLink-Cr1RWb4K.js → BackLink-BW0sb-M0.js} +1 -1
  5. package/web/dist/assets/{ChannelView-iK3ug1lV.js → ChannelView-DmTE9aUA.js} +1 -1
  6. package/web/dist/assets/CitizenView-lrA6vZDK.js +2 -0
  7. package/web/dist/assets/{CitizensView-CFrD6f7m.js → CitizensView-MRVffBnU.js} +1 -1
  8. package/web/dist/assets/KnowledgeView-C2N-x5yS.js +1 -0
  9. package/web/dist/assets/{Markdown-CYcTTxcR.js → Markdown-CS3NCze-.js} +1 -1
  10. package/web/dist/assets/{MessageItem-DDC_1Jqn.js → MessageItem-CL3XDi7l.js} +1 -1
  11. package/web/dist/assets/MetricsView-CR5u5k5n.js +1 -0
  12. package/web/dist/assets/{NeedsYouView-I7j_cnda.js → NeedsYouView-BwUvwyVt.js} +1 -1
  13. package/web/dist/assets/{Overview-DI7LPNEY.js → Overview-DxFOFviB.js} +1 -1
  14. package/web/dist/assets/{Pane-BNjb76U4.js → Pane-voa6GU0F.js} +1 -1
  15. package/web/dist/assets/{ProjectView-DYDcDsWg.js → ProjectView-BkNG_qdh.js} +1 -1
  16. package/web/dist/assets/{ProposalView-DrP3Cv9y.js → ProposalView-G89UmLF5.js} +1 -1
  17. package/web/dist/assets/{ProposalsView-90rufsQN.js → ProposalsView-CzpL-POB.js} +1 -1
  18. package/web/dist/assets/{RunnersView-iMbemuCe.js → RunnersView-SKNIDsjR.js} +1 -1
  19. package/web/dist/assets/{SocietyView-C9FdIFZY.js → SocietyView-CCFXEYZW.js} +1 -1
  20. package/web/dist/assets/{SubjectThread-Bw6wEJpH.js → SubjectThread-JQG2FxkA.js} +1 -1
  21. package/web/dist/assets/{TaskFileView-D0n7kzt2.js → TaskFileView-BKoGmHGz.js} +1 -1
  22. package/web/dist/assets/{TaskView-BZ6U_XpQ.js → TaskView-BIYqIFU4.js} +1 -1
  23. package/web/dist/assets/{TasksView-CtGF3i-n.js → TasksView-n2ssgQOL.js} +1 -1
  24. package/web/dist/assets/{ThreadCard-FPtAzvNM.js → ThreadCard-CzQXJXSd.js} +1 -1
  25. package/web/dist/assets/{ThreadForms-LFw2EF6r.js → ThreadForms-0LpUc2jJ.js} +1 -1
  26. package/web/dist/assets/{ThreadView-DnDx2SCv.js → ThreadView-_LgGISSH.js} +1 -1
  27. package/web/dist/assets/{citizen-Bm_r8vBY.js → citizen-DvJr6Ibu.js} +1 -1
  28. package/web/dist/assets/{format-Cj7NW9Y9.js → format-C7bqWt6N.js} +1 -1
  29. package/web/dist/assets/index-DM00hlle.js +23 -0
  30. package/web/dist/assets/{tasks-tFehkpRz.js → tasks-B4bx7Gg3.js} +1 -1
  31. package/web/dist/assets/{useStickyScroll-XrG6M-hT.js → useStickyScroll-DCsF4GYJ.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/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),
@@ -679,6 +697,7 @@ var BOARD_EVENT_TYPES = [
679
697
  "agent.joined",
680
698
  "agent.left",
681
699
  "knowledge.written",
700
+ "knowledge.removed",
682
701
  "skill.promoted",
683
702
  "runner.added",
684
703
  "user.signed_in",
@@ -1276,7 +1295,7 @@ var RunnerConfigSchema = z10.object({
1276
1295
  });
1277
1296
 
1278
1297
  // packages/shared/dist/version.js
1279
- var STELLARIS_VERSION = "0.3.6";
1298
+ var STELLARIS_VERSION = "0.3.7";
1280
1299
 
1281
1300
  // packages/board-core/dist/errors.js
1282
1301
  var BoardError = class extends Error {
@@ -2309,6 +2328,10 @@ var BoardPaths = class {
2309
2328
  pausedFile() {
2310
2329
  return path5.join(this.state(), "paused.json");
2311
2330
  }
2331
+ /** Removed knowledge topics, set aside outside the projection, by scope. */
2332
+ removedKnowledge(scope) {
2333
+ return path5.join(this.state(), "removed-knowledge", scope);
2334
+ }
2312
2335
  };
2313
2336
 
2314
2337
  // packages/board-core/dist/tokens.js
@@ -2402,6 +2425,7 @@ var OPS_CHANNEL_RETIRED = "ops-channel-retired";
2402
2425
  var DECISIONS_CHANNEL_RETIRED = "decisions-channel-retired";
2403
2426
  var THREAD_CONVERSATIONS = "thread-conversations";
2404
2427
  var ASKS_CHANNEL_OPENED = "asks-channel-opened";
2428
+ var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
2405
2429
  var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
2406
2430
  var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
2407
2431
  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 +2615,8 @@ var Board = class _Board {
2591
2615
  OPS_CHANNEL_RETIRED,
2592
2616
  DECISIONS_CHANNEL_RETIRED,
2593
2617
  THREAD_CONVERSATIONS,
2594
- ASKS_CHANNEL_OPENED
2618
+ ASKS_CHANNEL_OPENED,
2619
+ KNOWLEDGE_REMOVAL
2595
2620
  ].filter((name) => !applied.includes(name));
2596
2621
  if (pending.length === 0) {
2597
2622
  return;
@@ -2620,8 +2645,22 @@ var Board = class _Board {
2620
2645
  if (pending.includes(ASKS_CHANNEL_OPENED)) {
2621
2646
  await this.openAsksChannel();
2622
2647
  }
2648
+ if (pending.includes(KNOWLEDGE_REMOVAL)) {
2649
+ await this.grantKnowledgeRemoval();
2650
+ }
2623
2651
  await writeJson(file, { applied: [...applied, ...pending] });
2624
2652
  }
2653
+ /**
2654
+ * Whoever may write a scope's knowledge may remove it, so every charter granting
2655
+ * `write_knowledge` is granted `remove_knowledge` once; seed roles have it from their seed.
2656
+ */
2657
+ async grantKnowledgeRemoval() {
2658
+ for (const charter of await this.listRoles()) {
2659
+ if (charter.verbs.includes("write_knowledge") && !charter.verbs.includes("remove_knowledge")) {
2660
+ await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, "remove_knowledge"] }, { verbsAdded: ["remove_knowledge"] });
2661
+ }
2662
+ }
2663
+ }
2625
2664
  /**
2626
2665
  * A task sent back before tasks recorded it kept no trace outside the event log. A task still
2627
2666
  * short of the stage its latest `task.moved` came from is in that rework, and records it.
@@ -3853,16 +3892,7 @@ ${task2.body}`;
3853
3892
  const args = VerbInputs.write_knowledge.parse(input);
3854
3893
  await this.authorize(actor, "write_knowledge");
3855
3894
  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
- }
3895
+ await this.assertMayCurate(actor, args.project);
3866
3896
  const data = KnowledgeSchema.parse({
3867
3897
  topic: args.topic,
3868
3898
  project: args.project,
@@ -3882,6 +3912,52 @@ ${task2.body}`;
3882
3912
  return { ...data, body: args.body };
3883
3913
  });
3884
3914
  }
3915
+ /**
3916
+ * Retires a topic, by whoever may write it. Its file leaves the knowledge directory, so no turn,
3917
+ * search, or digest finds it again, and is set aside under `state/removed-knowledge`, outside
3918
+ * the projection, for the user to restore by hand. The note in the matching general channel
3919
+ * carries no mention, as a write's does.
3920
+ */
3921
+ async removeKnowledge(actor, input) {
3922
+ const args = VerbInputs.remove_knowledge.parse(input);
3923
+ await this.authorize(actor, "remove_knowledge");
3924
+ return this.mutex.run(async () => {
3925
+ await this.assertMayCurate(actor, args.project);
3926
+ const file = args.project === null ? this.paths.societyKnowledgeFile(args.topic) : this.paths.projectKnowledgeFile(args.project, args.topic);
3927
+ if (!await exists(file)) {
3928
+ throw new BoardError("NOT_FOUND", `${args.project ?? "the society"} has no knowledge topic ${args.topic}`);
3929
+ }
3930
+ const removed = RemovedKnowledgeSchema.parse({
3931
+ topic: args.topic,
3932
+ project: args.project,
3933
+ removedBy: actor.name,
3934
+ removedAt: this.now().toISOString()
3935
+ });
3936
+ const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
3937
+ await mkdir4(aside, { recursive: true });
3938
+ await rename3(file, path6.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
3939
+ await this.events.append("knowledge.removed", actor.name, {
3940
+ topic: args.topic,
3941
+ project: args.project
3942
+ });
3943
+ const where = args.project === null ? "general" : channelRef(args.project, "general");
3944
+ 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" : ""}.`);
3945
+ return removed;
3946
+ });
3947
+ }
3948
+ /** Society knowledge is the steward's and the user's; a project's, its members' too. */
3949
+ async assertMayCurate(actor, project2) {
3950
+ if (project2 === null) {
3951
+ if (!CURATING_ROLES.includes(actor.role)) {
3952
+ throw new BoardError("FORBIDDEN", "society knowledge is curated by the steward and the user");
3953
+ }
3954
+ return;
3955
+ }
3956
+ const active = await this.readActiveProject(project2);
3957
+ if (!CURATING_ROLES.includes(actor.role) && !active.members.includes(actor.name)) {
3958
+ throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${project2}`);
3959
+ }
3960
+ }
3885
3961
  async openThread(actor, input) {
3886
3962
  const args = VerbInputs.open_thread.parse(input);
3887
3963
  await this.authorize(actor, "open_thread");
@@ -4708,6 +4784,8 @@ ${rationale}`}`);
4708
4784
  return this.leaveProject(actor, VerbInputs.leave_project.parse(input));
4709
4785
  case "write_knowledge":
4710
4786
  return this.writeKnowledge(actor, VerbInputs.write_knowledge.parse(input));
4787
+ case "remove_knowledge":
4788
+ return this.removeKnowledge(actor, VerbInputs.remove_knowledge.parse(input));
4711
4789
  case "plan_task":
4712
4790
  return this.planTask(actor, VerbInputs.plan_task.parse(input));
4713
4791
  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),
@@ -663,6 +681,7 @@ var VERB_DESCRIPTIONS = {
663
681
  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
682
  leave_project: "Leave a project, or remove another citizen from one when your role allows it.",
665
683
  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.",
684
+ 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
685
  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
686
  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
687
  configure_project: "Set a project's completion effect: none, or merge to land each finished task's branch on the default branch.",
@@ -704,6 +723,7 @@ var BOARD_EVENT_TYPES = [
704
723
  "agent.joined",
705
724
  "agent.left",
706
725
  "knowledge.written",
726
+ "knowledge.removed",
707
727
  "skill.promoted",
708
728
  "runner.added",
709
729
  "user.signed_in",
@@ -1356,7 +1376,7 @@ var RunnerConfigSchema = z10.object({
1356
1376
  });
1357
1377
 
1358
1378
  // packages/shared/dist/version.js
1359
- var STELLARIS_VERSION = "0.3.6";
1379
+ var STELLARIS_VERSION = "0.3.7";
1360
1380
 
1361
1381
  // packages/board-core/dist/errors.js
1362
1382
  var BoardError = class extends Error {
@@ -2392,6 +2412,10 @@ var BoardPaths = class {
2392
2412
  pausedFile() {
2393
2413
  return path5.join(this.state(), "paused.json");
2394
2414
  }
2415
+ /** Removed knowledge topics, set aside outside the projection, by scope. */
2416
+ removedKnowledge(scope) {
2417
+ return path5.join(this.state(), "removed-knowledge", scope);
2418
+ }
2395
2419
  };
2396
2420
 
2397
2421
  // packages/board-core/dist/tokens.js
@@ -2485,6 +2509,7 @@ var OPS_CHANNEL_RETIRED = "ops-channel-retired";
2485
2509
  var DECISIONS_CHANNEL_RETIRED = "decisions-channel-retired";
2486
2510
  var THREAD_CONVERSATIONS = "thread-conversations";
2487
2511
  var ASKS_CHANNEL_OPENED = "asks-channel-opened";
2512
+ var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
2488
2513
  var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
2489
2514
  var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
2490
2515
  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 +2699,8 @@ var Board = class _Board {
2674
2699
  OPS_CHANNEL_RETIRED,
2675
2700
  DECISIONS_CHANNEL_RETIRED,
2676
2701
  THREAD_CONVERSATIONS,
2677
- ASKS_CHANNEL_OPENED
2702
+ ASKS_CHANNEL_OPENED,
2703
+ KNOWLEDGE_REMOVAL
2678
2704
  ].filter((name) => !applied.includes(name));
2679
2705
  if (pending.length === 0) {
2680
2706
  return;
@@ -2703,8 +2729,22 @@ var Board = class _Board {
2703
2729
  if (pending.includes(ASKS_CHANNEL_OPENED)) {
2704
2730
  await this.openAsksChannel();
2705
2731
  }
2732
+ if (pending.includes(KNOWLEDGE_REMOVAL)) {
2733
+ await this.grantKnowledgeRemoval();
2734
+ }
2706
2735
  await writeJson(file, { applied: [...applied, ...pending] });
2707
2736
  }
2737
+ /**
2738
+ * Whoever may write a scope's knowledge may remove it, so every charter granting
2739
+ * `write_knowledge` is granted `remove_knowledge` once; seed roles have it from their seed.
2740
+ */
2741
+ async grantKnowledgeRemoval() {
2742
+ for (const charter of await this.listRoles()) {
2743
+ if (charter.verbs.includes("write_knowledge") && !charter.verbs.includes("remove_knowledge")) {
2744
+ await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, "remove_knowledge"] }, { verbsAdded: ["remove_knowledge"] });
2745
+ }
2746
+ }
2747
+ }
2708
2748
  /**
2709
2749
  * A task sent back before tasks recorded it kept no trace outside the event log. A task still
2710
2750
  * short of the stage its latest `task.moved` came from is in that rework, and records it.
@@ -3936,16 +3976,7 @@ ${task.body}`;
3936
3976
  const args = VerbInputs.write_knowledge.parse(input);
3937
3977
  await this.authorize(actor, "write_knowledge");
3938
3978
  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
- }
3979
+ await this.assertMayCurate(actor, args.project);
3949
3980
  const data = KnowledgeSchema.parse({
3950
3981
  topic: args.topic,
3951
3982
  project: args.project,
@@ -3965,6 +3996,52 @@ ${task.body}`;
3965
3996
  return { ...data, body: args.body };
3966
3997
  });
3967
3998
  }
3999
+ /**
4000
+ * Retires a topic, by whoever may write it. Its file leaves the knowledge directory, so no turn,
4001
+ * search, or digest finds it again, and is set aside under `state/removed-knowledge`, outside
4002
+ * the projection, for the user to restore by hand. The note in the matching general channel
4003
+ * carries no mention, as a write's does.
4004
+ */
4005
+ async removeKnowledge(actor, input) {
4006
+ const args = VerbInputs.remove_knowledge.parse(input);
4007
+ await this.authorize(actor, "remove_knowledge");
4008
+ return this.mutex.run(async () => {
4009
+ await this.assertMayCurate(actor, args.project);
4010
+ const file = args.project === null ? this.paths.societyKnowledgeFile(args.topic) : this.paths.projectKnowledgeFile(args.project, args.topic);
4011
+ if (!await exists(file)) {
4012
+ throw new BoardError("NOT_FOUND", `${args.project ?? "the society"} has no knowledge topic ${args.topic}`);
4013
+ }
4014
+ const removed = RemovedKnowledgeSchema.parse({
4015
+ topic: args.topic,
4016
+ project: args.project,
4017
+ removedBy: actor.name,
4018
+ removedAt: this.now().toISOString()
4019
+ });
4020
+ const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
4021
+ await mkdir4(aside, { recursive: true });
4022
+ await rename3(file, path6.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
4023
+ await this.events.append("knowledge.removed", actor.name, {
4024
+ topic: args.topic,
4025
+ project: args.project
4026
+ });
4027
+ const where = args.project === null ? "general" : channelRef(args.project, "general");
4028
+ 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" : ""}.`);
4029
+ return removed;
4030
+ });
4031
+ }
4032
+ /** Society knowledge is the steward's and the user's; a project's, its members' too. */
4033
+ async assertMayCurate(actor, project) {
4034
+ if (project === null) {
4035
+ if (!CURATING_ROLES.includes(actor.role)) {
4036
+ throw new BoardError("FORBIDDEN", "society knowledge is curated by the steward and the user");
4037
+ }
4038
+ return;
4039
+ }
4040
+ const active = await this.readActiveProject(project);
4041
+ if (!CURATING_ROLES.includes(actor.role) && !active.members.includes(actor.name)) {
4042
+ throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${project}`);
4043
+ }
4044
+ }
3968
4045
  async openThread(actor, input) {
3969
4046
  const args = VerbInputs.open_thread.parse(input);
3970
4047
  await this.authorize(actor, "open_thread");
@@ -4791,6 +4868,8 @@ ${rationale}`}`);
4791
4868
  return this.leaveProject(actor, VerbInputs.leave_project.parse(input));
4792
4869
  case "write_knowledge":
4793
4870
  return this.writeKnowledge(actor, VerbInputs.write_knowledge.parse(input));
4871
+ case "remove_knowledge":
4872
+ return this.removeKnowledge(actor, VerbInputs.remove_knowledge.parse(input));
4794
4873
  case "plan_task":
4795
4874
  return this.planTask(actor, VerbInputs.plan_task.parse(input));
4796
4875
  case "advance_task":
@@ -8473,6 +8552,7 @@ var RunnerHub = class {
8473
8552
  pending = /* @__PURE__ */ new Map();
8474
8553
  /** Disconnects being recorded, which `close` waits for. */
8475
8554
  detaching = /* @__PURE__ */ new Set();
8555
+ runnerListeners = /* @__PURE__ */ new Set();
8476
8556
  constructor(options) {
8477
8557
  this.board = options.board;
8478
8558
  this.host = options.host;
@@ -8528,6 +8608,7 @@ var RunnerHub = class {
8528
8608
  clis: hello.clis,
8529
8609
  capabilities: hello.capabilities
8530
8610
  });
8611
+ this.runnerChanged(name);
8531
8612
  this.log.info({ runner: name, clis: hello.clis, slots: hello.slots }, "runner registered");
8532
8613
  return { name, version: this.version };
8533
8614
  }
@@ -8544,6 +8625,7 @@ var RunnerHub = class {
8544
8625
  seat.grace = null;
8545
8626
  }
8546
8627
  await this.board.markRunner(name, { status: "connected" });
8628
+ this.runnerChanged(name);
8547
8629
  return async () => {
8548
8630
  if (seat.send !== send) {
8549
8631
  return;
@@ -8816,16 +8898,37 @@ var RunnerHub = class {
8816
8898
  return changes;
8817
8899
  }
8818
8900
  /** 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}`);
8901
+ /**
8902
+ * The connected runner a model list for `cli` comes from: the first of `prefer` that has the CLI,
8903
+ * else the least busy that does; null when no connected runner has it.
8904
+ */
8905
+ modelRunner(cli, prefer = []) {
8906
+ const able = (seat) => seat !== void 0 && seat.send !== null && seat.hello.clis.includes(cli);
8907
+ const preferred = prefer.map((name) => this.seats.get(name)).find(able);
8908
+ return (preferred ?? this.byLoad().find(able))?.name ?? null;
8909
+ }
8910
+ /** The models `cli` lists on a runner, `modelRunner`'s choice when none is named. */
8911
+ async models(cli, runner) {
8912
+ const name = runner ?? this.modelRunner(cli);
8913
+ const seat = name === null ? void 0 : this.seats.get(name);
8914
+ if (seat === void 0 || seat.send === null || !seat.hello.clis.includes(cli)) {
8915
+ throw new RunnerAwayError(runner === void 0 ? `no connected runner has ${cli}` : `runner ${runner} is not connected with ${cli}`);
8823
8916
  }
8824
8917
  return ModelListSchema.parse(await this.request(seat, (request) => ({ type: "models", request, cli })));
8825
8918
  }
8919
+ /** Calls `listener` whenever a runner registers, connects, or goes away. */
8920
+ onRunnerChange(listener) {
8921
+ this.runnerListeners.add(listener);
8922
+ }
8923
+ runnerChanged(runner) {
8924
+ for (const listener of this.runnerListeners) {
8925
+ listener(runner);
8926
+ }
8927
+ }
8826
8928
  // -------------------------------------------------------------------------------------------
8827
8929
  async detach(seat) {
8828
8930
  seat.send = null;
8931
+ this.runnerChanged(seat.name);
8829
8932
  seat.disconnectedAt = this.now().getTime();
8830
8933
  for (const [id, pending] of this.pending) {
8831
8934
  if (pending.runner === seat.name) {
@@ -9632,7 +9735,19 @@ function createApp(deps) {
9632
9735
  });
9633
9736
  api.get("/models/:cli", async (c) => {
9634
9737
  const cli = CliKindSchema.parse(c.req.param("cli"));
9635
- return c.json(models === void 0 ? [] : await models.list(cli).catch(() => []));
9738
+ return c.json(models === void 0 ? [] : await models.list(cli).then((listed) => listed.models, () => []));
9739
+ });
9740
+ api.get("/agents/:name/models", async (c) => {
9741
+ const agent = await board2.readAgent(c.req.param("name"));
9742
+ if (agent.cli === null) {
9743
+ throw new BoardError("INVALID_STATE", `${agent.name} takes no turns, so it has no models`);
9744
+ }
9745
+ if (models === void 0) {
9746
+ throw new RunnerAwayError("this server asks no runner for models");
9747
+ }
9748
+ const placed = new Map((await board2.listProjects()).map((project) => [project.slug, project.runner]));
9749
+ const prefer = [agent.homeRunner, ...agent.memberships.map((slug) => placed.get(slug))].filter((runner) => runner !== void 0);
9750
+ return c.json(await models.list(agent.cli, prefer));
9636
9751
  });
9637
9752
  api.put("/roles/:name", async (c) => {
9638
9753
  const charter = RoleCharterSchema.parse({
@@ -9915,26 +10030,42 @@ var EnrollmentDesk = class {
9915
10030
  // apps/server/dist/models.js
9916
10031
  var HOUR = 60 * 6e4;
9917
10032
  var ModelCatalog = class {
9918
- lister;
10033
+ runners;
9919
10034
  ttlMs;
9920
10035
  now;
9921
10036
  kept = /* @__PURE__ */ new Map();
9922
- constructor(lister, ttlMs = HOUR, now = Date.now) {
9923
- this.lister = lister;
10037
+ constructor(runners2, ttlMs = HOUR, now = Date.now) {
10038
+ this.runners = runners2;
9924
10039
  this.ttlMs = ttlMs;
9925
10040
  this.now = now;
10041
+ runners2.onRunnerChange((runner) => this.forget(runner));
9926
10042
  }
9927
- list(cli) {
9928
- const kept = this.kept.get(cli);
10043
+ async list(cli, prefer = []) {
10044
+ const runner = this.runners.modelRunner(cli, prefer);
10045
+ if (runner === null) {
10046
+ throw new RunnerAwayError(`no connected runner has ${cli}`);
10047
+ }
10048
+ const key = `${runner}/${cli}`;
10049
+ const kept = this.kept.get(key);
9929
10050
  if (kept !== void 0 && this.now() - kept.at < this.ttlMs) {
9930
- return kept.models;
10051
+ return { runner, cli, models: await kept.models };
9931
10052
  }
9932
- const models = this.lister(cli);
9933
- this.kept.set(cli, { at: this.now(), models });
10053
+ const models = this.runners.models(cli, runner);
10054
+ this.kept.set(key, { at: this.now(), models });
9934
10055
  models.catch(() => {
9935
- this.kept.delete(cli);
10056
+ if (this.kept.get(key)?.models === models) {
10057
+ this.kept.delete(key);
10058
+ }
9936
10059
  });
9937
- return models;
10060
+ return { runner, cli, models: await models };
10061
+ }
10062
+ /** Drops every list a runner gave. */
10063
+ forget(runner) {
10064
+ for (const key of this.kept.keys()) {
10065
+ if (key.startsWith(`${runner}/`)) {
10066
+ this.kept.delete(key);
10067
+ }
10068
+ }
9938
10069
  }
9939
10070
  };
9940
10071
 
@@ -10042,7 +10173,7 @@ var app = createApp({
10042
10173
  version: VERSION,
10043
10174
  turns,
10044
10175
  scheduler: view,
10045
- models: new ModelCatalog((cli) => runners.models(cli)),
10176
+ models: new ModelCatalog(runners),
10046
10177
  runners,
10047
10178
  webDir,
10048
10179
  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.7",
12
12
  "license": "MIT",
13
13
  "homepage": "https://github.com/kr4t0n/stellaris",
14
14
  "repository": {
@@ -1 +1 @@
1
- import{b as e,ln as t,wt as n}from"./format-Cj7NW9Y9.js";import{L as r,S as i,n as a,r as o,t as s}from"./index-CthEgIzf.js";var c=t(),l=`grid size-7 shrink-0 place-items-center rounded-lg text-fg-tertiary hover:bg-surface-2/70 hover:text-fg-primary`;function u({fallback:t}){let u=n(),d=r({select:e=>e.location.state.__TSR_index}),f=o(),p=i(),m=e(),h=a(f,d);if(h===null)return t;let g=s(h,{entity:e=>p.get(e)?.title,project:e=>m.data?.find(t=>t.slug===e)?.name});return(0,c.jsx)(`button`,{type:`button`,onClick:()=>u.history.back(),"aria-label":`Back to ${g}`,title:`Back to ${g}`,className:l,children:`←`})}export{u as t};
1
+ import{b as e,ln as t,wt as n}from"./format-C7bqWt6N.js";import{L as r,S as i,n as a,r as o,t as s}from"./index-DM00hlle.js";var c=t(),l=`grid size-7 shrink-0 place-items-center rounded-lg text-fg-tertiary hover:bg-surface-2/70 hover:text-fg-primary`;function u({fallback:t}){let u=n(),d=r({select:e=>e.location.state.__TSR_index}),f=o(),p=i(),m=e(),h=a(f,d);if(h===null)return t;let g=s(h,{entity:e=>p.get(e)?.title,project:e=>m.data?.find(t=>t.slug===e)?.name});return(0,c.jsx)(`button`,{type:`button`,onClick:()=>u.history.back(),"aria-label":`Back to ${g}`,title:`Back to ${g}`,className:l,children:`←`})}export{u as t};
@@ -1 +1 @@
1
- import{D as e,F as t,Ht as n,R as r,b as i,h as a,ln as o,pn as s,un as c,y as l}from"./format-Cj7NW9Y9.js";import{I as u,V as d,c as f,i as p}from"./index-CthEgIzf.js";import{t as m}from"./MessageItem-DDC_1Jqn.js";import{n as h,t as g}from"./Pane-BNjb76U4.js";import{n as _}from"./ThreadCard-FPtAzvNM.js";import{i as v}from"./ThreadForms-LFw2EF6r.js";import{t as y}from"./useStickyScroll-XrG6M-hT.js";var b=s(c(),1),x=o();function S(){let{_splat:e=`general`}=d({from:`/c/$`});return(0,x.jsx)(C,{channel:e},e)}function C({channel:o}){let{api:s}=e(),c=n({queryKey:[`channel`,o],queryFn:()=>s.channel(o)}),d=t(),S=a(),C=i(),w=l(3e4),[T,E]=(0,b.useState)(!1),D=(0,b.useMemo)(()=>{let e=(c.data??[]).map(e=>({kind:`message`,at:e.ts,id:e.id,message:e})),t=(d.data??[]).filter(e=>e.channel===o).map(e=>({kind:`thread`,at:e.openedAt,id:e.id,thread:e}));return[...e,...t].toSorted((e,t)=>e.at.localeCompare(t.at)||e.id.localeCompare(t.id))},[c.data,d.data,o]),O=c.data?.at(-1)?.id??null;(0,b.useEffect)(()=>u(o,O),[o,O]);let{ref:k,onScroll:A}=y(),[j,M]=o.split(`/`),N=M??j??o,P=M===void 0?void 0:C.data?.find(e=>e.slug===j),F=M===void 0?`society`:P?.name??j,I=P?.archived!==void 0,L=D.filter(e=>e.kind===`thread`&&e.thread.state===`open`);return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(g,{title:`# ${N}`,subtitle:`${F} · ${c.data?.length??0} messages${L.length>0?` · ${L.length} open threads`:``}`,trailing:T||I?null:(0,x.jsx)(f,{onClick:()=>E(!0),children:`New thread`})}),T?(0,x.jsx)(v,{channel:o,onDone:()=>E(!1)}):null,(0,x.jsx)(`div`,{ref:k,onScroll:A,className:`flex-1 overflow-y-auto py-2`,children:c.error instanceof r&&c.error.status===404?(0,x.jsxs)(h,{children:[`There is no channel `,o,`.`]}):c.data===void 0?(0,x.jsx)(h,{children:`Reading the channel…`}):D.length===0?(0,x.jsx)(h,{children:`Nothing has been said here yet.`}):D.map(e=>e.kind===`message`?(0,x.jsx)(m,{message:e.message,members:S.data,now:w},e.id):(0,x.jsx)(_,{thread:e.thread,now:w},e.id))}),c.data===void 0?null:I?(0,x.jsxs)(`p`,{className:`border-t border-line px-4 py-3 text-meta`,children:[F,` is archived, so its channels are read-only.`]}):(0,x.jsx)(p,{target:{channel:o},placeholder:`Message #${N}`})]})}export{S as ChannelView};
1
+ import{D as e,F as t,Ht as n,R as r,b as i,g as a,ln as o,pn as s,un as c,y as l}from"./format-C7bqWt6N.js";import{I as u,V as d,c as f,i as p}from"./index-DM00hlle.js";import{t as m}from"./MessageItem-CL3XDi7l.js";import{n as h,t as g}from"./Pane-voa6GU0F.js";import{n as _}from"./ThreadCard-CzQXJXSd.js";import{i as v}from"./ThreadForms-0LpUc2jJ.js";import{t as y}from"./useStickyScroll-DCsF4GYJ.js";var b=s(c(),1),x=o();function S(){let{_splat:e=`general`}=d({from:`/c/$`});return(0,x.jsx)(C,{channel:e},e)}function C({channel:o}){let{api:s}=e(),c=n({queryKey:[`channel`,o],queryFn:()=>s.channel(o)}),d=t(),S=a(),C=i(),w=l(3e4),[T,E]=(0,b.useState)(!1),D=(0,b.useMemo)(()=>{let e=(c.data??[]).map(e=>({kind:`message`,at:e.ts,id:e.id,message:e})),t=(d.data??[]).filter(e=>e.channel===o).map(e=>({kind:`thread`,at:e.openedAt,id:e.id,thread:e}));return[...e,...t].toSorted((e,t)=>e.at.localeCompare(t.at)||e.id.localeCompare(t.id))},[c.data,d.data,o]),O=c.data?.at(-1)?.id??null;(0,b.useEffect)(()=>u(o,O),[o,O]);let{ref:k,onScroll:A}=y(),[j,M]=o.split(`/`),N=M??j??o,P=M===void 0?void 0:C.data?.find(e=>e.slug===j),F=M===void 0?`society`:P?.name??j,I=P?.archived!==void 0,L=D.filter(e=>e.kind===`thread`&&e.thread.state===`open`);return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(g,{title:`# ${N}`,subtitle:`${F} · ${c.data?.length??0} messages${L.length>0?` · ${L.length} open threads`:``}`,trailing:T||I?null:(0,x.jsx)(f,{onClick:()=>E(!0),children:`New thread`})}),T?(0,x.jsx)(v,{channel:o,onDone:()=>E(!1)}):null,(0,x.jsx)(`div`,{ref:k,onScroll:A,className:`flex-1 overflow-y-auto py-2`,children:c.error instanceof r&&c.error.status===404?(0,x.jsxs)(h,{children:[`There is no channel `,o,`.`]}):c.data===void 0?(0,x.jsx)(h,{children:`Reading the channel…`}):D.length===0?(0,x.jsx)(h,{children:`Nothing has been said here yet.`}):D.map(e=>e.kind===`message`?(0,x.jsx)(m,{message:e.message,members:S.data,now:w},e.id):(0,x.jsx)(_,{thread:e.thread,now:w},e.id))}),c.data===void 0?null:I?(0,x.jsxs)(`p`,{className:`border-t border-line px-4 py-3 text-meta`,children:[F,` is archived, so its channels are read-only.`]}):(0,x.jsx)(p,{target:{channel:o},placeholder:`Message #${N}`})]})}export{S as ChannelView};