@kubitnodes/stellaris 0.3.25 → 0.3.27

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 (47) hide show
  1. package/dist/cli/index.js +340 -131
  2. package/dist/server/index.js +818 -178
  3. package/package.json +1 -1
  4. package/web/dist/assets/{BackLink-UTiLF1J4.js → BackLink-qSxOMeEG.js} +1 -1
  5. package/web/dist/assets/{Button-Cp4cNRUX.js → Button-CxjWy598.js} +1 -1
  6. package/web/dist/assets/{ChannelForms-DD2B0tey.js → ChannelForms-BVidWMId.js} +1 -1
  7. package/web/dist/assets/{ChannelView-CY3oG9hZ.js → ChannelView-rqfk1Tyu.js} +1 -1
  8. package/web/dist/assets/{CitizenView-BNK5ro-6.js → CitizenView-xU99R8hS.js} +1 -1
  9. package/web/dist/assets/{CitizensView-WPQ6Jhl3.js → CitizensView-Bgw6PRSo.js} +1 -1
  10. package/web/dist/assets/{CliIcon-C7bwChMP.js → CliIcon-BB2NdLxN.js} +1 -1
  11. package/web/dist/assets/{CronsView-_pdgwX1I.js → CronsView-BrXrcfC6.js} +1 -1
  12. package/web/dist/assets/{FileTree-jyDUSu7g.js → FileTree-CHDmrOhY.js} +1 -1
  13. package/web/dist/assets/{FileView-DcQvlc8V.js → FileView-DUMfKYRW.js} +1 -1
  14. package/web/dist/assets/{KnowledgeView-DDl0HQPG.js → KnowledgeView-Qog6c92V.js} +1 -1
  15. package/web/dist/assets/{Markdown-DT3V-EVe.js → Markdown-C7RbUEZE.js} +1 -1
  16. package/web/dist/assets/{MessageItem-agtEugfG.js → MessageItem-DgWh5pJF.js} +1 -1
  17. package/web/dist/assets/{MetricsView-DiXyYeWP.js → MetricsView-DuOyISk1.js} +1 -1
  18. package/web/dist/assets/{NeedsYouView-CKmuYan_.js → NeedsYouView-CrmHXZAn.js} +1 -1
  19. package/web/dist/assets/{Overview-CKcTqsMM.js → Overview-xatORJK6.js} +1 -1
  20. package/web/dist/assets/{Pane-BaM0TdE6.js → Pane-BlIRwUtQ.js} +1 -1
  21. package/web/dist/assets/{Picker-C2ig-0dT.js → Picker-BSOB2JM_.js} +1 -1
  22. package/web/dist/assets/ProjectView-CVt28SoI.js +1 -0
  23. package/web/dist/assets/{ProposalView-B5yjTAcn.js → ProposalView-DJ0aJm7J.js} +1 -1
  24. package/web/dist/assets/{ProposalsView-p4KjwV9p.js → ProposalsView-C0r1ya8_.js} +1 -1
  25. package/web/dist/assets/{Quota-CfFGIql9.js → Quota-59QJZrPa.js} +1 -1
  26. package/web/dist/assets/{RunnersView-DdvwoOur.js → RunnersView-CcAAfw6y.js} +1 -1
  27. package/web/dist/assets/{SkillFileList-CgrCnuaC.js → SkillFileList-Cs_f3RPo.js} +1 -1
  28. package/web/dist/assets/{SkillView-Di5Z9h2L.js → SkillView-BvVx3LSL.js} +1 -1
  29. package/web/dist/assets/{SocietyView-BDiVxUC5.js → SocietyView-CJRrypjI.js} +1 -1
  30. package/web/dist/assets/{StickyList-BaV_KZ8x.js → StickyList-BbGq82Jj.js} +1 -1
  31. package/web/dist/assets/{SubjectThread-DD_ZCCnr.js → SubjectThread-DdkyhVqK.js} +1 -1
  32. package/web/dist/assets/{TaskView-CKcs-MOC.js → TaskView-CyEWpCvS.js} +1 -1
  33. package/web/dist/assets/{TasksView-oZMjncOU.js → TasksView-D10XP1uk.js} +1 -1
  34. package/web/dist/assets/{ThreadForms-CG5Y926b.js → ThreadForms-D94Jj0ES.js} +1 -1
  35. package/web/dist/assets/{ThreadItem-D-n2arFA.js → ThreadItem-CYmdCPOf.js} +1 -1
  36. package/web/dist/assets/{ThreadView-CT9vd0P4.js → ThreadView-D79G2zgF.js} +1 -1
  37. package/web/dist/assets/{citizen-BiHfph2A.js → citizen-CgA4vJjy.js} +1 -1
  38. package/web/dist/assets/{compose-DY1WL8zB.js → compose-3ksQaHF6.js} +1 -1
  39. package/web/dist/assets/format-DpQyO6r1.js +55 -0
  40. package/web/dist/assets/{governance-C6ez5MLh.js → governance-YialJVYK.js} +1 -1
  41. package/web/dist/assets/{index-CPXALUV6.js → index-DhuCp-VF.js} +3 -3
  42. package/web/dist/assets/{mentions-BEL5U6ra.js → mentions-Buc5vs-j.js} +1 -1
  43. package/web/dist/assets/{tasks-CAI8Eci7.js → tasks-DLTeaHys.js} +1 -1
  44. package/web/dist/assets/{useMatch-CINzyDWa.js → useMatch-DPEg8ohe.js} +1 -1
  45. package/web/dist/index.html +8 -8
  46. package/web/dist/assets/ProjectView-T2OHTh5j.js +0 -1
  47. package/web/dist/assets/format-CENiwkJC.js +0 -55
package/dist/cli/index.js CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  // apps/cli/dist/index.js
4
4
  import { readFile as readFile5 } from "node:fs/promises";
5
- import path8 from "node:path";
5
+ import path9 from "node:path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // packages/board-core/dist/board.js
9
- import { chmod as chmod2, cp, mkdir as mkdir4, readdir as readdir4, readFile as readFile4, rename as rename3, rm as rm2, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
10
- import path7 from "node:path";
9
+ import { chmod as chmod2, cp, mkdir as mkdir5, readdir as readdir4, readFile as readFile4, rename as rename3, rm as rm3, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
10
+ import path8 from "node:path";
11
11
  import { decodeTime, monotonicFactory } from "ulid";
12
12
  import { z as z16 } from "zod";
13
13
 
@@ -1190,6 +1190,7 @@ var BOARD_EVENT_TYPES = [
1190
1190
  "project.configured",
1191
1191
  "project.archived",
1192
1192
  "project.placed",
1193
+ "project.moved",
1193
1194
  "agent.added",
1194
1195
  "agent.configured",
1195
1196
  "agent.placed",
@@ -1370,6 +1371,11 @@ var RunnerHelloSchema = z9.object({
1370
1371
  quotaReads: z9.boolean().default(false),
1371
1372
  /** Whether the runner reads an image a turn running there uploads; one from before never does. */
1372
1373
  imageReads: z9.boolean().default(false),
1374
+ /**
1375
+ * Whether the runner carries a project's repository out to another runner and in from one, and
1376
+ * removes its copy once the project has moved; one from before never does.
1377
+ */
1378
+ projectMoves: z9.boolean().default(false),
1373
1379
  capabilities: z9.array(z9.string()).default([]),
1374
1380
  /**
1375
1381
  * What the runner's account lacks of the git identity citizens' own commits carry, checked
@@ -1471,6 +1477,11 @@ var TurnJobSchema = z9.object({
1471
1477
  session: z9.string().min(1).nullable(),
1472
1478
  /** The running total the session reported after its last turn, which a resumed session starts from. */
1473
1479
  costSoFarUsd: z9.number().nonnegative(),
1480
+ /**
1481
+ * The session's running usage after its last turn, which a resumed session's own usage is
1482
+ * measured from. Absent when the board holds none for the session, and from a server from before.
1483
+ */
1484
+ usageSoFar: UsageSchema.optional(),
1474
1485
  /** The rendered charter, norms, memory core, and skills index, with `PATH_TOKENS` for places. */
1475
1486
  instructions: z9.string(),
1476
1487
  prompt: z9.string(),
@@ -1499,6 +1510,17 @@ var TurnOutcomeSchema = z9.object({
1499
1510
  costUsd: z9.number().nonnegative(),
1500
1511
  /** The session's running total after this turn, for CLIs that report one. */
1501
1512
  sessionCostUsd: z9.number().nonnegative().optional(),
1513
+ /**
1514
+ * The session's running usage after this turn, for CLIs that report one, else the job's
1515
+ * `usageSoFar`. A runner from before never sends it.
1516
+ */
1517
+ sessionUsage: UsageSchema.optional(),
1518
+ /**
1519
+ * The whole input of the turn's latest model call in the session's own thread, cached or not:
1520
+ * how large the session had grown. Sub-agents' calls are left out. A turn that made no call,
1521
+ * and a runner from before, never send it.
1522
+ */
1523
+ contextTokens: z9.number().int().nonnegative().optional(),
1502
1524
  /**
1503
1525
  * The session the CLI used, which differs from the job's for CLIs that assign their own ids. A
1504
1526
  * turn whose CLI never started, because it failed or was stopped first, reports the job's own,
@@ -1658,6 +1680,43 @@ var HeldWorkspaceSchema = z9.object({
1658
1680
  });
1659
1681
  var HeldWorkspacesSchema = z9.object({ workspaces: z9.array(HeldWorkspaceSchema) });
1660
1682
  var SweepResultSchema = z9.object({ kept: z9.array(HeldWorkspaceSchema) });
1683
+ var MOVE_PACK_LIMIT_BYTES = 256 * 1024 * 1024;
1684
+ var RepoRefsSchema = z9.record(z9.string().regex(/^refs\/(heads|tags)\/\S+$/), z9.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/));
1685
+ var ProjectExportSchema = z9.object({ move: UlidSchema, repo: ProjectRepoSchema });
1686
+ var HeldWorkSchema = z9.object({
1687
+ agent: NameSchema,
1688
+ conversation: WorkspaceConversationSchema.nullable(),
1689
+ uncommitted: z9.number().int().nonnegative(),
1690
+ onNoBranch: z9.number().int().nonnegative()
1691
+ });
1692
+ var ProjectExportedSchema = z9.discriminatedUnion("ok", [
1693
+ z9.object({
1694
+ ok: z9.literal(true),
1695
+ refs: RepoRefsSchema,
1696
+ bytes: z9.number().int().nonnegative()
1697
+ }),
1698
+ z9.object({
1699
+ ok: z9.literal(false),
1700
+ held: z9.array(HeldWorkSchema),
1701
+ bytes: z9.number().int().nonnegative()
1702
+ })
1703
+ ]);
1704
+ var ProjectImportSchema = z9.object({
1705
+ move: UlidSchema,
1706
+ repo: ProjectRepoSchema,
1707
+ pullRequests: z9.boolean()
1708
+ });
1709
+ var ProjectImportedSchema = z9.object({ refs: RepoRefsSchema });
1710
+ var ProjectReleaseSchema = z9.object({ slug: NameSchema });
1711
+ var ProjectReleasedSchema = z9.object({
1712
+ worktrees: z9.number().int().nonnegative(),
1713
+ clone: z9.boolean()
1714
+ });
1715
+ var ProjectMovedSchema = z9.object({
1716
+ project: ProjectSchema,
1717
+ carried: z9.number().int().nonnegative().nullable(),
1718
+ released: z9.object({ ok: z9.boolean(), detail: z9.string() }).nullable()
1719
+ });
1661
1720
  var TurnSteerSchema = z9.object({ id: z9.uuid(), text: z9.string().min(1) });
1662
1721
  var RunnerMessageSchema = z9.discriminatedUnion("type", [
1663
1722
  z9.object({ type: z9.literal("turn"), job: TurnJobSchema }),
@@ -1694,6 +1753,19 @@ var RunnerMessageSchema = z9.discriminatedUnion("type", [
1694
1753
  }),
1695
1754
  /** Answered with whether the turn was there to stop; it ends `stopped`. */
1696
1755
  z9.object({ type: z9.literal("stop"), request: z9.string().min(1), turnId: UlidSchema }),
1756
+ /**
1757
+ * Answered with a `ProjectExported`. Sent only to runners whose hello says `projectMoves`, as are
1758
+ * the import and the release.
1759
+ */
1760
+ z9.object({ type: z9.literal("export"), request: z9.string().min(1), export: ProjectExportSchema }),
1761
+ /** Answered with a `ProjectImported`. */
1762
+ z9.object({ type: z9.literal("import"), request: z9.string().min(1), import: ProjectImportSchema }),
1763
+ /** Answered with a `ProjectReleased`. */
1764
+ z9.object({
1765
+ type: z9.literal("release"),
1766
+ request: z9.string().min(1),
1767
+ release: ProjectReleaseSchema
1768
+ }),
1697
1769
  /**
1698
1770
  * A conversation ended: the runner removes every citizen's workspace for it but those a turn
1699
1771
  * still uses, and answers with a `SweepResult` naming those it kept for holding work on no branch.
@@ -1808,6 +1880,16 @@ var TurnRecordSchema = z10.object({
1808
1880
  * resumed session starts from it, so the next turn's own cost is measured from here.
1809
1881
  */
1810
1882
  sessionCostUsd: z10.number().nonnegative().optional(),
1883
+ /**
1884
+ * The running usage the CLI reported for the session after this turn, when the runner reports
1885
+ * one. A resumed session's next turn measures its own usage from here.
1886
+ */
1887
+ sessionUsage: UsageSchema.optional(),
1888
+ /**
1889
+ * The whole input of the turn's latest model call in the session's own thread, as the runner
1890
+ * reported it: how large the session had grown. Absent for a turn that made no call.
1891
+ */
1892
+ contextTokens: z10.number().int().nonnegative().optional(),
1811
1893
  toolCalls: z10.number().int().nonnegative().default(0),
1812
1894
  /** The model the CLI reported for this turn, else the model configured for the agent, else null. */
1813
1895
  model: z10.string().nullable().default(null),
@@ -1821,7 +1903,10 @@ var TurnRecordSchema = z10.object({
1821
1903
  transient: z10.boolean().optional(),
1822
1904
  /** How many turns in a row, this one included, could not resume the session for a `retry`. */
1823
1905
  resumeRetries: z10.number().int().positive().optional(),
1824
- /** Whether the server dropped the conversation's session after this turn, which the next starts anew. */
1906
+ /**
1907
+ * Whether the server dropped the conversation's session after this turn, which the next starts
1908
+ * anew: one its CLI could not resume, or a society role's home after a reflection completed.
1909
+ */
1825
1910
  sessionDropped: z10.boolean().optional(),
1826
1911
  /** The crons whose fire started the turn, which record how it ended. */
1827
1912
  crons: z10.array(UlidSchema).optional()
@@ -2034,11 +2119,17 @@ var TokenUseSchema = z12.object({
2034
2119
  // packages/shared/dist/config.js
2035
2120
  import { z as z13 } from "zod";
2036
2121
  var LogLevelSchema = z13.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
2122
+ var AppleAppIdSchema = z13.string().regex(/^[A-Z0-9]{10}\.[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*$/, "not <team id>.<bundle id>");
2037
2123
  var GithubSignInSchema = z13.object({
2038
2124
  clientId: z13.string().min(1),
2039
2125
  clientSecret: z13.string().min(1),
2040
2126
  /** Logins, lowercased; GitHub logins are case-insensitive. */
2041
- users: z13.array(z13.string().min(1)).min(1)
2127
+ users: z13.array(z13.string().min(1)).min(1),
2128
+ /**
2129
+ * The iOS app that may sign in, which Apple's association file names so that only it can take the
2130
+ * board's `https` landing; null when no app signs in.
2131
+ */
2132
+ appleAppId: AppleAppIdSchema.nullable()
2042
2133
  });
2043
2134
  var ServerConfigSchema = z13.object({
2044
2135
  dataDir: z13.string().min(1),
@@ -2084,7 +2175,7 @@ var RunnerConfigSchema = z13.object({
2084
2175
  });
2085
2176
 
2086
2177
  // packages/shared/dist/version.js
2087
- var STELLARIS_VERSION = "0.3.25";
2178
+ var STELLARIS_VERSION = "0.3.27";
2088
2179
 
2089
2180
  // packages/shared/dist/mentions.js
2090
2181
  import { parse, postprocess, preprocess } from "micromark";
@@ -2776,6 +2867,41 @@ function filesOf(lines, status) {
2776
2867
  return [...files.values()];
2777
2868
  }
2778
2869
 
2870
+ // packages/board-core/dist/moves.js
2871
+ import { mkdir as mkdir4, rm as rm2 } from "node:fs/promises";
2872
+ import path5 from "node:path";
2873
+ import { execa as execa2 } from "execa";
2874
+ var MoveRepos = class {
2875
+ root;
2876
+ constructor(root) {
2877
+ this.root = root;
2878
+ }
2879
+ /** Makes the move's empty repository, which checks every object a push brings, and answers its path. */
2880
+ async open(dir) {
2881
+ await mkdir4(this.root, { recursive: true });
2882
+ await git2(["init", "--quiet", "--bare", dir], this.root);
2883
+ await git2(["config", "receive.fsckObjects", "true"], dir);
2884
+ return dir;
2885
+ }
2886
+ async drop(dir) {
2887
+ await rm2(path5.resolve(this.root, dir), { recursive: true, force: true });
2888
+ }
2889
+ /** Removes every move's repository, as the server starts: a move never survives a restart. */
2890
+ async clear() {
2891
+ await rm2(this.root, { recursive: true, force: true });
2892
+ }
2893
+ };
2894
+ async function git2(args, cwd) {
2895
+ const result = await execa2("git", args, {
2896
+ cwd,
2897
+ reject: false,
2898
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
2899
+ });
2900
+ if (result.exitCode !== 0) {
2901
+ throw new Error(`git ${args[0] ?? ""} failed: ${result.stderr.trim()}`);
2902
+ }
2903
+ }
2904
+
2779
2905
  // packages/board-core/dist/mentions.js
2780
2906
  import vm from "node:vm";
2781
2907
  var MENTION_PARSE_MS = 500;
@@ -3189,121 +3315,121 @@ var Mutex = class {
3189
3315
  };
3190
3316
 
3191
3317
  // packages/board-core/dist/paths.js
3192
- import path5 from "node:path";
3318
+ import path6 from "node:path";
3193
3319
  var BoardPaths = class {
3194
3320
  dataDir;
3195
3321
  constructor(dataDir) {
3196
- this.dataDir = path5.resolve(dataDir);
3322
+ this.dataDir = path6.resolve(dataDir);
3197
3323
  }
3198
3324
  // Board projection, also the storage of record; only the core library writes it.
3199
3325
  get board() {
3200
- return path5.join(this.dataDir, "board");
3326
+ return path6.join(this.dataDir, "board");
3201
3327
  }
3202
3328
  get society() {
3203
- return path5.join(this.board, "society");
3329
+ return path6.join(this.board, "society");
3204
3330
  }
3205
3331
  societyFile() {
3206
- return path5.join(this.society, "society.md");
3332
+ return path6.join(this.society, "society.md");
3207
3333
  }
3208
3334
  societyChannels() {
3209
- return path5.join(this.society, "channels");
3335
+ return path6.join(this.society, "channels");
3210
3336
  }
3211
3337
  societyChannel(name) {
3212
- return path5.join(this.societyChannels(), name);
3338
+ return path6.join(this.societyChannels(), name);
3213
3339
  }
3214
3340
  /** Threads whose channel is a society channel; project channels' threads live under the project. */
3215
3341
  societyThreads() {
3216
- return path5.join(this.society, "threads");
3342
+ return path6.join(this.society, "threads");
3217
3343
  }
3218
3344
  societyKnowledge() {
3219
- return path5.join(this.society, "knowledge");
3345
+ return path6.join(this.society, "knowledge");
3220
3346
  }
3221
3347
  societyKnowledgeFile(topic) {
3222
- return path5.join(this.societyKnowledge(), `${topic}.md`);
3348
+ return path6.join(this.societyKnowledge(), `${topic}.md`);
3223
3349
  }
3224
3350
  /** Skills promoted to the society, one directory per skill holding SKILL.md. */
3225
3351
  societySkills() {
3226
- return path5.join(this.society, "skills");
3352
+ return path6.join(this.society, "skills");
3227
3353
  }
3228
3354
  societySkill(name) {
3229
- return path5.join(this.societySkills(), name);
3355
+ return path6.join(this.societySkills(), name);
3230
3356
  }
3231
3357
  societySkillFile(name) {
3232
- return path5.join(this.societySkill(name), "SKILL.md");
3358
+ return path6.join(this.societySkill(name), "SKILL.md");
3233
3359
  }
3234
3360
  roles() {
3235
- return path5.join(this.society, "roles");
3361
+ return path6.join(this.society, "roles");
3236
3362
  }
3237
3363
  role(name) {
3238
- return path5.join(this.roles(), `${name}.md`);
3364
+ return path6.join(this.roles(), `${name}.md`);
3239
3365
  }
3240
3366
  proposals() {
3241
- return path5.join(this.society, "proposals");
3367
+ return path6.join(this.society, "proposals");
3242
3368
  }
3243
3369
  proposal(id) {
3244
- return path5.join(this.proposals(), `${id}.md`);
3370
+ return path6.join(this.proposals(), `${id}.md`);
3245
3371
  }
3246
3372
  /** A skill proposal's folder, as the board took it from its proposer's home. */
3247
3373
  proposalFiles(id) {
3248
- return path5.join(this.proposals(), id);
3374
+ return path6.join(this.proposals(), id);
3249
3375
  }
3250
3376
  decisions() {
3251
- return path5.join(this.society, "decisions");
3377
+ return path6.join(this.society, "decisions");
3252
3378
  }
3253
3379
  decision(id) {
3254
- return path5.join(this.decisions(), `${id}.md`);
3380
+ return path6.join(this.decisions(), `${id}.md`);
3255
3381
  }
3256
3382
  runners() {
3257
- return path5.join(this.society, "runners");
3383
+ return path6.join(this.society, "runners");
3258
3384
  }
3259
3385
  runner(name) {
3260
- return path5.join(this.runners(), `${name}.md`);
3386
+ return path6.join(this.runners(), `${name}.md`);
3261
3387
  }
3262
3388
  members() {
3263
- return path5.join(this.society, "members");
3389
+ return path6.join(this.society, "members");
3264
3390
  }
3265
3391
  /** Crons, one file each with its note as the body; ended ones stay. */
3266
3392
  crons() {
3267
- return path5.join(this.society, "crons");
3393
+ return path6.join(this.society, "crons");
3268
3394
  }
3269
3395
  cron(id) {
3270
- return path5.join(this.crons(), `${id}.md`);
3396
+ return path6.join(this.crons(), `${id}.md`);
3271
3397
  }
3272
3398
  member(name) {
3273
- return path5.join(this.members(), `${name}.md`);
3399
+ return path6.join(this.members(), `${name}.md`);
3274
3400
  }
3275
3401
  projects() {
3276
- return path5.join(this.board, "projects");
3402
+ return path6.join(this.board, "projects");
3277
3403
  }
3278
3404
  project(slug) {
3279
- return path5.join(this.projects(), slug);
3405
+ return path6.join(this.projects(), slug);
3280
3406
  }
3281
3407
  projectFile(slug) {
3282
- return path5.join(this.project(slug), "project.md");
3408
+ return path6.join(this.project(slug), "project.md");
3283
3409
  }
3284
3410
  projectChannels(slug) {
3285
- return path5.join(this.project(slug), "channels");
3411
+ return path6.join(this.project(slug), "channels");
3286
3412
  }
3287
3413
  projectChannel(slug, name) {
3288
- return path5.join(this.projectChannels(slug), name);
3414
+ return path6.join(this.projectChannels(slug), name);
3289
3415
  }
3290
3416
  threads(slug) {
3291
- return path5.join(this.project(slug), "threads");
3417
+ return path6.join(this.project(slug), "threads");
3292
3418
  }
3293
3419
  tasks(slug) {
3294
- return path5.join(this.project(slug), "tasks");
3420
+ return path6.join(this.project(slug), "tasks");
3295
3421
  }
3296
3422
  task(slug, id) {
3297
- return path5.join(this.tasks(slug), `${id}.md`);
3423
+ return path6.join(this.tasks(slug), `${id}.md`);
3298
3424
  }
3299
3425
  projectKnowledge(slug) {
3300
- return path5.join(this.project(slug), "knowledge");
3426
+ return path6.join(this.project(slug), "knowledge");
3301
3427
  }
3302
3428
  projectKnowledgeFile(slug, topic) {
3303
- return path5.join(this.projectKnowledge(slug), `${topic}.md`);
3429
+ return path6.join(this.projectKnowledge(slug), `${topic}.md`);
3304
3430
  }
3305
3431
  dashboard(slug) {
3306
- return path5.join(this.project(slug), "dashboard.md");
3432
+ return path6.join(this.project(slug), "dashboard.md");
3307
3433
  }
3308
3434
  channelDir(ref) {
3309
3435
  const parsed = parseChannelRef(ref);
@@ -3316,57 +3442,57 @@ var BoardPaths = class {
3316
3442
  }
3317
3443
  /** A thread's record, `<id>.md`, beside the directory of its messages, `<id>/`. */
3318
3444
  threadFile(threads, id) {
3319
- return path5.join(threads, `${id}.md`);
3445
+ return path6.join(threads, `${id}.md`);
3320
3446
  }
3321
3447
  threadMessages(threads, id) {
3322
- return path5.join(threads, id);
3448
+ return path6.join(threads, id);
3323
3449
  }
3324
3450
  messageFile(dir, id, author) {
3325
- return path5.join(dir, `${id}-${author}.md`);
3451
+ return path6.join(dir, `${id}-${author}.md`);
3326
3452
  }
3327
3453
  // Agent homes: authored by the agent, source of truth on the board server.
3328
3454
  agents() {
3329
- return path5.join(this.dataDir, "agents");
3455
+ return path6.join(this.dataDir, "agents");
3330
3456
  }
3331
3457
  agent(name) {
3332
- return path5.join(this.agents(), name);
3458
+ return path6.join(this.agents(), name);
3333
3459
  }
3334
3460
  agentFile(name) {
3335
- return path5.join(this.agent(name), "agent.json");
3461
+ return path6.join(this.agent(name), "agent.json");
3336
3462
  }
3337
3463
  agentRole(name) {
3338
- return path5.join(this.agent(name), "role.md");
3464
+ return path6.join(this.agent(name), "role.md");
3339
3465
  }
3340
3466
  agentProfile(name) {
3341
- return path5.join(this.agent(name), "profile.md");
3467
+ return path6.join(this.agent(name), "profile.md");
3342
3468
  }
3343
3469
  agentMemory(name) {
3344
- return path5.join(this.agent(name), "memory");
3470
+ return path6.join(this.agent(name), "memory");
3345
3471
  }
3346
3472
  agentMemoryCore(name) {
3347
- return path5.join(this.agentMemory(name), "core.md");
3473
+ return path6.join(this.agentMemory(name), "core.md");
3348
3474
  }
3349
3475
  /** Transcripts of the agent's finished turns, one JSON line per step. */
3350
3476
  agentTurns(name) {
3351
- return path5.join(this.agent(name), "turns");
3477
+ return path6.join(this.agent(name), "turns");
3352
3478
  }
3353
3479
  agentTranscript(name, turnId) {
3354
- return path5.join(this.agentTurns(name), `${turnId}.jsonl`);
3480
+ return path6.join(this.agentTurns(name), `${turnId}.jsonl`);
3355
3481
  }
3356
3482
  agentSkills(name) {
3357
- return path5.join(this.agent(name), "skills");
3483
+ return path6.join(this.agent(name), "skills");
3358
3484
  }
3359
3485
  agentSkill(name, skill2) {
3360
- return path5.join(this.agentSkills(name), skill2);
3486
+ return path6.join(this.agentSkills(name), skill2);
3361
3487
  }
3362
3488
  agentSkillFile(name, skill2) {
3363
- return path5.join(this.agentSkill(name, skill2), "SKILL.md");
3489
+ return path6.join(this.agentSkill(name, skill2), "SKILL.md");
3364
3490
  }
3365
3491
  agentProjects(name) {
3366
- return path5.join(this.agent(name), "projects");
3492
+ return path6.join(this.agent(name), "projects");
3367
3493
  }
3368
3494
  agentProject(name, slug) {
3369
- return path5.join(this.agentProjects(name), slug);
3495
+ return path6.join(this.agentProjects(name), slug);
3370
3496
  }
3371
3497
  /**
3372
3498
  * A thread's, a channel's, or, in the society scope, a project's conversation's session and last
@@ -3374,46 +3500,56 @@ var BoardPaths = class {
3374
3500
  */
3375
3501
  agentConversation(name, slug, part) {
3376
3502
  if (part.startsWith("#")) {
3377
- return path5.join(this.agentProject(name, slug), "channels", part.slice(1));
3503
+ return path6.join(this.agentProject(name, slug), "channels", part.slice(1));
3378
3504
  }
3379
- return part.startsWith("~") ? path5.join(this.agentProject(name, slug), "places", part.slice(1)) : path5.join(this.agentProject(name, slug), "threads", part);
3505
+ return part.startsWith("~") ? path6.join(this.agentProject(name, slug), "places", part.slice(1)) : path6.join(this.agentProject(name, slug), "threads", part);
3380
3506
  }
3381
3507
  agentCursors(name) {
3382
- return path5.join(this.agent(name), "cursors.json");
3508
+ return path6.join(this.agent(name), "cursors.json");
3383
3509
  }
3384
3510
  events() {
3385
- return path5.join(this.dataDir, "events");
3511
+ return path6.join(this.dataDir, "events");
3386
3512
  }
3387
3513
  eventLog() {
3388
- return path5.join(this.events(), "log.jsonl");
3514
+ return path6.join(this.events(), "log.jsonl");
3389
3515
  }
3390
3516
  /** Images citizens uploaded, by id: outside the projection, which every runner mirrors. */
3391
3517
  images() {
3392
- return path5.join(this.dataDir, "images");
3518
+ return path6.join(this.dataDir, "images");
3393
3519
  }
3394
3520
  image(id) {
3395
- return path5.join(this.images(), id);
3521
+ return path6.join(this.images(), id);
3522
+ }
3523
+ /**
3524
+ * The repositories projects travel through between runners, one per move while it runs:
3525
+ * outside the projection, which every runner mirrors, and served only to the move's runners.
3526
+ */
3527
+ moves() {
3528
+ return path6.join(this.dataDir, "moves");
3529
+ }
3530
+ move(id) {
3531
+ return path6.join(this.moves(), `${id}.git`);
3396
3532
  }
3397
3533
  state() {
3398
- return path5.join(this.dataDir, "state");
3534
+ return path6.join(this.dataDir, "state");
3399
3535
  }
3400
3536
  pausedFile() {
3401
- return path5.join(this.state(), "paused.json");
3537
+ return path6.join(this.state(), "paused.json");
3402
3538
  }
3403
3539
  /** Removed knowledge topics, set aside outside the projection, by scope. */
3404
3540
  removedKnowledge(scope) {
3405
- return path5.join(this.state(), "removed-knowledge", scope);
3541
+ return path6.join(this.state(), "removed-knowledge", scope);
3406
3542
  }
3407
3543
  /** Skills removed from the society, set aside outside the projection, one directory per removal. */
3408
3544
  removedSkills() {
3409
- return path5.join(this.state(), "removed-skills");
3545
+ return path6.join(this.state(), "removed-skills");
3410
3546
  }
3411
3547
  };
3412
3548
 
3413
3549
  // packages/board-core/dist/skill-folder.js
3414
3550
  import { constants } from "node:fs";
3415
3551
  import { lstat, open as open2, readdir as readdir3, readlink, realpath } from "node:fs/promises";
3416
- import path6 from "node:path";
3552
+ import path7 from "node:path";
3417
3553
  var SKILL_TEXT_LIMIT_CHARS = 1e5;
3418
3554
  var SKILL_TEXT_LIMIT_BYTES = SKILL_TEXT_LIMIT_CHARS * 4;
3419
3555
  function skillFileText(bytes) {
@@ -3424,8 +3560,8 @@ function skillFileText(bytes) {
3424
3560
  };
3425
3561
  }
3426
3562
  function inside(root, file) {
3427
- const relative = path6.relative(root, file);
3428
- return relative !== ".." && !relative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(relative);
3563
+ const relative = path7.relative(root, file);
3564
+ return relative !== ".." && !relative.startsWith(`..${path7.sep}`) && !path7.isAbsolute(relative);
3429
3565
  }
3430
3566
  async function handleInside(root, handle) {
3431
3567
  if (process.platform !== "linux") {
@@ -3435,7 +3571,7 @@ async function handleInside(root, handle) {
3435
3571
  return file !== null && inside(root, file);
3436
3572
  }
3437
3573
  async function walk(root, prefix, found) {
3438
- const entries = await readdir3(path6.join(root, ...prefix === "" ? [] : prefix.split("/")), {
3574
+ const entries = await readdir3(path7.join(root, ...prefix === "" ? [] : prefix.split("/")), {
3439
3575
  withFileTypes: true
3440
3576
  }).catch(() => []);
3441
3577
  for (const entry of entries.toSorted((a, b) => a.name < b.name ? -1 : 1)) {
@@ -3456,7 +3592,7 @@ async function walk(root, prefix, found) {
3456
3592
  async function readEntry(root, relative) {
3457
3593
  let real;
3458
3594
  try {
3459
- real = await realpath(path6.join(root, ...relative.split("/")));
3595
+ real = await realpath(path7.join(root, ...relative.split("/")));
3460
3596
  } catch {
3461
3597
  return null;
3462
3598
  }
@@ -3772,9 +3908,11 @@ var Board = class _Board {
3772
3908
  signIns = /* @__PURE__ */ new Map();
3773
3909
  files = new FileTree();
3774
3910
  homes;
3911
+ moves;
3775
3912
  constructor(dataDir, options) {
3776
3913
  this.paths = new BoardPaths(dataDir);
3777
- this.homes = new HomeRepos(path7.join(this.paths.state(), "home-hooks"));
3914
+ this.homes = new HomeRepos(path8.join(this.paths.state(), "home-hooks"));
3915
+ this.moves = new MoveRepos(this.paths.moves());
3778
3916
  this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
3779
3917
  this.now = options.now ?? (() => /* @__PURE__ */ new Date());
3780
3918
  this.events = new EventLog(this.paths.eventLog(), () => this.newId(), this.now);
@@ -3800,6 +3938,7 @@ var Board = class _Board {
3800
3938
  await board.loadRunnerTokens();
3801
3939
  await board.loadSignIns();
3802
3940
  await board.homes.installHooks();
3941
+ await board.moves.clear();
3803
3942
  return board;
3804
3943
  }
3805
3944
  /**
@@ -3809,7 +3948,7 @@ var Board = class _Board {
3809
3948
  */
3810
3949
  async applyAlignments() {
3811
3950
  const alignments = [];
3812
- const file = path7.join(this.paths.state(), "alignments.json");
3951
+ const file = path8.join(this.paths.state(), "alignments.json");
3813
3952
  const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
3814
3953
  const pending = alignments.filter(({ name }) => !applied.includes(name));
3815
3954
  if (pending.length === 0) {
@@ -4017,7 +4156,7 @@ ${charter.purpose}
4017
4156
  });
4018
4157
  }
4019
4158
  signInsFile() {
4020
- return path7.join(this.paths.state(), "sign-ins.json");
4159
+ return path8.join(this.paths.state(), "sign-ins.json");
4021
4160
  }
4022
4161
  async readSignIns() {
4023
4162
  const file = this.signInsFile();
@@ -4114,7 +4253,7 @@ ${charter.purpose}
4114
4253
  const files = await listFiles(this.paths.tasks(project2));
4115
4254
  const tasks = [];
4116
4255
  for (const file of files) {
4117
- const doc = await readMarkdown(path7.join(this.paths.tasks(project2), file), TaskFrontmatterSchema);
4256
+ const doc = await readMarkdown(path8.join(this.paths.tasks(project2), file), TaskFrontmatterSchema);
4118
4257
  tasks.push({ ...doc.data, body: doc.body });
4119
4258
  }
4120
4259
  return tasks;
@@ -4133,7 +4272,7 @@ ${charter.purpose}
4133
4272
  const files = await listFiles(this.paths.proposals());
4134
4273
  const proposals = [];
4135
4274
  for (const file of files) {
4136
- const doc = await readMarkdown(path7.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
4275
+ const doc = await readMarkdown(path8.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
4137
4276
  proposals.push({ ...doc.data, body: doc.body });
4138
4277
  }
4139
4278
  return proposals;
@@ -4160,7 +4299,7 @@ ${charter.purpose}
4160
4299
  async listMembers() {
4161
4300
  const members = [];
4162
4301
  for (const file of await listFiles(this.paths.members())) {
4163
- const doc = await readMarkdown(path7.join(this.paths.members(), file), MemberSchema);
4302
+ const doc = await readMarkdown(path8.join(this.paths.members(), file), MemberSchema);
4164
4303
  members.push({ ...doc.data, profile: doc.body });
4165
4304
  }
4166
4305
  return members;
@@ -4179,7 +4318,7 @@ ${charter.purpose}
4179
4318
  const dir = project2 === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project2);
4180
4319
  const topics = [];
4181
4320
  for (const file of await listFiles(dir)) {
4182
- topics.push(await this.knowledgeAt(path7.join(dir, file), project2, file.replace(/\.md$/, "")));
4321
+ topics.push(await this.knowledgeAt(path8.join(dir, file), project2, file.replace(/\.md$/, "")));
4183
4322
  }
4184
4323
  return topics;
4185
4324
  }
@@ -4209,7 +4348,7 @@ ${charter.purpose}
4209
4348
  const aside = this.paths.removedKnowledge(project2 ?? SOCIETY_SCOPE);
4210
4349
  let highest = 0;
4211
4350
  for (const file of (await listFiles(aside)).filter((name) => name.startsWith(`${topic}.`))) {
4212
- const copy = await this.knowledgeAt(path7.join(aside, file), project2, topic);
4351
+ const copy = await this.knowledgeAt(path8.join(aside, file), project2, topic);
4213
4352
  highest = Math.max(highest, copy.revision);
4214
4353
  }
4215
4354
  return highest;
@@ -4225,7 +4364,7 @@ ${charter.purpose}
4225
4364
  async listSkillsIn(dir, scope) {
4226
4365
  const skills = [];
4227
4366
  for (const entry of await listDirs(dir)) {
4228
- const file = path7.join(dir, entry, "SKILL.md");
4367
+ const file = path8.join(dir, entry, "SKILL.md");
4229
4368
  if (!await exists(file)) {
4230
4369
  continue;
4231
4370
  }
@@ -4315,7 +4454,7 @@ ${charter.purpose}
4315
4454
  if (!wanted.delete(file.slice(0, 26))) {
4316
4455
  continue;
4317
4456
  }
4318
- const { data } = await readMarkdown(path7.join(dir, file), MessageFrontmatterSchema);
4457
+ const { data } = await readMarkdown(path8.join(dir, file), MessageFrontmatterSchema);
4319
4458
  found.push(MessageRefSchema.parse(data));
4320
4459
  }
4321
4460
  if (wanted.size === 0) {
@@ -4347,7 +4486,7 @@ ${charter.purpose}
4347
4486
  if (!file.endsWith(".md")) {
4348
4487
  continue;
4349
4488
  }
4350
- const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
4489
+ const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
4351
4490
  if (doc.data.state !== "open") {
4352
4491
  continue;
4353
4492
  }
@@ -4374,7 +4513,7 @@ ${charter.purpose}
4374
4513
  const all = [];
4375
4514
  for (const threads of await this.threadScopes()) {
4376
4515
  for (const file of await listFiles(threads)) {
4377
- const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
4516
+ const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
4378
4517
  const messages = await listFiles(this.paths.threadMessages(threads, doc.data.id));
4379
4518
  const last = messages.at(-1);
4380
4519
  all.push({
@@ -4969,7 +5108,7 @@ ${task2.body}`;
4969
5108
  }
4970
5109
  }
4971
5110
  for (const file of await listFiles(this.paths.projectKnowledge(project2))) {
4972
- const content = await readFile4(path7.join(this.paths.projectKnowledge(project2), file), "utf8");
5111
+ const content = await readFile4(path8.join(this.paths.projectKnowledge(project2), file), "utf8");
4973
5112
  if (content.toLowerCase().includes(needle)) {
4974
5113
  hits.push({
4975
5114
  kind: "knowledge",
@@ -4990,7 +5129,7 @@ ${task2.body}`;
4990
5129
  shared.push({
4991
5130
  kind: "knowledge",
4992
5131
  ref: `society/knowledge/${file}`,
4993
- file: path7.join(this.paths.societyKnowledge(), file)
5132
+ file: path8.join(this.paths.societyKnowledge(), file)
4994
5133
  });
4995
5134
  }
4996
5135
  for (const skill2 of await this.listSocietySkills()) {
@@ -5003,7 +5142,7 @@ ${task2.body}`;
5003
5142
  shared.push({
5004
5143
  kind: "memory",
5005
5144
  ref: `memory/${file}`,
5006
- file: path7.join(this.paths.agentMemory(actor.name), file)
5145
+ file: path8.join(this.paths.agentMemory(actor.name), file)
5007
5146
  });
5008
5147
  }
5009
5148
  for (const entry of shared) {
@@ -5092,8 +5231,8 @@ ${current.body}`);
5092
5231
  removedAt: this.now().toISOString()
5093
5232
  });
5094
5233
  const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
5095
- await mkdir4(aside, { recursive: true });
5096
- await rename3(file, path7.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
5234
+ await mkdir5(aside, { recursive: true });
5235
+ await rename3(file, path8.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
5097
5236
  await this.events.append("knowledge.removed", actor.name, {
5098
5237
  topic: args.topic,
5099
5238
  project: args.project
@@ -5140,8 +5279,8 @@ ${current.body}`);
5140
5279
  removedAt: this.now().toISOString()
5141
5280
  });
5142
5281
  const aside = this.paths.removedSkills();
5143
- await mkdir4(aside, { recursive: true });
5144
- await rename3(this.paths.societySkill(args.name), path7.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
5282
+ await mkdir5(aside, { recursive: true });
5283
+ await rename3(this.paths.societySkill(args.name), path8.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
5145
5284
  await this.events.append("skill.removed", actor.name, {
5146
5285
  name: args.name,
5147
5286
  reason: args.reason
@@ -5218,7 +5357,7 @@ ${current.body}`);
5218
5357
  type,
5219
5358
  size: bytes.byteLength
5220
5359
  });
5221
- const name = path7.basename(args.path).replaceAll(/[[\]\\\r\n]/g, "");
5360
+ const name = path8.basename(args.path).replaceAll(/[[\]\\\r\n]/g, "");
5222
5361
  return {
5223
5362
  id,
5224
5363
  type,
@@ -5244,7 +5383,7 @@ ${current.body}`);
5244
5383
  async listCrons() {
5245
5384
  const crons = [];
5246
5385
  for (const file of await listFiles(this.paths.crons())) {
5247
- crons.push(await this.readCronFile(path7.join(this.paths.crons(), file)));
5386
+ crons.push(await this.readCronFile(path8.join(this.paths.crons(), file)));
5248
5387
  }
5249
5388
  return crons;
5250
5389
  }
@@ -5591,7 +5730,7 @@ ${current.body}`);
5591
5730
  async openThreadIds(dir, keep) {
5592
5731
  const ids = /* @__PURE__ */ new Set();
5593
5732
  for (const file of await listFiles(dir)) {
5594
- const doc = await readMarkdown(path7.join(dir, file), ThreadFrontmatterSchema);
5733
+ const doc = await readMarkdown(path8.join(dir, file), ThreadFrontmatterSchema);
5595
5734
  if (doc.data.state === "open" && keep({ ...doc.data, body: doc.body })) {
5596
5735
  ids.add(doc.data.id);
5597
5736
  }
@@ -5599,12 +5738,12 @@ ${current.body}`);
5599
5738
  return ids;
5600
5739
  }
5601
5740
  /**
5602
- * Refuses to archive a project while a turn runs in it, in a citizen's conversation of its
5603
- * channels in the society scope, or in a thread its archive closes.
5741
+ * Refuses to archive or move a project while a turn runs in it, in a citizen's conversation of
5742
+ * its channels in the society scope, or in one of its open threads.
5604
5743
  */
5605
- async assertProjectQuiet(actor, slug) {
5744
+ async assertProjectQuiet(actor, slug, what = `project ${slug} cannot be archived`) {
5606
5745
  const threads = await this.openThreadIds(this.paths.threads(slug), () => true);
5607
- this.assertNoTurnCut(actor, `project ${slug} cannot be archived`, (turn2) => turn2.scope === slug || turn2.place === slug || turn2.thread !== void 0 && threads.has(turn2.thread));
5746
+ this.assertNoTurnCut(actor, what, (turn2) => turn2.scope === slug || turn2.place === slug || turn2.thread !== void 0 && threads.has(turn2.thread));
5608
5747
  }
5609
5748
  async closeThread(actor, input) {
5610
5749
  const args = VerbInputs.close_thread.parse(input);
@@ -6571,7 +6710,7 @@ ${rationale}`}`);
6571
6710
  return part === void 0 ? this.paths.agentProject(agent2, scope) : this.paths.agentConversation(agent2, scope, part);
6572
6711
  }
6573
6712
  async readSessions(agent2, project2, part) {
6574
- const file = path7.join(this.sessionDir(agent2, project2, part), "sessions.json");
6713
+ const file = path8.join(this.sessionDir(agent2, project2, part), "sessions.json");
6575
6714
  return await exists(file) ? readJson(file, SessionsFileSchema) : {};
6576
6715
  }
6577
6716
  /**
@@ -6582,7 +6721,7 @@ ${rationale}`}`);
6582
6721
  await this.mutex.run(async () => {
6583
6722
  const dir = this.sessionDir(agent2, project2, part);
6584
6723
  await ensureDir(dir);
6585
- const file = path7.join(dir, "sessions.json");
6724
+ const file = path8.join(dir, "sessions.json");
6586
6725
  const current = await exists(file) ? await readJson(file, SessionsFileSchema) : {};
6587
6726
  const kept = current.runner === void 0 || current.runner === runner2 ? current : {};
6588
6727
  await writeJson(file, { ...kept, [cli]: sessionId, runner: runner2 });
@@ -6594,7 +6733,7 @@ ${rationale}`}`);
6594
6733
  */
6595
6734
  async dropSession(agent2, project2, cli, sessionId, part) {
6596
6735
  return this.mutex.run(async () => {
6597
- const file = path7.join(this.sessionDir(agent2, project2, part), "sessions.json");
6736
+ const file = path8.join(this.sessionDir(agent2, project2, part), "sessions.json");
6598
6737
  if (!await exists(file)) {
6599
6738
  return false;
6600
6739
  }
@@ -6609,7 +6748,7 @@ ${rationale}`}`);
6609
6748
  }
6610
6749
  /** The last turn of one conversation: the scope's home, a channel's, a project's, or a thread's. */
6611
6750
  async readLastTurn(agent2, project2, part) {
6612
- const file = path7.join(this.sessionDir(agent2, project2, part), "last-turn.json");
6751
+ const file = path8.join(this.sessionDir(agent2, project2, part), "last-turn.json");
6613
6752
  return await exists(file) ? readJson(file, TurnRecordSchema) : null;
6614
6753
  }
6615
6754
  /** The latest turn a citizen took in a scope, in any of its conversations there. */
@@ -6617,9 +6756,9 @@ ${rationale}`}`);
6617
6756
  let latest = await this.readLastTurn(agent2, project2);
6618
6757
  const place = this.paths.agentProject(agent2, project2);
6619
6758
  const parts = [
6620
- ...await listDirs(path7.join(place, "threads")),
6621
- ...(await listDirs(path7.join(place, "channels"))).map((name) => `#${name}`),
6622
- ...(await listDirs(path7.join(place, "places"))).map((name) => `~${name}`)
6759
+ ...await listDirs(path8.join(place, "threads")),
6760
+ ...(await listDirs(path8.join(place, "channels"))).map((name) => `#${name}`),
6761
+ ...(await listDirs(path8.join(place, "places"))).map((name) => `~${name}`)
6623
6762
  ];
6624
6763
  for (const part of parts) {
6625
6764
  const turn2 = await this.readLastTurn(agent2, project2, part);
@@ -6685,6 +6824,7 @@ ${rationale}`}`);
6685
6824
  exitReason: parsed.exitReason,
6686
6825
  costUsd: parsed.costUsd,
6687
6826
  ...parsed.usage === null ? {} : { usage: parsed.usage },
6827
+ ...parsed.contextTokens === void 0 ? {} : { contextTokens: parsed.contextTokens },
6688
6828
  toolCalls: parsed.toolCalls,
6689
6829
  model: parsed.model,
6690
6830
  summary: parsed.status?.summary ?? null,
@@ -6885,11 +7025,11 @@ ${rationale}`}`);
6885
7025
  }
6886
7026
  /** Small named state files under data/state, for cursors the scheduler must keep across restarts. */
6887
7027
  async readState(name, schema, fallback) {
6888
- const file = path7.join(this.paths.state(), `${name}.json`);
7028
+ const file = path8.join(this.paths.state(), `${name}.json`);
6889
7029
  return await exists(file) ? readJson(file, schema) : fallback;
6890
7030
  }
6891
7031
  async writeState(name, value) {
6892
- await this.mutex.run(() => writeJson(path7.join(this.paths.state(), `${name}.json`), value));
7032
+ await this.mutex.run(() => writeJson(path8.join(this.paths.state(), `${name}.json`), value));
6893
7033
  }
6894
7034
  /** The project's dashboard, empty at revision 0 when it has none. */
6895
7035
  async readDashboard(slug) {
@@ -6920,7 +7060,7 @@ ${rationale}`}`);
6920
7060
  async listRunners() {
6921
7061
  const runners = [];
6922
7062
  for (const file of await listFiles(this.paths.runners())) {
6923
- runners.push((await readMarkdown(path7.join(this.paths.runners(), file), RunnerSchema)).data);
7063
+ runners.push((await readMarkdown(path8.join(this.paths.runners(), file), RunnerSchema)).data);
6924
7064
  }
6925
7065
  return runners;
6926
7066
  }
@@ -6972,7 +7112,7 @@ ${rationale}`}`);
6972
7112
  return this.runnerTokens.get(hashToken(token)) ?? null;
6973
7113
  }
6974
7114
  runnerTokensFile() {
6975
- return path7.join(this.paths.state(), "runners.json");
7115
+ return path8.join(this.paths.state(), "runners.json");
6976
7116
  }
6977
7117
  async readRunnerTokens() {
6978
7118
  const file = this.runnerTokensFile();
@@ -6986,7 +7126,8 @@ ${rationale}`}`);
6986
7126
  }
6987
7127
  /**
6988
7128
  * Places a project on the runner that will hold its repository, unless it already lives on one,
6989
- * and returns where it lives. Placement is once: every later turn of the project goes there.
7129
+ * and returns where it lives. Placement is once: every later turn of the project goes there,
7130
+ * until the user moves the project (`recordProjectMove`).
6990
7131
  */
6991
7132
  async placeProject(slug, runner2) {
6992
7133
  return this.mutex.run(async () => {
@@ -7028,12 +7169,12 @@ ${rationale}`}`);
7028
7169
  }
7029
7170
  const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
7030
7171
  if (entry.isDirectory()) {
7031
- await walk2(path7.join(dir, entry.name), relative);
7172
+ await walk2(path8.join(dir, entry.name), relative);
7032
7173
  continue;
7033
7174
  }
7034
7175
  const file = entry.isFile() ? conflictCopyOf(relative) : null;
7035
7176
  if (file !== null) {
7036
- const since = await this.homes.changedAt(home, relative) ?? (await stat3(path7.join(home, relative)).catch(() => null))?.mtime.toISOString();
7177
+ const since = await this.homes.changedAt(home, relative) ?? (await stat3(path8.join(home, relative)).catch(() => null))?.mtime.toISOString();
7037
7178
  if (since !== void 0) {
7038
7179
  found.push({ path: relative, file, since });
7039
7180
  }
@@ -7107,6 +7248,74 @@ ${rationale}`}`);
7107
7248
  return next;
7108
7249
  });
7109
7250
  }
7251
+ /**
7252
+ * The board's half of moving a project to another runner, which only the user does, checked
7253
+ * before anything travels and while the hub holds the project's turns: the project is active and
7254
+ * lives on a runner other than `to`, which the board knows; no task there is landing, since its
7255
+ * landing runs on the old runner; and no turn runs in the project. Tasks in play move with their
7256
+ * branches, so the answer lists them for the capabilities the new runner must offer.
7257
+ */
7258
+ async prepareProjectMove(actor, slug, to) {
7259
+ this.assertUser(actor, "only the user may move a project to another runner");
7260
+ const runner2 = NameSchema.parse(to);
7261
+ return this.mutex.run(async () => {
7262
+ const project2 = await this.readActiveProject(slug);
7263
+ const from = project2.runner;
7264
+ if (from === void 0) {
7265
+ throw new BoardError("INVALID_STATE", `project ${slug} lives on no runner yet; its first turn places it`);
7266
+ }
7267
+ if (from === runner2) {
7268
+ throw new BoardError("INVALID_STATE", `project ${slug} already lives on ${runner2}`);
7269
+ }
7270
+ await this.readRunner(runner2);
7271
+ const inPlay = (await this.listTasks(slug)).filter((task2) => task2.status === "open" || task2.status === "claimed");
7272
+ const landing = inPlay.filter((task2) => task2.completing);
7273
+ if (landing.length > 0) {
7274
+ throw new BoardError("INVALID_STATE", `project ${slug} cannot be moved while ${landing.length} task(s) land on ${from} (${landing.map((task2) => task2.id).join(", ")}); move it once they have landed`);
7275
+ }
7276
+ await this.assertProjectQuiet(actor, slug, `project ${slug} cannot be moved`);
7277
+ return { project: { ...project2, runner: from }, inPlay };
7278
+ });
7279
+ }
7280
+ /**
7281
+ * Records that a project moved, once its new runner holds every branch its old one had, or with
7282
+ * nothing carried when the user moved it while the old runner was away: every later turn of
7283
+ * the project goes to `to`. Refused when the project was archived or moved meanwhile.
7284
+ */
7285
+ async recordProjectMove(actor, move) {
7286
+ this.assertUser(actor, "only the user may move a project to another runner");
7287
+ return this.mutex.run(async () => {
7288
+ const current = await this.readActiveProject(move.slug);
7289
+ if (current.runner !== move.from) {
7290
+ throw new BoardError("INVALID_STATE", `project ${move.slug} no longer lives on ${move.from}, so this move no longer applies`);
7291
+ }
7292
+ await this.readRunner(move.to);
7293
+ const project2 = await this.updateProject(move.slug, (each) => ({ ...each, runner: move.to }));
7294
+ await this.events.append("project.moved", actor.name, {
7295
+ slug: move.slug,
7296
+ from: move.from,
7297
+ to: move.to,
7298
+ carried: move.carried
7299
+ });
7300
+ return project2;
7301
+ });
7302
+ }
7303
+ /**
7304
+ * Makes the repository a move carries a project through, for the server's git route to serve,
7305
+ * and answers the move's id, which names it; `dropMoveRepo` removes it when the move ends.
7306
+ */
7307
+ async openMoveRepo() {
7308
+ const id = this.newId();
7309
+ await this.moves.open(this.paths.move(id));
7310
+ return id;
7311
+ }
7312
+ async dropMoveRepo(id) {
7313
+ await this.moves.drop(this.paths.move(UlidSchema.parse(id)));
7314
+ }
7315
+ /** Where the moves' repositories live, which the server's git route serves from. */
7316
+ get moveRepos() {
7317
+ return this.paths.moves();
7318
+ }
7110
7319
  /** The projection a runner mirrors for its agents to read, by relative path, with hashes. */
7111
7320
  async boardManifest() {
7112
7321
  return this.files.manifest(this.paths.board, () => true);
@@ -7117,7 +7326,7 @@ ${rationale}`}`);
7117
7326
  async writeTurnRecord(record) {
7118
7327
  const dir = this.sessionDir(record.agent, record.project, conversationPart(record));
7119
7328
  await ensureDir(dir);
7120
- await writeJson(path7.join(dir, "last-turn.json"), record);
7329
+ await writeJson(path8.join(dir, "last-turn.json"), record);
7121
7330
  }
7122
7331
  // ---------------------------------------------------------------------------------------------
7123
7332
  // Internals
@@ -7236,8 +7445,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7236
7445
  async ensureAgentProject(name, slug) {
7237
7446
  const dir = this.paths.agentProject(name, slug);
7238
7447
  await ensureDir(dir);
7239
- if (!await exists(path7.join(dir, "sessions.json"))) {
7240
- await writeJson(path7.join(dir, "sessions.json"), {});
7448
+ if (!await exists(path8.join(dir, "sessions.json"))) {
7449
+ await writeJson(path8.join(dir, "sessions.json"), {});
7241
7450
  }
7242
7451
  }
7243
7452
  /**
@@ -7479,11 +7688,11 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7479
7688
  const file = this.paths.societySkillFile(skill2.name);
7480
7689
  const replaced = await exists(file);
7481
7690
  const taken = proposal2.skillFiles;
7482
- const staged = path7.join(this.paths.proposalFiles(proposal2.id), "SKILL.md");
7691
+ const staged = path8.join(this.paths.proposalFiles(proposal2.id), "SKILL.md");
7483
7692
  if (taken === void 0 || !await exists(staged)) {
7484
7693
  throw new BoardError("INVALID_STATE", `proposal ${proposal2.id} holds no skill files`);
7485
7694
  }
7486
- await rm2(dir, { recursive: true, force: true });
7695
+ await rm3(dir, { recursive: true, force: true });
7487
7696
  await cp(this.paths.proposalFiles(proposal2.id), dir, { recursive: true });
7488
7697
  const { body } = await readLooseMarkdown(file);
7489
7698
  await writeMarkdown(file, {
@@ -7536,10 +7745,10 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7536
7745
  return `the frontmatter of ${folder}/SKILL.md in ${proposal2.proposedBy}'s home does not parse (${manual.error})`;
7537
7746
  }
7538
7747
  const dir = this.paths.proposalFiles(proposal2.id);
7539
- await rm2(dir, { recursive: true, force: true });
7748
+ await rm3(dir, { recursive: true, force: true });
7540
7749
  for (const entry of taken.files) {
7541
- const target = path7.join(dir, ...entry.path.split("/"));
7542
- await ensureDir(path7.dirname(target));
7750
+ const target = path8.join(dir, ...entry.path.split("/"));
7751
+ await ensureDir(path8.dirname(target));
7543
7752
  await writeFile4(target, contents.get(entry.path) ?? Buffer.alloc(0));
7544
7753
  await chmod2(target, entry.executable ? 493 : 420);
7545
7754
  }
@@ -7630,7 +7839,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7630
7839
  if (!isSafeRelativePath(entry.path)) {
7631
7840
  continue;
7632
7841
  }
7633
- const bytes = await readFile4(path7.join(dir, ...entry.path.split("/"))).catch(() => null);
7842
+ const bytes = await readFile4(path8.join(dir, ...entry.path.split("/"))).catch(() => null);
7634
7843
  files.push({
7635
7844
  ...entry,
7636
7845
  ...bytes === null ? { text: null, truncated: false } : skillFileText(bytes)
@@ -7779,7 +7988,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7779
7988
  this.assertNoTurnCut(actor, `${ref} cannot be archived`, (turn2) => turn2.channel === channel && turn2.scope === scope || turn2.thread !== void 0 && closing.has(turn2.thread));
7780
7989
  const threadsClosed = [];
7781
7990
  for (const file of await listFiles(threads)) {
7782
- const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
7991
+ const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
7783
7992
  if (doc.data.channel !== ref || doc.data.state !== "open") {
7784
7993
  continue;
7785
7994
  }
@@ -7911,7 +8120,7 @@ ${charter.purpose}
7911
8120
  const threads = this.paths.threads(slug);
7912
8121
  const threadsClosed = [];
7913
8122
  for (const file of await listFiles(threads)) {
7914
- const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
8123
+ const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
7915
8124
  if (doc.data.state !== "open") {
7916
8125
  continue;
7917
8126
  }
@@ -7986,10 +8195,10 @@ ${charter.purpose}
7986
8195
  ...project2,
7987
8196
  members: project2.members.filter((member) => member !== name)
7988
8197
  }));
7989
- const sessions = path7.join(this.paths.agentProject(name, slug), "sessions.json");
8198
+ const sessions = path8.join(this.paths.agentProject(name, slug), "sessions.json");
7990
8199
  if (await exists(sessions)) {
7991
8200
  const archived = `sessions.archived.${ts.replace(/[:.]/g, "-")}.json`;
7992
- await rename3(sessions, path7.join(this.paths.agentProject(name, slug), archived));
8201
+ await rename3(sessions, path8.join(this.paths.agentProject(name, slug), archived));
7993
8202
  }
7994
8203
  }
7995
8204
  this.tokenIndex.delete(current.tokenHash);
@@ -8330,7 +8539,7 @@ ${charter.purpose}
8330
8539
  if (since !== null && id <= since || upto !== void 0 && id > upto) {
8331
8540
  continue;
8332
8541
  }
8333
- const doc = await readMarkdown(path7.join(dir, file), MessageFrontmatterSchema);
8542
+ const doc = await readMarkdown(path8.join(dir, file), MessageFrontmatterSchema);
8334
8543
  messages.push({ ...doc.data, body: doc.body });
8335
8544
  }
8336
8545
  return messages;
@@ -8852,7 +9061,7 @@ knowledge.command("show <topic>").description("Print a knowledge topic").option(
8852
9061
  });
8853
9062
  knowledge.command("write <topic>").description("Write a knowledge topic as the user, from a file or standard input").requiredOption("--revision <n>", "the revision it replaces, as knowledge list shows it; 0 for a new topic").option("--project <slug>", "the project's topic instead of the society's").option("--file <path>", "read the body from this file instead of standard input").option("--as <agent>", "act as this agent instead of the user").action(async (topic, opts) => {
8854
9063
  const board = await open3();
8855
- const body = opts.file === void 0 ? await readStdin() : await readFile5(path8.resolve(opts.file), "utf8");
9064
+ const body = opts.file === void 0 ? await readStdin() : await readFile5(path9.resolve(opts.file), "utf8");
8856
9065
  const written = await board.writeKnowledge(await actorFor(board, opts.as), {
8857
9066
  project: opts.project ?? null,
8858
9067
  topic,