@kubitnodes/stellaris 0.3.8 → 0.3.10

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 (35) hide show
  1. package/dist/cli/index.js +42 -20
  2. package/dist/server/index.js +117 -80
  3. package/package.json +1 -1
  4. package/web/dist/assets/{BackLink-DrYyD7II.js → BackLink-B3tz8OFm.js} +1 -1
  5. package/web/dist/assets/{ChannelView-B2sufhRs.js → ChannelView-Dkzr1zkF.js} +1 -1
  6. package/web/dist/assets/{CitizenView-B9u0X7ZY.js → CitizenView-BLgyEvYz.js} +1 -1
  7. package/web/dist/assets/{CitizensView-B60Z7gmH.js → CitizensView-DttP5FqR.js} +1 -1
  8. package/web/dist/assets/{KnowledgeView-Cppc2D7_.js → KnowledgeView-DFAC95aF.js} +1 -1
  9. package/web/dist/assets/{Markdown-B390nfRS.js → Markdown-4W4xjIq7.js} +1 -1
  10. package/web/dist/assets/{MessageItem-D54BS9OS.js → MessageItem-Bfpve1_Y.js} +1 -1
  11. package/web/dist/assets/{MetricsView-CsoRuj4g.js → MetricsView-Bp486U0v.js} +1 -1
  12. package/web/dist/assets/{NeedsYouView-BPjjyZRx.js → NeedsYouView-CJ4COcNn.js} +1 -1
  13. package/web/dist/assets/{Overview-BWEXLQaq.js → Overview-BnO1K52h.js} +1 -1
  14. package/web/dist/assets/{Pane-DvocjNie.js → Pane-xzIpwnBD.js} +1 -1
  15. package/web/dist/assets/ProjectView-w0mcF0KW.js +1 -0
  16. package/web/dist/assets/{ProposalView-cPi-EUXR.js → ProposalView-CyJCwUYK.js} +1 -1
  17. package/web/dist/assets/{ProposalsView-Bq7iCMtk.js → ProposalsView-XrraDhwt.js} +1 -1
  18. package/web/dist/assets/{RunnersView-BiJmbsoG.js → RunnersView-H3kCdSQB.js} +1 -1
  19. package/web/dist/assets/{SocietyView-6iPyxKze.js → SocietyView-Cf8ENDAw.js} +1 -1
  20. package/web/dist/assets/{SubjectThread-D2BHnecl.js → SubjectThread-ClMmgVVd.js} +1 -1
  21. package/web/dist/assets/{TaskFileView-DDs6OHWY.js → TaskFileView-CRwwL5K8.js} +1 -1
  22. package/web/dist/assets/{TaskView-3_2vs2fy.js → TaskView-A9IgTxQ0.js} +1 -1
  23. package/web/dist/assets/{TasksView-B7a0sWKu.js → TasksView-CnhTIldG.js} +1 -1
  24. package/web/dist/assets/{ThreadCard-9h03aKCB.js → ThreadCard-fvf8cY_l.js} +1 -1
  25. package/web/dist/assets/{ThreadForms-uORhDqAo.js → ThreadForms-D09Ykgj9.js} +1 -1
  26. package/web/dist/assets/{ThreadView-DVSe1HGK.js → ThreadView-BqNSssCf.js} +1 -1
  27. package/web/dist/assets/{citizen-CDTy3g7f.js → citizen-C27Wvimj.js} +1 -1
  28. package/web/dist/assets/{format-C6tBTRKZ.js → format-lz-5NQWT.js} +1 -1
  29. package/web/dist/assets/{index-CugUsWCm.js → index-CoKyPk79.js} +3 -3
  30. package/web/dist/assets/index-t2fXHB5Y.css +2 -0
  31. package/web/dist/assets/{tasks-Bh3GaY7V.js → tasks-DK1cnXqs.js} +1 -1
  32. package/web/dist/assets/{useStickyScroll-C6bu36lV.js → useStickyScroll-CfP3Yl_4.js} +1 -1
  33. package/web/dist/index.html +4 -4
  34. package/web/dist/assets/ProjectView-BIC7kE4L.js +0 -1
  35. package/web/dist/assets/index-D4oCtkuZ.css +0 -2
package/dist/cli/index.js CHANGED
@@ -168,6 +168,7 @@ var SocietySchema = z3.object({
168
168
  channels: z3.array(NameSchema)
169
169
  });
170
170
  var CompletionEffectSchema = z3.enum(["none", "merge"]);
171
+ var DefaultBranchSchema = z3.string().trim().min(1).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/, "a branch name is letters, digits, and . _ / -").refine((branch) => !branch.includes("..") && !branch.includes("//") && !branch.endsWith("/") && !branch.endsWith(".") && !branch.endsWith(".lock"), "not a branch name git takes").refine((branch) => !branch.startsWith("task/") && !branch.startsWith("agent/"), "task/ and agent/ hold the board's own branches");
171
172
  var StageIdSchema = z3.string().regex(/^s[1-9][0-9]*$/, "expected a stage id such as s1");
172
173
  var stageFields = {
173
174
  name: z3.string().trim().min(1).max(120),
@@ -632,7 +633,7 @@ var VerbInputs = {
632
633
  slug: NameSchema,
633
634
  name: z4.string().min(1).optional(),
634
635
  repo: z4.string().min(1).nullable().default(null),
635
- default_branch: z4.string().min(1).default("main"),
636
+ default_branch: DefaultBranchSchema.default("main"),
636
637
  on_done: CompletionEffectSchema.optional()
637
638
  }),
638
639
  join_project: z4.object({ project: NameSchema, agent: NameSchema.optional() }),
@@ -659,7 +660,9 @@ var VerbInputs = {
659
660
  project: NameSchema,
660
661
  on_done: CompletionEffectSchema.optional(),
661
662
  /** 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()
663
+ name: z4.string().trim().min(1).max(120).optional(),
664
+ /** The branch tasks start from and land on from now; tasks in play keep their branches. */
665
+ default_branch: DefaultBranchSchema.optional()
663
666
  }),
664
667
  archive_project: z4.object({ project: NameSchema, reason: z4.string().min(1) })
665
668
  };
@@ -863,11 +866,12 @@ var ProjectRepoSchema = z6.object({
863
866
  defaultBranch: z6.string().min(1)
864
867
  });
865
868
  var TurnWorkspaceSchema = z6.discriminatedUnion("kind", [
866
- z6.object({ kind: z6.literal("home") }),
869
+ z6.object({ kind: z6.literal("home"), thread: UlidSchema.optional() }),
867
870
  z6.object({
868
871
  kind: z6.literal("project"),
869
872
  repo: ProjectRepoSchema,
870
- branches: z6.array(z6.string().min(1))
873
+ branches: z6.array(z6.string().min(1)),
874
+ thread: UlidSchema.optional()
871
875
  }),
872
876
  z6.object({ kind: z6.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
873
877
  ]);
@@ -918,7 +922,7 @@ var TurnOutcomeSchema = z6.object({
918
922
  model: z6.string().nullable(),
919
923
  work: TurnWorkSchema.nullable()
920
924
  });
921
- var TurnAckSchema = z6.object({ dropWorktree: z6.boolean() });
925
+ var TurnAckSchema = z6.object({ dropWorkspace: z6.boolean() });
922
926
  var LandRequestSchema = z6.object({
923
927
  repo: ProjectRepoSchema,
924
928
  branch: z6.string().min(1)
@@ -1297,7 +1301,7 @@ var RunnerConfigSchema = z10.object({
1297
1301
  });
1298
1302
 
1299
1303
  // packages/shared/dist/version.js
1300
- var STELLARIS_VERSION = "0.3.8";
1304
+ var STELLARIS_VERSION = "0.3.10";
1301
1305
 
1302
1306
  // packages/board-core/dist/errors.js
1303
1307
  var BoardError = class extends Error {
@@ -1342,15 +1346,28 @@ async function writeJson(file, value) {
1342
1346
  await writeFileAtomic(file, `${JSON.stringify(value, null, 2)}
1343
1347
  `);
1344
1348
  }
1349
+ function parseMatter(raw) {
1350
+ return matter(raw, {});
1351
+ }
1345
1352
  async function readMarkdown(file, schema) {
1346
1353
  const raw = await readFile(file, "utf8");
1347
- const parsed = matter(raw);
1354
+ const parsed = parseMatter(raw);
1348
1355
  return { data: schema.parse(parsed.data), body: parsed.content.replace(/^\n/, "") };
1349
1356
  }
1357
+ async function readLooseMarkdown(file) {
1358
+ const raw = await readFile(file, "utf8");
1359
+ try {
1360
+ const parsed = parseMatter(raw);
1361
+ return { data: parsed.data, body: parsed.content.replace(/^\n/, ""), error: null };
1362
+ } catch (error) {
1363
+ const reason = (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
1364
+ return { data: {}, body: raw, error: reason.replace(/:$/, "") };
1365
+ }
1366
+ }
1350
1367
  async function writeMarkdown(file, data, body) {
1351
1368
  const normalized = body.endsWith("\n") || body.length === 0 ? body : `${body}
1352
1369
  `;
1353
- await writeFileAtomic(file, matter.stringify(normalized, data));
1370
+ await writeFileAtomic(file, matter.stringify({ content: normalized }, data, {}));
1354
1371
  }
1355
1372
  function isMissing(error) {
1356
1373
  return typeof error === "object" && error !== null && error.code === "ENOENT";
@@ -3202,7 +3219,7 @@ ${charter.purpose}
3202
3219
  }
3203
3220
  async readProfile(name) {
3204
3221
  const file = this.paths.agentProfile(name);
3205
- return await exists(file) ? (await readMarkdown(file, z12.record(z12.string(), z12.unknown()))).body : "";
3222
+ return await exists(file) ? (await readLooseMarkdown(file)).body : "";
3206
3223
  }
3207
3224
  /** The society's norms, the one knowledge topic every turn loads. Empty until the steward writes it. */
3208
3225
  async readSocietyNorms() {
@@ -3249,10 +3266,11 @@ ${charter.purpose}
3249
3266
  if (!name.success) {
3250
3267
  continue;
3251
3268
  }
3252
- const doc = await readMarkdown(file, z12.record(z12.string(), z12.unknown()));
3269
+ const doc = await readLooseMarkdown(file);
3253
3270
  skills.push({
3254
3271
  name: name.data,
3255
- summary: skillSummary(doc.data, doc.body),
3272
+ // The index is where its writer sees that the file needs fixing.
3273
+ summary: doc.error === null ? skillSummary(doc.data, doc.body) : `its frontmatter does not parse (${doc.error}), so fix the file`,
3256
3274
  scope,
3257
3275
  path: file
3258
3276
  });
@@ -4628,29 +4646,32 @@ ${rationale}`}`);
4628
4646
  ...args.on_done === void 0 ? {} : { onDone: args.on_done }
4629
4647
  }));
4630
4648
  }
4631
- /** Sets a project's completion effect. */
4632
4649
  /**
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.
4650
+ * Sets a project's completion effect, display name, or default branch, any of them at once. The
4651
+ * slug stays: addresses, channel references, branches, and runners' directories all carry it. A
4652
+ * new default branch the project's repository lacks is made by its runner before the next turn.
4635
4653
  */
4636
4654
  async configureProject(actor, input) {
4637
4655
  const args = VerbInputs.configure_project.parse(input);
4638
4656
  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");
4657
+ if (args.on_done === void 0 && args.name === void 0 && args.default_branch === void 0) {
4658
+ throw new BoardError("VALIDATION", "configure_project needs on_done, name, or default_branch");
4641
4659
  }
4642
4660
  return this.mutex.run(async () => {
4643
4661
  const previous = await this.readActiveProject(args.project);
4644
4662
  const project2 = await this.updateProject(args.project, (current) => ({
4645
4663
  ...current,
4646
4664
  ...args.on_done === void 0 ? {} : { onDone: args.on_done },
4647
- ...args.name === void 0 ? {} : { name: args.name }
4665
+ ...args.name === void 0 ? {} : { name: args.name },
4666
+ ...args.default_branch === void 0 ? {} : { defaultBranch: args.default_branch }
4648
4667
  }));
4649
4668
  await this.events.append("project.configured", actor.name, {
4650
4669
  slug: project2.slug,
4651
4670
  onDone: project2.onDone,
4652
4671
  name: project2.name,
4653
- ...project2.name === previous.name ? {} : { previousName: previous.name }
4672
+ defaultBranch: project2.defaultBranch,
4673
+ ...project2.name === previous.name ? {} : { previousName: previous.name },
4674
+ ...project2.defaultBranch === previous.defaultBranch ? {} : { previousDefaultBranch: previous.defaultBranch }
4654
4675
  });
4655
4676
  return project2;
4656
4677
  });
@@ -4911,7 +4932,7 @@ ${rationale}`}`);
4911
4932
  }
4912
4933
  async readMemoryCore(agent2) {
4913
4934
  const file = this.paths.agentMemoryCore(agent2);
4914
- return await exists(file) ? (await readMarkdown(file, z12.record(z12.string(), z12.unknown()))).body : "";
4935
+ return await exists(file) ? (await readLooseMarkdown(file)).body : "";
4915
4936
  }
4916
4937
  /**
4917
4938
  * Moves a conversation's cursor forward to `cursor` once a turn there has delivered up to it. It
@@ -5126,7 +5147,8 @@ ${rationale}`}`);
5126
5147
  if (!await exists(file)) {
5127
5148
  return { data: { project: slug }, body: "" };
5128
5149
  }
5129
- return readMarkdown(file, z12.record(z12.string(), z12.unknown()));
5150
+ const { data, body } = await readLooseMarkdown(file);
5151
+ return { data, body };
5130
5152
  }
5131
5153
  async listRunners() {
5132
5154
  const runners = [];
@@ -168,6 +168,7 @@ var SocietySchema = z3.object({
168
168
  channels: z3.array(NameSchema)
169
169
  });
170
170
  var CompletionEffectSchema = z3.enum(["none", "merge"]);
171
+ var DefaultBranchSchema = z3.string().trim().min(1).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/, "a branch name is letters, digits, and . _ / -").refine((branch) => !branch.includes("..") && !branch.includes("//") && !branch.endsWith("/") && !branch.endsWith(".") && !branch.endsWith(".lock"), "not a branch name git takes").refine((branch) => !branch.startsWith("task/") && !branch.startsWith("agent/"), "task/ and agent/ hold the board's own branches");
171
172
  var StageIdSchema = z3.string().regex(/^s[1-9][0-9]*$/, "expected a stage id such as s1");
172
173
  var stageFields = {
173
174
  name: z3.string().trim().min(1).max(120),
@@ -632,7 +633,7 @@ var VerbInputs = {
632
633
  slug: NameSchema,
633
634
  name: z4.string().min(1).optional(),
634
635
  repo: z4.string().min(1).nullable().default(null),
635
- default_branch: z4.string().min(1).default("main"),
636
+ default_branch: DefaultBranchSchema.default("main"),
636
637
  on_done: CompletionEffectSchema.optional()
637
638
  }),
638
639
  join_project: z4.object({ project: NameSchema, agent: NameSchema.optional() }),
@@ -659,7 +660,9 @@ var VerbInputs = {
659
660
  project: NameSchema,
660
661
  on_done: CompletionEffectSchema.optional(),
661
662
  /** 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()
663
+ name: z4.string().trim().min(1).max(120).optional(),
664
+ /** The branch tasks start from and land on from now; tasks in play keep their branches. */
665
+ default_branch: DefaultBranchSchema.optional()
663
666
  }),
664
667
  archive_project: z4.object({ project: NameSchema, reason: z4.string().min(1) })
665
668
  };
@@ -686,7 +689,7 @@ var VERB_DESCRIPTIONS = {
686
689
  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.",
687
690
  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.",
688
691
  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.",
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.",
692
+ 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 its default branch, any of them at once. The slug never changes.",
690
693
  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."
691
694
  };
692
695
 
@@ -896,11 +899,12 @@ var ProjectRepoSchema = z6.object({
896
899
  defaultBranch: z6.string().min(1)
897
900
  });
898
901
  var TurnWorkspaceSchema = z6.discriminatedUnion("kind", [
899
- z6.object({ kind: z6.literal("home") }),
902
+ z6.object({ kind: z6.literal("home"), thread: UlidSchema.optional() }),
900
903
  z6.object({
901
904
  kind: z6.literal("project"),
902
905
  repo: ProjectRepoSchema,
903
- branches: z6.array(z6.string().min(1))
906
+ branches: z6.array(z6.string().min(1)),
907
+ thread: UlidSchema.optional()
904
908
  }),
905
909
  z6.object({ kind: z6.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
906
910
  ]);
@@ -951,7 +955,7 @@ var TurnOutcomeSchema = z6.object({
951
955
  model: z6.string().nullable(),
952
956
  work: TurnWorkSchema.nullable()
953
957
  });
954
- var TurnAckSchema = z6.object({ dropWorktree: z6.boolean() });
958
+ var TurnAckSchema = z6.object({ dropWorkspace: z6.boolean() });
955
959
  var LandRequestSchema = z6.object({
956
960
  repo: ProjectRepoSchema,
957
961
  branch: z6.string().min(1)
@@ -1378,7 +1382,7 @@ var RunnerConfigSchema = z10.object({
1378
1382
  });
1379
1383
 
1380
1384
  // packages/shared/dist/version.js
1381
- var STELLARIS_VERSION = "0.3.8";
1385
+ var STELLARIS_VERSION = "0.3.10";
1382
1386
 
1383
1387
  // packages/board-core/dist/errors.js
1384
1388
  var BoardError = class extends Error {
@@ -1426,15 +1430,28 @@ async function writeJson(file, value) {
1426
1430
  await writeFileAtomic(file, `${JSON.stringify(value, null, 2)}
1427
1431
  `);
1428
1432
  }
1433
+ function parseMatter(raw) {
1434
+ return matter(raw, {});
1435
+ }
1429
1436
  async function readMarkdown(file, schema) {
1430
1437
  const raw = await readFile(file, "utf8");
1431
- const parsed = matter(raw);
1438
+ const parsed = parseMatter(raw);
1432
1439
  return { data: schema.parse(parsed.data), body: parsed.content.replace(/^\n/, "") };
1433
1440
  }
1441
+ async function readLooseMarkdown(file) {
1442
+ const raw = await readFile(file, "utf8");
1443
+ try {
1444
+ const parsed = parseMatter(raw);
1445
+ return { data: parsed.data, body: parsed.content.replace(/^\n/, ""), error: null };
1446
+ } catch (error) {
1447
+ const reason = (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
1448
+ return { data: {}, body: raw, error: reason.replace(/:$/, "") };
1449
+ }
1450
+ }
1434
1451
  async function writeMarkdown(file, data, body) {
1435
1452
  const normalized2 = body.endsWith("\n") || body.length === 0 ? body : `${body}
1436
1453
  `;
1437
- await writeFileAtomic(file, matter.stringify(normalized2, data));
1454
+ await writeFileAtomic(file, matter.stringify({ content: normalized2 }, data, {}));
1438
1455
  }
1439
1456
  function isMissing(error) {
1440
1457
  return typeof error === "object" && error !== null && error.code === "ENOENT";
@@ -3286,7 +3303,7 @@ ${charter.purpose}
3286
3303
  }
3287
3304
  async readProfile(name) {
3288
3305
  const file = this.paths.agentProfile(name);
3289
- return await exists(file) ? (await readMarkdown(file, z12.record(z12.string(), z12.unknown()))).body : "";
3306
+ return await exists(file) ? (await readLooseMarkdown(file)).body : "";
3290
3307
  }
3291
3308
  /** The society's norms, the one knowledge topic every turn loads. Empty until the steward writes it. */
3292
3309
  async readSocietyNorms() {
@@ -3333,10 +3350,11 @@ ${charter.purpose}
3333
3350
  if (!name.success) {
3334
3351
  continue;
3335
3352
  }
3336
- const doc = await readMarkdown(file, z12.record(z12.string(), z12.unknown()));
3353
+ const doc = await readLooseMarkdown(file);
3337
3354
  skills.push({
3338
3355
  name: name.data,
3339
- summary: skillSummary(doc.data, doc.body),
3356
+ // The index is where its writer sees that the file needs fixing.
3357
+ summary: doc.error === null ? skillSummary(doc.data, doc.body) : `its frontmatter does not parse (${doc.error}), so fix the file`,
3340
3358
  scope,
3341
3359
  path: file
3342
3360
  });
@@ -4712,29 +4730,32 @@ ${rationale}`}`);
4712
4730
  ...args.on_done === void 0 ? {} : { onDone: args.on_done }
4713
4731
  }));
4714
4732
  }
4715
- /** Sets a project's completion effect. */
4716
4733
  /**
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.
4734
+ * Sets a project's completion effect, display name, or default branch, any of them at once. The
4735
+ * slug stays: addresses, channel references, branches, and runners' directories all carry it. A
4736
+ * new default branch the project's repository lacks is made by its runner before the next turn.
4719
4737
  */
4720
4738
  async configureProject(actor, input) {
4721
4739
  const args = VerbInputs.configure_project.parse(input);
4722
4740
  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");
4741
+ if (args.on_done === void 0 && args.name === void 0 && args.default_branch === void 0) {
4742
+ throw new BoardError("VALIDATION", "configure_project needs on_done, name, or default_branch");
4725
4743
  }
4726
4744
  return this.mutex.run(async () => {
4727
4745
  const previous = await this.readActiveProject(args.project);
4728
4746
  const project = await this.updateProject(args.project, (current) => ({
4729
4747
  ...current,
4730
4748
  ...args.on_done === void 0 ? {} : { onDone: args.on_done },
4731
- ...args.name === void 0 ? {} : { name: args.name }
4749
+ ...args.name === void 0 ? {} : { name: args.name },
4750
+ ...args.default_branch === void 0 ? {} : { defaultBranch: args.default_branch }
4732
4751
  }));
4733
4752
  await this.events.append("project.configured", actor.name, {
4734
4753
  slug: project.slug,
4735
4754
  onDone: project.onDone,
4736
4755
  name: project.name,
4737
- ...project.name === previous.name ? {} : { previousName: previous.name }
4756
+ defaultBranch: project.defaultBranch,
4757
+ ...project.name === previous.name ? {} : { previousName: previous.name },
4758
+ ...project.defaultBranch === previous.defaultBranch ? {} : { previousDefaultBranch: previous.defaultBranch }
4738
4759
  });
4739
4760
  return project;
4740
4761
  });
@@ -4995,7 +5016,7 @@ ${rationale}`}`);
4995
5016
  }
4996
5017
  async readMemoryCore(agent) {
4997
5018
  const file = this.paths.agentMemoryCore(agent);
4998
- return await exists(file) ? (await readMarkdown(file, z12.record(z12.string(), z12.unknown()))).body : "";
5019
+ return await exists(file) ? (await readLooseMarkdown(file)).body : "";
4999
5020
  }
5000
5021
  /**
5001
5022
  * Moves a conversation's cursor forward to `cursor` once a turn there has delivered up to it. It
@@ -5210,7 +5231,8 @@ ${rationale}`}`);
5210
5231
  if (!await exists(file)) {
5211
5232
  return { data: { project: slug }, body: "" };
5212
5233
  }
5213
- return readMarkdown(file, z12.record(z12.string(), z12.unknown()));
5234
+ const { data, body } = await readLooseMarkdown(file);
5235
+ return { data, body };
5214
5236
  }
5215
5237
  async listRunners() {
5216
5238
  const runners2 = [];
@@ -6497,7 +6519,10 @@ var Scheduler = class {
6497
6519
  now;
6498
6520
  log;
6499
6521
  pending = /* @__PURE__ */ new Map();
6500
- /** Turns in flight by session key, each with the lane it holds (see `laneOf`). */
6522
+ /**
6523
+ * Turns in flight by session key. A conversation runs one turn at a time, and different
6524
+ * conversations side by side, since each thread's works in a place of its own.
6525
+ */
6501
6526
  running = /* @__PURE__ */ new Map();
6502
6527
  /** Steers under way, by session key; a pass does not wait for a runner to answer one. */
6503
6528
  steering = /* @__PURE__ */ new Map();
@@ -6554,7 +6579,7 @@ var Scheduler = class {
6554
6579
  }
6555
6580
  await this.passing?.catch(() => void 0);
6556
6581
  await Promise.allSettled([
6557
- ...[...this.running.values()].map((turn) => turn.promise),
6582
+ ...this.running.values(),
6558
6583
  ...this.completing,
6559
6584
  ...this.steering.values()
6560
6585
  ]);
@@ -6604,7 +6629,7 @@ var Scheduler = class {
6604
6629
  async drain() {
6605
6630
  await Promise.allSettled([
6606
6631
  ...this.steering.values(),
6607
- ...[...this.running.values()].map((turn) => turn.promise),
6632
+ ...this.running.values(),
6608
6633
  ...this.completing
6609
6634
  ]);
6610
6635
  }
@@ -7222,7 +7247,7 @@ var Scheduler = class {
7222
7247
  continue;
7223
7248
  }
7224
7249
  const key = sessionKey(agent.name, latest.scope);
7225
- if (this.pending.has(key) || this.laneBusy(key)) {
7250
+ if (this.pending.has(key) || this.running.has(key)) {
7226
7251
  continue;
7227
7252
  }
7228
7253
  this.state.lastReflection[agent.name] = iso(now);
@@ -7520,22 +7545,6 @@ var Scheduler = class {
7520
7545
  this.state.costSinceReport = 0;
7521
7546
  this.state.turnsSinceReport = 0;
7522
7547
  }
7523
- /**
7524
- * The lane a turn runs in: a task's conversation has its own, with a worktree of its own; a
7525
- * member's home conversation and its proposal and topic threads in a scope share the scope's
7526
- * worktree, and so one lane, one turn at a time.
7527
- */
7528
- laneOf(dispatch) {
7529
- return dispatch.thread?.task === true ? sessionKey(dispatch.agent, dispatch.project, dispatch.thread.id) : sessionKey(dispatch.agent, dispatch.project);
7530
- }
7531
- laneBusy(lane) {
7532
- for (const turn of this.running.values()) {
7533
- if (turn.lane === lane) {
7534
- return true;
7535
- }
7536
- }
7537
- return false;
7538
- }
7539
7548
  /**
7540
7549
  * Delivers ready message wakes into the turn already running in their conversation, rather than
7541
7550
  * keeping them for a turn of their own. Each stays queued until that turn ends and is checked
@@ -7586,10 +7595,6 @@ var Scheduler = class {
7586
7595
  if (this.running.size >= this.concurrency) {
7587
7596
  break;
7588
7597
  }
7589
- const lane = this.laneOf(item.dispatch);
7590
- if (this.laneBusy(lane)) {
7591
- continue;
7592
- }
7593
7598
  this.pending.delete(key);
7594
7599
  if (await this.alreadyRead(item)) {
7595
7600
  this.log.info({ agent: item.dispatch.agent, project: item.dispatch.project }, "dropping message wake, its conversation has read the message");
@@ -7630,7 +7635,7 @@ var Scheduler = class {
7630
7635
  this.running.delete(key);
7631
7636
  }
7632
7637
  })();
7633
- this.running.set(key, { lane, promise });
7638
+ this.running.set(key, promise);
7634
7639
  }
7635
7640
  }
7636
7641
  /** Starts each queued completion once the turn that finished the task's last stage has ended. */
@@ -7663,7 +7668,7 @@ import { randomUUID } from "node:crypto";
7663
7668
  var TURN_CONTRACT = [
7664
7669
  "- Every turn belongs to one conversation, named at the top of your prompt: your home conversation in a project or the society, for its channels and for work tied to no thread, or the conversation of one thread you take part in, a task's, a proposal's, or a topic's. Each has its own session and its own turns, which may run while this one does. The prompt carries this conversation only: in a thread, its new messages, or the whole thread when the conversation is new, and in a task's thread the task and its stage; at home, the channel messages that concern you. Read anything else you need with get_task, search, and the board's files, and leave other threads' and other projects' work to their own turns.",
7665
7670
  "- Act through the board tools (the `board` MCP server). Holding a stage is a lease; every turn that touches the task renews it.",
7666
- "- Work only inside your worktree. In a task's conversation it is a worktree of its own on the task's branch `task/<id>`: commit your work there. After each turn the runner commits what you left on the branch and lets it go for the next holder. Your home worktree is on your own branch, `agent/<name>`, for work tied to no task.",
7671
+ "- Work only inside your worktree. In a task's conversation it is a worktree of its own on the task's branch `task/<id>`: commit your work there. After each turn the runner commits what you left on the branch and lets it go for the next holder. Your home worktree is on your own branch, `agent/<name>`, for work tied to no task. In a proposal's or a topic's conversation it is a worktree of its own, detached at your own branch's tip, which goes when the thread closes, so work that should last goes on a task's branch.",
7667
7672
  "- Posts that would wake this conversation may arrive while you work, delivered into the turn under a heading that says so and formatted like the digest. They are part of this turn: act on them now, after the step in hand, or not at all, as with the digest. The user may also stop a turn; the next turn is told.",
7668
7673
  "- Silence is allowed. If the digest needs no reply, post nothing.",
7669
7674
  "- An @mention wakes the citizen named, and every wake costs a turn. Address someone with @ only when you need them to act; when you merely refer to citizens, write their names plainly. @user wakes nobody, since the user takes no turns; it is what puts a question before the user, so use it only when you need the user to answer or decide.",
@@ -7692,7 +7697,7 @@ var GOVERNANCE = [
7692
7697
  ' - 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',
7693
7698
  ' - reallocation: {"description"}',
7694
7699
  ` - skill: {"name", "summary", "body"}: the SKILL.md text; approval publishes it under society/skills, where every citizen's skills index lists it`,
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.",
7700
+ "- Projects and membership: a project is a lasting area of work, not one request; `create_project` opens one with its general channel (front desk and user), and `configure_project` sets its completion effect, its display name, or its default branch, never its slug (user, steward, concierge). A 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.",
7696
7701
  "- 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.",
7697
7702
  "- 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."
7698
7703
  ].join("\n");
@@ -7728,7 +7733,7 @@ function renderInstructions(input) {
7728
7733
  if (norms.length > 0) {
7729
7734
  sections.push("", "## Society norms", "", norms);
7730
7735
  }
7731
- sections.push("", "## Core memory", "", memory.length === 0 ? "(empty)" : memory, "", "## Skills", "", ...renderSkillsIndex(input.skills ?? []), "", "## Where things are", "", `- Your home directory: ${input.homeDir}. memory/core.md is loaded every turn; memory/<topic>.md is your archive; skills/<name>/SKILL.md are your skills; projects/<slug>/notes.md are your notes. You may read and write all of it, and the board's search covers your archive and skills for you alone. The runner keeps it in git and shares it with the board after every turn, so whatever machine your next turn runs on has it; leave git there to the runner. Files over ${HOME_FILE_LIMIT_BYTES / (1024 * 1024)} MB are not kept.`, `- ${HOME_SCRATCH}/ in your home is your working folder in turns outside any project: it is never kept or shared, so put downloads, environments, and other working files there, and keep the rest of your home for what you mean to remember.`, `- The board projection: ${input.boardDir} (read-only markdown: society and project channels, tasks, threads; shared knowledge under projects/<slug>/knowledge/ and society/knowledge/; the society's skills under society/skills/). Search it with your file tools; act through the board tools.`, "", "## Turn contract", "", TURN_CONTRACT, "", "## Planning", "", PLANNING, "", "## Governance", "", GOVERNANCE);
7736
+ sections.push("", "## Core memory", "", memory.length === 0 ? "(empty)" : memory, "", "## Skills", "", ...renderSkillsIndex(input.skills ?? []), "", "## Where things are", "", `- Your home directory: ${input.homeDir}. memory/core.md is loaded every turn; memory/<topic>.md is your archive; skills/<name>/SKILL.md are your skills; projects/<slug>/notes.md are your notes. You may read and write all of it, and the board's search covers your archive and skills for you alone. The runner keeps it in git and shares it with the board after every turn, so whatever machine your next turn runs on has it; leave git there to the runner. Files over ${HOME_FILE_LIMIT_BYTES / (1024 * 1024)} MB are not kept.`, `- ${HOME_SCRATCH}/ in your home is your working folder in turns outside any project, and a thread's conversation there works in a folder of its own inside it, which goes when the thread closes: none of it is kept or shared, so put downloads, environments, and other working files there, and keep the rest of your home for what you mean to remember.`, `- The board projection: ${input.boardDir} (read-only markdown: society and project channels, tasks, threads; shared knowledge under projects/<slug>/knowledge/ and society/knowledge/; the society's skills under society/skills/). Search it with your file tools; act through the board tools.`, "", "## Turn contract", "", TURN_CONTRACT, "", "## Planning", "", PLANNING, "", "## Governance", "", GOVERNANCE);
7732
7737
  if (input.onboarding !== void 0) {
7733
7738
  sections.push("", "## First turn", "", renderOnboardingPreamble(input.onboarding));
7734
7739
  }
@@ -8060,7 +8065,7 @@ var TurnHost = class {
8060
8065
  const societyScope = dispatch.project === SOCIETY_SCOPE;
8061
8066
  const taskId = thread?.task === true ? thread.id : void 0;
8062
8067
  const project = societyScope ? null : await this.board.readProject(dispatch.project);
8063
- const workspace = await this.workspaceOf(project, taskId);
8068
+ const workspace = await this.workspaceOf(project, thread);
8064
8069
  const sessions = await this.board.readSessions(agent.name, dispatch.project, thread?.id);
8065
8070
  const recorded = sessions.runner === runner.name ? sessions[agent.cli] : void 0;
8066
8071
  const newSession = recorded === void 0;
@@ -8172,7 +8177,8 @@ var TurnHost = class {
8172
8177
  void this.board.heldClaims(agent.name).then((claims) => this.renewLeases(actor, claims.filter((each) => each.project === dispatch.project).map((each) => each.id))).catch(() => void 0);
8173
8178
  }, LEASE_RENEW_MS);
8174
8179
  keepLeases.unref?.();
8175
- const { promise: ended, resolve: done } = Promise.withResolvers();
8180
+ const { promise: ended, resolve: done, reject: fail } = Promise.withResolvers();
8181
+ ended.catch(() => void 0);
8176
8182
  this.open.set(record.id, {
8177
8183
  job,
8178
8184
  runner: runner.name,
@@ -8183,14 +8189,14 @@ var TurnHost = class {
8183
8189
  outstanding: null,
8184
8190
  stopRequestedBy: null,
8185
8191
  held: held.map((each) => each.id),
8186
- taskId,
8187
8192
  costSoFarUsd,
8188
8193
  configuredModel: agent.model ?? null,
8189
8194
  transcript: [],
8190
8195
  coldToken: residentKey === null ? token : null,
8191
8196
  residentKey: residentKey === null ? null : `${runner.name}|${residentKey}`,
8192
8197
  keepLeases,
8193
- done
8198
+ done,
8199
+ fail
8194
8200
  });
8195
8201
  this.log.info({
8196
8202
  agent: agent.name,
@@ -8329,7 +8335,7 @@ var TurnHost = class {
8329
8335
  }
8330
8336
  /**
8331
8337
  * Records a turn's end from its outcome. Returns null for a turn the host does not know, else
8332
- * whether the task the turn worked on has ended, so the runner may drop its worktree.
8338
+ * whether the task or thread the turn worked in has ended, so the runner may drop its workspace.
8333
8339
  */
8334
8340
  async closeTurn(turnId, outcome) {
8335
8341
  const turn = this.open.get(turnId);
@@ -8342,9 +8348,6 @@ var TurnHost = class {
8342
8348
  this.board.revokeTurnToken(turn.coldToken);
8343
8349
  }
8344
8350
  const { job } = turn;
8345
- if (outcome.session !== job.session) {
8346
- await this.board.writeSession(job.agent, job.scope, job.cli, outcome.session, turn.runner, job.thread);
8347
- }
8348
8351
  const finished = {
8349
8352
  ...turn.record,
8350
8353
  session: outcome.session,
@@ -8360,17 +8363,29 @@ var TurnHost = class {
8360
8363
  ...outcome.work === null ? {} : { work: outcome.work },
8361
8364
  ...outcome.exitReason === "stopped" ? { stoppedBy: turn.stopRequestedBy ?? USER_NAME } : {}
8362
8365
  };
8363
- if (outcome.exitReason === "completed" || outcome.exitReason === "blocked" || outcome.exitReason === "stopped") {
8364
- await this.board.setDigestCursor(job.agent, job.scope, turn.delivered, job.thread);
8365
- await this.renewLeases(turn.actor, turn.held);
8366
- if (outcome.status?.needsUserDecision === true) {
8367
- await this.askUser(turn.actor, job.scope, job.thread, turn.record.startedAt, outcome.status.summary);
8366
+ const recorded = await this.recorded(turn, async () => {
8367
+ if (outcome.session !== job.session) {
8368
+ await this.board.writeSession(job.agent, job.scope, job.cli, outcome.session, turn.runner, job.thread);
8369
+ }
8370
+ if (outcome.exitReason === "completed" || outcome.exitReason === "blocked" || outcome.exitReason === "stopped") {
8371
+ await this.board.setDigestCursor(job.agent, job.scope, turn.delivered, job.thread);
8372
+ await this.renewLeases(turn.actor, turn.held);
8373
+ if (outcome.status?.needsUserDecision === true) {
8374
+ await this.askUser(turn.actor, job.scope, job.thread, turn.record.startedAt, outcome.status.summary);
8375
+ }
8368
8376
  }
8377
+ await this.board.finishTurn(finished, turn.transcript);
8378
+ });
8379
+ if (recorded) {
8380
+ this.log.info({
8381
+ agent: job.agent,
8382
+ project: job.scope,
8383
+ thread: job.thread,
8384
+ exitReason: finished.exitReason
8385
+ }, "turn finished");
8386
+ turn.done(finished);
8369
8387
  }
8370
- await this.board.finishTurn(finished, turn.transcript);
8371
- this.log.info({ agent: job.agent, project: job.scope, thread: job.thread, exitReason: finished.exitReason }, "turn finished");
8372
- turn.done(finished);
8373
- return { dropWorktree: await this.taskEnded(turn.taskId) };
8388
+ return { dropWorkspace: await this.conversationEnded(job) };
8374
8389
  }
8375
8390
  /** Ends a turn whose outcome will never come, as when its runner went away, as failed. */
8376
8391
  async abortTurn(turnId, reason) {
@@ -8390,9 +8405,26 @@ var TurnHost = class {
8390
8405
  error: reason,
8391
8406
  toolCalls: turn.transcript.filter((entry) => entry.event.type === "tool_call").length
8392
8407
  };
8393
- await this.board.finishTurn(finished, turn.transcript);
8394
- this.log.warn({ agent: turn.job.agent, project: turn.job.scope, reason }, "turn aborted");
8395
- turn.done(finished);
8408
+ if (await this.recorded(turn, () => this.board.finishTurn(finished, turn.transcript))) {
8409
+ this.log.warn({ agent: turn.job.agent, project: turn.job.scope, reason }, "turn aborted");
8410
+ turn.done(finished);
8411
+ }
8412
+ }
8413
+ /**
8414
+ * Records the end of a turn already taken out of `open`, and says whether it could. The scheduler
8415
+ * holds the turn's conversation until the turn ends, and nothing would end it once it has left
8416
+ * `open`, so a turn whose end could not be recorded ends with the error rather than holding its
8417
+ * conversation for good.
8418
+ */
8419
+ async recorded(turn, record) {
8420
+ try {
8421
+ await record();
8422
+ return true;
8423
+ } catch (error) {
8424
+ this.log.error({ agent: turn.job.agent, project: turn.job.scope, error: String(error) }, "could not record the end of a turn");
8425
+ turn.fail(error);
8426
+ return false;
8427
+ }
8396
8428
  }
8397
8429
  /** Records a turn that could never start, as when no runner could take its agent's CLI. */
8398
8430
  async refuseTurn(dispatch, runner, reason) {
@@ -8445,29 +8477,34 @@ var TurnHost = class {
8445
8477
  this.residentTokens.set(slot, token);
8446
8478
  return token;
8447
8479
  }
8448
- async workspaceOf(project, taskId) {
8480
+ /** Where a conversation works: a task's on its branch, any other thread's in a place of its own. */
8481
+ async workspaceOf(project, thread) {
8482
+ const own = thread === void 0 || thread.task ? {} : { thread: thread.id };
8449
8483
  if (project === null) {
8450
- return { kind: "home" };
8484
+ return { kind: "home", ...own };
8451
8485
  }
8452
8486
  const repo = {
8453
8487
  slug: project.slug,
8454
8488
  origin: project.repo,
8455
8489
  defaultBranch: project.defaultBranch
8456
8490
  };
8457
- if (taskId !== void 0) {
8458
- return { kind: "task", repo, taskId };
8491
+ if (thread?.task === true) {
8492
+ return { kind: "task", repo, taskId: thread.id };
8459
8493
  }
8460
8494
  const branches = (await this.board.listTasks(project.slug)).filter((task) => task.status === "open" || task.status === "claimed").map((task) => `task/${task.id}`);
8461
- return { kind: "project", repo, branches };
8495
+ return { kind: "project", repo, branches, ...own };
8462
8496
  }
8463
- /** Whether a task conversation's task has ended, which frees its worktree. */
8464
- async taskEnded(taskId) {
8465
- if (taskId === void 0) {
8497
+ /** Whether a thread conversation's task or thread has ended, which frees its workspace. */
8498
+ async conversationEnded(job) {
8499
+ if (job.thread === void 0) {
8466
8500
  return false;
8467
8501
  }
8468
8502
  try {
8469
- const { task } = await this.board.findTask(taskId);
8470
- return task.status === "done" || task.status === "abandoned";
8503
+ if (job.workspace.kind === "task") {
8504
+ const { task } = await this.board.findTask(job.thread);
8505
+ return task.status === "done" || task.status === "abandoned";
8506
+ }
8507
+ return (await this.board.readThread(job.thread)).state === "closed";
8471
8508
  } catch {
8472
8509
  return false;
8473
8510
  }
@@ -8670,7 +8707,7 @@ var RunnerHub = class {
8670
8707
  async turnOutcome(runner, turnId, outcome) {
8671
8708
  this.assertOwns(runner, turnId);
8672
8709
  const ack = await this.host.closeTurn(turnId, outcome);
8673
- return ack ?? { dropWorktree: false };
8710
+ return ack ?? { dropWorkspace: false };
8674
8711
  }
8675
8712
  answer(runner, request, answer) {
8676
8713
  const pending = this.pending.get(request);
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "codex",
9
9
  "multi-agent"
10
10
  ],
11
- "version": "0.3.8",
11
+ "version": "0.3.10",
12
12
  "license": "MIT",
13
13
  "homepage": "https://github.com/kr4t0n/stellaris",
14
14
  "repository": {