@kubitnodes/stellaris 0.3.11 → 0.3.13

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 (62) hide show
  1. package/dist/cli/index.js +287 -28
  2. package/dist/server/index.js +542 -63
  3. package/package.json +1 -1
  4. package/web/dist/assets/{BackLink-C-vad6K2.js → BackLink-d0xz7ZLb.js} +1 -1
  5. package/web/dist/assets/ChannelForms-BIRwT9CY.js +1 -0
  6. package/web/dist/assets/ChannelView-BvMlzMiN.js +1 -0
  7. package/web/dist/assets/CitizenView-DNQwjpIp.js +2 -0
  8. package/web/dist/assets/CitizensView-Bv04iH-y.js +1 -0
  9. package/web/dist/assets/CliIcon-tYTwI_RQ.js +1 -0
  10. package/web/dist/assets/KnowledgeView-L0pdaM_2.js +1 -0
  11. package/web/dist/assets/{Markdown-J2pAPfCa.js → Markdown-BLiIulgZ.js} +4 -4
  12. package/web/dist/assets/MessageItem-CHrmjF7P.js +1 -0
  13. package/web/dist/assets/MetricsView-DG1alTgs.js +1 -0
  14. package/web/dist/assets/NeedsYouView-Doc0fKPi.js +1 -0
  15. package/web/dist/assets/Overview-ChsOgIEW.js +1 -0
  16. package/web/dist/assets/{Pane-caYWCiyo.js → Pane-MJgGXQBb.js} +1 -1
  17. package/web/dist/assets/ProjectView-CQTn7yDo.js +1 -0
  18. package/web/dist/assets/ProposalView-C92OJ_w1.js +1 -0
  19. package/web/dist/assets/ProposalsView-CSA8neXR.js +1 -0
  20. package/web/dist/assets/RunnersView-Dx21LDFu.js +1 -0
  21. package/web/dist/assets/SocietyView-CdVW2DgG.js +1 -0
  22. package/web/dist/assets/{SubjectThread-D_L3vB-A.js → SubjectThread-egXeFhT1.js} +1 -1
  23. package/web/dist/assets/TaskFileView-BqqzxJKb.js +4 -0
  24. package/web/dist/assets/TaskView-rks26vuF.js +3 -0
  25. package/web/dist/assets/TasksView-Ba5fPAvy.js +1 -0
  26. package/web/dist/assets/ThreadCard-D6i69_15.js +1 -0
  27. package/web/dist/assets/ThreadForms-ChTFRu-P.js +1 -0
  28. package/web/dist/assets/ThreadView-CnJPNS_8.js +1 -0
  29. package/web/dist/assets/{citizen-BtOZq62_.js → citizen-D-dxX00V.js} +1 -1
  30. package/web/dist/assets/compose-BgpUWGYE.js +1 -0
  31. package/web/dist/assets/{format-EfoceZkm.js → format-C3aawMM6.js} +1 -1
  32. package/web/dist/assets/governance-Di4MijNG.js +1 -0
  33. package/web/dist/assets/index-VVnGdAXc.css +2 -0
  34. package/web/dist/assets/index-krHkeIbX.js +23 -0
  35. package/web/dist/assets/link-DAMxBItI.js +1 -0
  36. package/web/dist/assets/mentions-BCdV0bMK.js +1 -0
  37. package/web/dist/assets/{tasks-Duke4U7N.js → tasks-bwj_EdwF.js} +1 -1
  38. package/web/dist/assets/useMatch-B-8IcuU6.js +1 -0
  39. package/web/dist/assets/{useStickyScroll-n5d7sdPZ.js → useStickyScroll-BILPUgGj.js} +1 -1
  40. package/web/dist/index.html +10 -4
  41. package/web/dist/assets/ChannelForms-jNbbNnIC.js +0 -1
  42. package/web/dist/assets/ChannelView-kLoegYUQ.js +0 -1
  43. package/web/dist/assets/CitizenView-Cb8f2mB7.js +0 -2
  44. package/web/dist/assets/CitizensView-Ds2rojfa.js +0 -1
  45. package/web/dist/assets/KnowledgeView-k10TU0jc.js +0 -1
  46. package/web/dist/assets/MessageItem-BjM8BdpK.js +0 -1
  47. package/web/dist/assets/MetricsView-D9N54rV7.js +0 -1
  48. package/web/dist/assets/NeedsYouView-ML8i7Pl7.js +0 -1
  49. package/web/dist/assets/Overview-BBXvRhfR.js +0 -1
  50. package/web/dist/assets/ProjectView-sDpFNjcW.js +0 -1
  51. package/web/dist/assets/ProposalView-Bc91NjKC.js +0 -1
  52. package/web/dist/assets/ProposalsView-Dg_bEryX.js +0 -1
  53. package/web/dist/assets/RunnersView-Dv5nbwYv.js +0 -1
  54. package/web/dist/assets/SocietyView-ClaEWgOq.js +0 -1
  55. package/web/dist/assets/TaskFileView-B19YtrtW.js +0 -4
  56. package/web/dist/assets/TaskView-FYSoCb3T.js +0 -1
  57. package/web/dist/assets/TasksView-CniXTU3R.js +0 -1
  58. package/web/dist/assets/ThreadCard-DCpvhSlc.js +0 -1
  59. package/web/dist/assets/ThreadForms-DUGkAwTp.js +0 -1
  60. package/web/dist/assets/ThreadView-D8eXlVbC.js +0 -1
  61. package/web/dist/assets/index-BeOGtAz0.css +0 -2
  62. package/web/dist/assets/index-D75IxLme.js +0 -23
package/dist/cli/index.js CHANGED
@@ -59,7 +59,8 @@ var VERB_NAMES = [
59
59
  "archive_project",
60
60
  "remove_knowledge",
61
61
  "create_channel",
62
- "archive_channel"
62
+ "archive_channel",
63
+ "update_dashboard"
63
64
  ];
64
65
  var VerbNameSchema = z2.enum(VERB_NAMES);
65
66
  var CharterTriggerSchema = z2.enum(["user_post", "ops_event", "heartbeat"]);
@@ -105,7 +106,8 @@ var MEMBER_VERBS = [
105
106
  "write_knowledge",
106
107
  "remove_knowledge",
107
108
  "plan_task",
108
- "advance_task"
109
+ "advance_task",
110
+ "update_dashboard"
109
111
  ];
110
112
  var GOVERNANCE_VERBS = ["approve", "reject"];
111
113
  var FRONT_DESK_VERBS = ["create_project"];
@@ -185,7 +187,44 @@ var SocietySchema = z3.object({
185
187
  channels: z3.array(NameSchema),
186
188
  archivedChannels: z3.array(ArchivedChannelSchema).default([])
187
189
  });
188
- var CompletionEffectSchema = z3.enum(["none", "merge"]);
190
+ var CompletionEffectSchema = z3.enum(["none", "merge", "ghpr"]);
191
+ var GITHUB_REMOTE = [
192
+ /^https:\/\/github\.com\/([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?$/,
193
+ /^git@github\.com:([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?$/,
194
+ /^ssh:\/\/git@github\.com\/([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?$/
195
+ ];
196
+ function githubRepository(remote) {
197
+ for (const pattern of GITHUB_REMOTE) {
198
+ const match = remote === null ? null : pattern.exec(remote);
199
+ if (match?.[1] !== void 0 && match[2] !== void 0) {
200
+ return { owner: match[1], name: match[2] };
201
+ }
202
+ }
203
+ return null;
204
+ }
205
+ var PULL_REQUEST_URL = /^https:\/\/github\.com\/([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+)\/pull\/([1-9]\d*)\/?$/;
206
+ var PullRequestUrlSchema = z3.string().trim().regex(PULL_REQUEST_URL, "a pull request's address, https://github.com/<owner>/<repo>/pull/<number>");
207
+ function parsePullRequestUrl(url) {
208
+ const match = PULL_REQUEST_URL.exec(url.trim());
209
+ return match?.[1] === void 0 || match[2] === void 0 || match[3] === void 0 ? null : { owner: match[1], name: match[2], number: Number(match[3]) };
210
+ }
211
+ function sameRepository(a, b) {
212
+ return a.owner.toLowerCase() === b.owner.toLowerCase() && a.name.toLowerCase() === b.name.toLowerCase();
213
+ }
214
+ var MergeSubjectSchema = z3.string().trim().min(1).max(300).regex(/^[^\n]+$/, "a merge commit's subject is one line");
215
+ var PullRequestLinkSchema = z3.object({
216
+ url: PullRequestUrlSchema,
217
+ number: z3.number().int().positive(),
218
+ subject: MergeSubjectSchema.optional(),
219
+ body: z3.string().max(2e4).optional(),
220
+ linkedBy: NameSchema,
221
+ linkedAt: IsoDateTimeSchema,
222
+ /**
223
+ * Once its `ghpr` task was abandoned, how the board's closing of it on GitHub went: `ok` false
224
+ * leaves it open, for the reason `detail` gives.
225
+ */
226
+ closed: z3.object({ at: IsoDateTimeSchema, ok: z3.boolean(), detail: z3.string() }).optional()
227
+ });
189
228
  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");
190
229
  var StageIdSchema = z3.string().regex(/^s[1-9][0-9]*$/, "expected a stage id such as s1");
191
230
  var stageFields = {
@@ -300,6 +339,8 @@ var TaskFrontmatterSchema = z3.object({
300
339
  /** The highest stage number issued, so a removed stage's id is never reused. */
301
340
  stageSeq: z3.number().int().positive(),
302
341
  onDone: CompletionEffectSchema.default("none"),
342
+ /** The pull request a `ghpr` task lands through. */
343
+ pullRequest: PullRequestLinkSchema.optional(),
303
344
  /** The last stage is complete and the completion effect is running. */
304
345
  completing: z3.boolean().default(false),
305
346
  /** The latest send-back while its rework lasts: cleared once the task reaches that stage again. */
@@ -392,6 +433,8 @@ var RunnerSchema = z3.object({
392
433
  os: RunnerOsSchema,
393
434
  clis: z3.array(CliKindSchema),
394
435
  capabilities: z3.array(z3.string()),
436
+ /** The release the runner reported when it last registered; absent until it has. */
437
+ version: z3.string().optional(),
395
438
  status: z3.enum(["connected", "disconnected"]),
396
439
  lastSeen: IsoDateTimeSchema.optional()
397
440
  });
@@ -570,12 +613,27 @@ var RemovedKnowledgeSchema = z3.object({
570
613
  removedBy: NameSchema,
571
614
  removedAt: IsoDateTimeSchema
572
615
  });
616
+ var DashboardSchema = z3.object({
617
+ project: NameSchema,
618
+ revision: z3.number().int().min(0),
619
+ /** Absent until someone updates the dashboard the project began with. */
620
+ updatedBy: NameSchema.optional(),
621
+ updatedAt: IsoDateTimeSchema.optional(),
622
+ body: z3.string()
623
+ });
624
+ var ProposalOriginSchema = z3.object({
625
+ scope: NameSchema,
626
+ thread: UlidSchema.optional(),
627
+ channel: NameSchema.optional()
628
+ });
573
629
  var ProposalFrontmatterSchema = z3.object({
574
630
  id: UlidSchema,
575
631
  kind: ProposalKindSchema,
576
632
  proposedBy: NameSchema,
577
633
  status: ProposalStatusSchema,
578
634
  createdAt: IsoDateTimeSchema,
635
+ /** Absent for a proposal filed outside a turn, or before proposals recorded it. */
636
+ filedFrom: ProposalOriginSchema.optional(),
579
637
  decidedBy: NameSchema.optional(),
580
638
  decidedAt: IsoDateTimeSchema.optional(),
581
639
  reason: z3.string().optional(),
@@ -641,7 +699,16 @@ var VerbInputs = {
641
699
  status: z4.enum(["abandoned"]).optional(),
642
700
  /** Posted to the task's thread as the caller, marked with the step when the task moved. */
643
701
  note: z4.string().min(1).optional(),
644
- blocked_by: z4.array(UlidSchema).optional()
702
+ blocked_by: z4.array(UlidSchema).optional(),
703
+ /**
704
+ * The pull request a `ghpr` task lands through, on its project's repository, with the merge
705
+ * commit's subject and body; null unlinks it.
706
+ */
707
+ pull_request: z4.object({
708
+ url: PullRequestUrlSchema,
709
+ merge_subject: MergeSubjectSchema.optional(),
710
+ merge_body: z4.string().max(2e4).optional()
711
+ }).nullable().optional()
645
712
  }),
646
713
  get_task: z4.object({ task_id: UlidSchema }),
647
714
  subscribe: z4.object({ channel: ChannelRefSchema }),
@@ -695,7 +762,14 @@ var VerbInputs = {
695
762
  name: NameSchema,
696
763
  purpose: z4.string().min(1)
697
764
  }),
698
- archive_channel: z4.object({ channel: ChannelRefSchema, reason: z4.string().min(1) })
765
+ archive_channel: z4.object({ channel: ChannelRefSchema, reason: z4.string().min(1) }),
766
+ update_dashboard: z4.object({
767
+ project: NameSchema,
768
+ /** The whole dashboard as markdown; the board keeps its frontmatter. */
769
+ body: z4.string().min(1),
770
+ /** The `revision` in the frontmatter of the dashboard you read, 0 when it names none. */
771
+ revision: z4.number().int().min(0)
772
+ })
699
773
  };
700
774
 
701
775
  // packages/shared/dist/events.js
@@ -735,6 +809,7 @@ var BOARD_EVENT_TYPES = [
735
809
  "agent.left",
736
810
  "knowledge.written",
737
811
  "knowledge.removed",
812
+ "dashboard.updated",
738
813
  "skill.promoted",
739
814
  "runner.added",
740
815
  "user.signed_in",
@@ -751,6 +826,8 @@ var BOARD_EVENT_TYPES = [
751
826
  "turn.failed",
752
827
  "merge.completed",
753
828
  "merge.failed",
829
+ "pull_request.closed",
830
+ "pull_request.close_failed",
754
831
  "workspace.leftovers"
755
832
  ];
756
833
  var BoardEventTypeSchema = z5.enum(BOARD_EVENT_TYPES);
@@ -898,7 +975,12 @@ var PendingEnrollmentSchema = z6.object({
898
975
  var ProjectRepoSchema = z6.object({
899
976
  slug: NameSchema,
900
977
  origin: z6.string().nullable(),
901
- defaultBranch: z6.string().min(1)
978
+ defaultBranch: z6.string().min(1),
979
+ /**
980
+ * The board lands finished tasks on the default branch, itself or through their pull requests,
981
+ * so the runner lets nothing else move it.
982
+ */
983
+ boardLands: z6.boolean()
902
984
  });
903
985
  var TurnWorkspaceSchema = z6.discriminatedUnion("kind", [
904
986
  z6.object({
@@ -972,7 +1054,22 @@ var TurnOutcomeSchema = z6.object({
972
1054
  var TurnAckSchema = z6.object({ dropWorkspace: z6.boolean() });
973
1055
  var LandRequestSchema = z6.object({
974
1056
  repo: ProjectRepoSchema,
975
- branch: z6.string().min(1)
1057
+ branch: z6.string().min(1),
1058
+ /**
1059
+ * For a `ghpr` task, the pull request to merge in place of the branch, which must hold the
1060
+ * branch's files, and the merge commit's subject and body, the pull request's title and an
1061
+ * empty body when left out.
1062
+ */
1063
+ pullRequest: z6.object({
1064
+ url: z6.string().min(1),
1065
+ subject: z6.string().min(1).optional(),
1066
+ body: z6.string().optional()
1067
+ }).optional()
1068
+ });
1069
+ var CloseRequestSchema = z6.object({
1070
+ repo: ProjectRepoSchema,
1071
+ url: z6.string().min(1),
1072
+ comment: z6.string().min(1)
976
1073
  });
977
1074
  var MergeOutcomeSchema = z6.object({ ok: z6.boolean(), detail: z6.string() });
978
1075
  var ModelListSchema = z6.array(ModelOptionSchema);
@@ -1041,6 +1138,8 @@ var TurnSteerSchema = z6.object({ id: z6.uuid(), text: z6.string().min(1) });
1041
1138
  var RunnerMessageSchema = z6.discriminatedUnion("type", [
1042
1139
  z6.object({ type: z6.literal("turn"), job: TurnJobSchema }),
1043
1140
  z6.object({ type: z6.literal("land"), request: z6.string().min(1), land: LandRequestSchema }),
1141
+ /** Answered with a `MergeOutcome`. */
1142
+ z6.object({ type: z6.literal("close"), request: z6.string().min(1), close: CloseRequestSchema }),
1044
1143
  z6.object({ type: z6.literal("models"), request: z6.string().min(1), cli: CliKindSchema }),
1045
1144
  /** Answered with a `BranchFile`, or null when the branch or the path does not exist. */
1046
1145
  z6.object({ type: z6.literal("file"), request: z6.string().min(1), read: BranchReadSchema }),
@@ -1385,7 +1484,7 @@ var RunnerConfigSchema = z10.object({
1385
1484
  });
1386
1485
 
1387
1486
  // packages/shared/dist/version.js
1388
- var STELLARIS_VERSION = "0.3.11";
1487
+ var STELLARIS_VERSION = "0.3.13";
1389
1488
 
1390
1489
  // packages/board-core/dist/errors.js
1391
1490
  var BoardError = class extends Error {
@@ -2566,6 +2665,9 @@ var THREAD_CONVERSATIONS = "thread-conversations";
2566
2665
  var ASKS_CHANNEL_OPENED = "asks-channel-opened";
2567
2666
  var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
2568
2667
  var CHANNEL_CONVERSATIONS = "channel-conversations";
2668
+ var DASHBOARD_UPDATES = "dashboard-updates-granted";
2669
+ var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
2670
+ var DASHBOARD_LINE = "The project's members keep this page current with update_dashboard.";
2569
2671
  var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
2570
2672
  var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
2571
2673
  var PROFILE_TEMPLATE = "# Profile\n\nOne short paragraph, kept current: what I do well, what I am working on, and what to send my way. The board projects this into the roster the front desk reads.\n";
@@ -2602,6 +2704,11 @@ function memberToAgentInput(member) {
2602
2704
  ...member.seedInstructions === void 0 ? {} : { seedInstructions: member.seedInstructions }
2603
2705
  };
2604
2706
  }
2707
+ function assertLandsByPullRequest(effect, repo, where) {
2708
+ if (effect === "ghpr" && githubRepository(repo) === null) {
2709
+ throw new BoardError("VALIDATION", `${where} has ${repo === null ? "no remote" : `its remote at ${repo}, not on GitHub`}, so its tasks cannot land through pull requests (ghpr)`);
2710
+ }
2711
+ }
2605
2712
  function stageFrom(planned, id) {
2606
2713
  return {
2607
2714
  id,
@@ -2759,7 +2866,8 @@ var Board = class _Board {
2759
2866
  THREAD_CONVERSATIONS,
2760
2867
  ASKS_CHANNEL_OPENED,
2761
2868
  KNOWLEDGE_REMOVAL,
2762
- CHANNEL_CONVERSATIONS
2869
+ CHANNEL_CONVERSATIONS,
2870
+ DASHBOARD_UPDATES
2763
2871
  ].filter((name) => !applied.includes(name));
2764
2872
  if (pending.length === 0) {
2765
2873
  return;
@@ -2789,21 +2897,38 @@ var Board = class _Board {
2789
2897
  await this.openAsksChannel();
2790
2898
  }
2791
2899
  if (pending.includes(KNOWLEDGE_REMOVAL)) {
2792
- await this.grantKnowledgeRemoval();
2900
+ await this.grantBesideKnowledge("remove_knowledge");
2793
2901
  }
2794
2902
  if (pending.includes(CHANNEL_CONVERSATIONS)) {
2795
2903
  await this.startChannelCursors();
2796
2904
  }
2905
+ if (pending.includes(DASHBOARD_UPDATES)) {
2906
+ await this.grantBesideKnowledge("update_dashboard");
2907
+ await this.rewordDashboards();
2908
+ }
2797
2909
  await writeJson(file, { applied: [...applied, ...pending] });
2798
2910
  }
2799
2911
  /**
2800
- * Whoever may write a scope's knowledge may remove it, so every charter granting
2801
- * `write_knowledge` is granted `remove_knowledge` once; seed roles have it from their seed.
2912
+ * Grants `verb` once to every charter granting `write_knowledge`, the work roles' charters in
2913
+ * practice; seed roles have it from their seed.
2802
2914
  */
2803
- async grantKnowledgeRemoval() {
2915
+ async grantBesideKnowledge(verb) {
2804
2916
  for (const charter of await this.listRoles()) {
2805
- if (charter.verbs.includes("write_knowledge") && !charter.verbs.includes("remove_knowledge")) {
2806
- await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, "remove_knowledge"] }, { verbsAdded: ["remove_knowledge"] });
2917
+ if (charter.verbs.includes("write_knowledge") && !charter.verbs.includes(verb)) {
2918
+ await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
2919
+ }
2920
+ }
2921
+ }
2922
+ /** A dashboard still saying agents may edit its file says how it changes now. */
2923
+ async rewordDashboards() {
2924
+ for (const slug of await listDirs(this.paths.projects())) {
2925
+ const file = this.paths.dashboard(slug);
2926
+ if (!await exists(file)) {
2927
+ continue;
2928
+ }
2929
+ const { data, body } = await readLooseMarkdown(file);
2930
+ if (body.includes(OLD_DASHBOARD_LINE)) {
2931
+ await writeMarkdown(file, data, body.replace(OLD_DASHBOARD_LINE, DASHBOARD_LINE));
2807
2932
  }
2808
2933
  }
2809
2934
  }
@@ -3565,6 +3690,7 @@ ${charter.purpose}
3565
3690
  if (await exists(file)) {
3566
3691
  throw new BoardError("ALREADY_EXISTS", `project ${input.slug} already exists`);
3567
3692
  }
3693
+ assertLandsByPullRequest(input.onDone ?? "none", input.repo ?? null, `project ${input.slug}`);
3568
3694
  const project2 = ProjectSchema.parse({
3569
3695
  slug: input.slug,
3570
3696
  name: input.name ?? input.slug,
@@ -3585,9 +3711,9 @@ ${charter.purpose}
3585
3711
  await ensureDir(this.paths.tasks(project2.slug));
3586
3712
  await ensureDir(this.paths.threads(project2.slug));
3587
3713
  await ensureDir(this.paths.projectKnowledge(project2.slug));
3588
- await writeMarkdown(this.paths.dashboard(project2.slug), { project: project2.slug, updatedAt: this.now().toISOString() }, `# ${project2.name} dashboard
3714
+ await writeMarkdown(this.paths.dashboard(project2.slug), { project: project2.slug, revision: 0, updatedAt: this.now().toISOString() }, `# ${project2.name} dashboard
3589
3715
 
3590
- Agents may edit this file. It is rendered by the board UI.
3716
+ ${DASHBOARD_LINE}
3591
3717
  `);
3592
3718
  await this.updateAgent(USER_NAME, (user2) => ({
3593
3719
  ...user2,
@@ -3692,6 +3818,7 @@ Agents may edit this file. It is rendered by the board UI.
3692
3818
  ...patch.os === void 0 ? {} : { os: patch.os },
3693
3819
  ...patch.clis === void 0 ? {} : { clis: [...patch.clis] },
3694
3820
  ...patch.capabilities === void 0 ? {} : { capabilities: [...patch.capabilities] },
3821
+ ...patch.version === void 0 ? {} : { version: patch.version },
3695
3822
  lastSeen: this.now().toISOString()
3696
3823
  });
3697
3824
  await writeMarkdown(file, next, doc.body);
@@ -4166,6 +4293,42 @@ ${task2.body}`;
4166
4293
  throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${project2}`);
4167
4294
  }
4168
4295
  }
4296
+ /**
4297
+ * Replaces a project's dashboard, by a member or the user. A turn reads the dashboard from the
4298
+ * copy of the projection pulled when it started, so a revision other than the current one means
4299
+ * someone updated it since; the refusal carries the current text, which that copy lacks.
4300
+ */
4301
+ async updateDashboard(actor, input) {
4302
+ const args = VerbInputs.update_dashboard.parse(input);
4303
+ await this.authorize(actor, "update_dashboard");
4304
+ return this.mutex.run(async () => {
4305
+ const project2 = await this.readActiveProject(args.project);
4306
+ if (actor.role !== USER_ROLE && !project2.members.includes(actor.name)) {
4307
+ throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${args.project}`);
4308
+ }
4309
+ const current = await this.dashboardOf(args.project);
4310
+ if (args.revision !== current.revision) {
4311
+ const by = current.updatedBy === void 0 ? "" : `, updated by ${current.updatedBy} at ${current.updatedAt ?? "an unknown time"}`;
4312
+ throw new BoardError("INVALID_STATE", `the dashboard of ${args.project} is at revision ${current.revision}${by}, not ${args.revision}. Merge your changes into its current text, below, and update again with revision ${current.revision}.
4313
+
4314
+ ${current.body}`);
4315
+ }
4316
+ const updated = DashboardSchema.parse({
4317
+ project: args.project,
4318
+ revision: current.revision + 1,
4319
+ updatedBy: actor.name,
4320
+ updatedAt: this.now().toISOString(),
4321
+ body: args.body
4322
+ });
4323
+ const { body, ...frontmatter } = updated;
4324
+ await writeMarkdown(this.paths.dashboard(args.project), frontmatter, body);
4325
+ await this.events.append("dashboard.updated", actor.name, {
4326
+ project: args.project,
4327
+ revision: updated.revision
4328
+ });
4329
+ return updated;
4330
+ });
4331
+ }
4169
4332
  async openThread(actor, input) {
4170
4333
  const args = VerbInputs.open_thread.parse(input);
4171
4334
  await this.authorize(actor, "open_thread");
@@ -4547,6 +4710,9 @@ ${args.summary}`);
4547
4710
  completedAt: ts
4548
4711
  };
4549
4712
  const next = current.stages[index + 1];
4713
+ if (next === void 0 && current.onDone === "ghpr" && current.pullRequest === void 0) {
4714
+ throw new BoardError("INVALID_STATE", `task ${current.id} lands through its pull request: link it with update_task pull_request before the last stage completes, or have the user, the steward, or the concierge set the task's on_done to none`);
4715
+ }
4550
4716
  if (next === void 0 && current.onDone === "none") {
4551
4717
  this.assertNoTurnCut(actor, `task ${current.id} cannot end`, (turn2) => turn2.thread === current.id);
4552
4718
  }
@@ -4663,6 +4829,7 @@ ${args.summary}`);
4663
4829
  const guarded = gateChanges(ahead, replanned);
4664
4830
  if (onDone !== current.onDone) {
4665
4831
  guarded.push("change the completion effect");
4832
+ assertLandsByPullRequest(onDone, (await this.readProject(current.project)).repo, `project ${current.project}`);
4666
4833
  }
4667
4834
  if (guarded.length > 0) {
4668
4835
  this.assertMayGate(actor, guarded.join(", "));
@@ -4751,6 +4918,12 @@ ${args.summary}`);
4751
4918
  }
4752
4919
  next = { ...next, blockedBy: [...args.blocked_by] };
4753
4920
  }
4921
+ if (args.pull_request !== void 0) {
4922
+ next = {
4923
+ ...next,
4924
+ pullRequest: await this.pullRequestLink(actor, current, args.pull_request)
4925
+ };
4926
+ }
4754
4927
  if (args.note !== void 0) {
4755
4928
  const step = moved !== null ? { action: "returned", stage: moved.from, to: moved.to } : next.status === "abandoned" ? { action: "abandoned", stage: current.stage, to: null } : void 0;
4756
4929
  await this.postTaskNote(actor.name, current, args.note, step);
@@ -4776,12 +4949,46 @@ ${args.summary}`);
4776
4949
  project: location.project,
4777
4950
  from: current.status,
4778
4951
  to: task2.status,
4779
- note: args.note ?? null
4952
+ note: args.note ?? null,
4953
+ ...args.pull_request === void 0 ? {} : { pullRequest: task2.pullRequest?.url ?? null }
4780
4954
  });
4781
4955
  }
4782
4956
  return task2;
4783
4957
  });
4784
4958
  }
4959
+ /**
4960
+ * The pull request a task is linked to, or none for `null`: one on the repository of its project,
4961
+ * by a member of the project or a planner, while the task is in play and before it lands.
4962
+ */
4963
+ async pullRequestLink(actor, task2, link) {
4964
+ this.assertInPlay(task2);
4965
+ if (task2.completing) {
4966
+ throw new BoardError("INVALID_STATE", `task ${task2.id} is landing, so its pull request no longer changes`);
4967
+ }
4968
+ if (!PLANNING_ROLES.includes(actor.role)) {
4969
+ const agent2 = await this.readAgent(actor.name);
4970
+ if (!agent2.memberships.includes(task2.project)) {
4971
+ throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${task2.project}`);
4972
+ }
4973
+ }
4974
+ if (link === null) {
4975
+ return void 0;
4976
+ }
4977
+ const project2 = await this.readProject(task2.project);
4978
+ const repository = githubRepository(project2.repo);
4979
+ const named = parsePullRequestUrl(link.url);
4980
+ if (repository === null || named === null || !sameRepository(repository, named)) {
4981
+ throw new BoardError("VALIDATION", `${link.url} is not a pull request of ${project2.slug}'s repository${project2.repo === null ? ", since it has no remote" : ` at ${project2.repo}`}`);
4982
+ }
4983
+ return PullRequestLinkSchema.parse({
4984
+ url: link.url,
4985
+ number: named.number,
4986
+ ...link.merge_subject === void 0 ? {} : { subject: link.merge_subject },
4987
+ ...link.merge_body === void 0 ? {} : { body: link.merge_body },
4988
+ linkedBy: actor.name,
4989
+ linkedAt: this.now().toISOString()
4990
+ });
4991
+ }
4785
4992
  async getTask(actor, input) {
4786
4993
  const args = VerbInputs.get_task.parse(input);
4787
4994
  await this.authorize(actor, "get_task");
@@ -4839,6 +5046,13 @@ ${args.summary}`);
4839
5046
  proposedBy: actor.name,
4840
5047
  status: "proposed",
4841
5048
  createdAt: this.now().toISOString(),
5049
+ ...actor.scope === void 0 ? {} : {
5050
+ filedFrom: {
5051
+ scope: actor.scope,
5052
+ ...actor.thread === void 0 ? {} : { thread: actor.thread },
5053
+ ...actor.channel === void 0 ? {} : { channel: actor.channel }
5054
+ }
5055
+ },
4842
5056
  charter: parsed.data
4843
5057
  }),
4844
5058
  body: args.rationale
@@ -4892,6 +5106,9 @@ ${rationale}`}`);
4892
5106
  }
4893
5107
  return this.mutex.run(async () => {
4894
5108
  const previous = await this.readActiveProject(args.project);
5109
+ if (args.on_done !== void 0) {
5110
+ assertLandsByPullRequest(args.on_done, previous.repo, `project ${previous.slug}`);
5111
+ }
4895
5112
  const project2 = await this.updateProject(args.project, (current) => ({
4896
5113
  ...current,
4897
5114
  ...args.on_done === void 0 ? {} : { onDone: args.on_done },
@@ -5073,6 +5290,8 @@ ${rationale}`}`);
5073
5290
  return this.createChannel(actor, VerbInputs.create_channel.parse(input));
5074
5291
  case "archive_channel":
5075
5292
  return this.archiveChannel(actor, VerbInputs.archive_channel.parse(input));
5293
+ case "update_dashboard":
5294
+ return this.updateDashboard(actor, VerbInputs.update_dashboard.parse(input));
5076
5295
  default:
5077
5296
  throw new BoardError("VALIDATION", `unknown verb ${String(verb)}`);
5078
5297
  }
@@ -5334,6 +5553,29 @@ ${rationale}`}`);
5334
5553
  });
5335
5554
  });
5336
5555
  }
5556
+ /**
5557
+ * Records how closing an abandoned `ghpr` task's pull request on GitHub went, once: the task's
5558
+ * link keeps it, so a restart does not try again.
5559
+ */
5560
+ async recordPullRequestClosed(actor, input) {
5561
+ return this.mutex.run(async () => {
5562
+ const location = await this.findTask(input.taskId);
5563
+ const current = location.task;
5564
+ const pull = current.pullRequest;
5565
+ if (pull === void 0 || pull.closed !== void 0) {
5566
+ return current;
5567
+ }
5568
+ const task2 = await this.writeTask(location.project, {
5569
+ ...current,
5570
+ pullRequest: {
5571
+ ...pull,
5572
+ closed: { at: this.now().toISOString(), ok: input.ok, detail: input.detail }
5573
+ }
5574
+ });
5575
+ await this.events.append(input.ok ? "pull_request.closed" : "pull_request.close_failed", actor.name, { project: location.project, taskId: task2.id, url: pull.url, detail: input.detail });
5576
+ return task2;
5577
+ });
5578
+ }
5337
5579
  /**
5338
5580
  * Records that a citizen's workspace in a conversation that has ended holds work on no branch, so
5339
5581
  * the scheduler gives the citizen a closing turn there to decide about it. False, recording
@@ -5377,8 +5619,8 @@ ${rationale}`}`);
5377
5619
  }
5378
5620
  /**
5379
5621
  * Records the outcome of a completing task's effect: done, or waiting again at its last stage so
5380
- * its participants can reshape the plan. A merge's outcome is posted to the task's thread, and a
5381
- * landed one announced in the project's general channel.
5622
+ * its participants can reshape the plan. A landing's outcome, a merge's or a pull request's, is
5623
+ * posted to the task's thread, and a landed one announced in the task's channel.
5382
5624
  */
5383
5625
  async finishCompletion(actor, input) {
5384
5626
  return this.mutex.run(async () => {
@@ -5388,8 +5630,8 @@ ${rationale}`}`);
5388
5630
  throw new BoardError("INVALID_STATE", `task ${current.id} is not completing`);
5389
5631
  }
5390
5632
  const ts = this.now().toISOString();
5391
- const merged = current.onDone === "merge";
5392
- if (merged) {
5633
+ const landed = current.onDone !== "none";
5634
+ if (landed) {
5393
5635
  await this.postTaskNote(actor.name, current, input.ok ? `${sentence(input.detail)}.` : `Landing failed: ${sentence(input.detail)}. The task waits at its last stage for its participants to reshape the plan.`, input.ok ? { action: "landed", stage: current.stage, to: null } : { action: "reopened", stage: current.stage, to: current.stage });
5394
5636
  }
5395
5637
  if (input.ok) {
@@ -5409,7 +5651,7 @@ ${rationale}`}`);
5409
5651
  for (const name of new Set(task3.stages.flatMap((stage) => stage.completedBy ?? []))) {
5410
5652
  await this.refreshMember(name);
5411
5653
  }
5412
- if (merged) {
5654
+ if (landed) {
5413
5655
  await this.appendMessage(actor.name, channelRef(location.project, task3.channel), `Task ${task3.id} "${task3.title}" is done: ${sentence(input.detail)}.`);
5414
5656
  }
5415
5657
  return task3;
@@ -5439,15 +5681,31 @@ ${rationale}`}`);
5439
5681
  async writeState(name, value) {
5440
5682
  await this.mutex.run(() => writeJson(path6.join(this.paths.state(), `${name}.json`), value));
5441
5683
  }
5442
- /** The project's editable dashboard: frontmatter plus markdown body. Empty when none exists. */
5684
+ /** The project's dashboard, empty at revision 0 when it has none. */
5443
5685
  async readDashboard(slug) {
5444
5686
  await this.readProject(slug);
5687
+ return this.dashboardOf(slug);
5688
+ }
5689
+ /**
5690
+ * Read loosely, since agents wrote dashboards by hand before they had a verb: a revision or an
5691
+ * author that does not parse counts as none.
5692
+ */
5693
+ async dashboardOf(slug) {
5445
5694
  const file = this.paths.dashboard(slug);
5446
5695
  if (!await exists(file)) {
5447
- return { data: { project: slug }, body: "" };
5696
+ return { project: slug, revision: 0, body: "" };
5448
5697
  }
5449
5698
  const { data, body } = await readLooseMarkdown(file);
5450
- return { data, body };
5699
+ const revision = z12.number().int().min(0).safeParse(data["revision"]);
5700
+ const updatedBy = NameSchema.safeParse(data["updatedBy"]);
5701
+ const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
5702
+ return {
5703
+ project: slug,
5704
+ revision: revision.success ? revision.data : 0,
5705
+ ...updatedBy.success ? { updatedBy: updatedBy.data } : {},
5706
+ ...updatedAt.success ? { updatedAt: updatedAt.data } : {},
5707
+ body
5708
+ };
5451
5709
  }
5452
5710
  async listRunners() {
5453
5711
  const runners = [];
@@ -6550,7 +6808,8 @@ ${charter.purpose}
6550
6808
  proposalId,
6551
6809
  outcome,
6552
6810
  kind: proposal2.kind,
6553
- proposedBy: proposal2.proposedBy
6811
+ proposedBy: proposal2.proposedBy,
6812
+ ...proposal2.filedFrom === void 0 ? {} : { filedFrom: proposal2.filedFrom }
6554
6813
  });
6555
6814
  if (provision !== void 0) {
6556
6815
  await this.events.append("proposal.provisioned", actor.name, {
@@ -6794,7 +7053,7 @@ runner.command("add <name>").description("Register a runner and print its token
6794
7053
  runner.command("list").description("List runners and what they offer").action(async () => {
6795
7054
  const board = await open();
6796
7055
  const runners = await board.listRunners();
6797
- print(runners, () => runners.length === 0 ? "No runners yet; add one with `runner add <name>`." : runners.map((each) => `${each.name} ${each.status} ${each.os} ${each.clis.join(",") || "no CLIs yet"}${each.capabilities.length === 0 ? "" : ` ${each.capabilities.join(",")}`}`).join("\n"));
7056
+ print(runners, () => runners.length === 0 ? "No runners yet; add one with `runner add <name>`." : runners.map((each) => `${each.name} ${each.status} ${each.os}${each.version === void 0 ? "" : ` v${each.version}`} ${each.clis.join(",") || "no CLIs yet"}${each.capabilities.length === 0 ? "" : ` ${each.capabilities.join(",")}`}`).join("\n"));
6798
7057
  });
6799
7058
  var agent = program.command("agent").description("Manage agents");
6800
7059
  agent.command("add <name>").description("Add an agent and print its board token once").requiredOption("--role <role>", "role charter name").option("--cli <cli>", "claude or codex").option("--model <model>", "model to run the CLI with; the CLI's own default otherwise").option("--runner <name>", "runner preferred for its turns; any with its CLI otherwise").option("-p, --project <slug...>", "project memberships").action(async (name, opts) => {