@kubitnodes/stellaris 0.3.11 → 0.3.12
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.
- package/dist/cli/index.js +234 -27
- package/dist/server/index.js +374 -58
- package/package.json +1 -1
- package/web/dist/assets/{BackLink-C-vad6K2.js → BackLink-BlJvN2RA.js} +1 -1
- package/web/dist/assets/ChannelForms-CmFxBXNK.js +1 -0
- package/web/dist/assets/ChannelView-4s--IwKC.js +1 -0
- package/web/dist/assets/CitizenView-B5-LVVCG.js +2 -0
- package/web/dist/assets/CitizensView-B8lVGSBD.js +1 -0
- package/web/dist/assets/CliIcon-CT5ljQDC.js +1 -0
- package/web/dist/assets/KnowledgeView-DbI8DIZb.js +1 -0
- package/web/dist/assets/{Markdown-J2pAPfCa.js → Markdown-B5zEdYuO.js} +4 -4
- package/web/dist/assets/MessageItem-DuxSZwn0.js +1 -0
- package/web/dist/assets/MetricsView-D8D-y-sz.js +1 -0
- package/web/dist/assets/NeedsYouView-Buwq8-_l.js +1 -0
- package/web/dist/assets/Overview-CrQ8ybpq.js +1 -0
- package/web/dist/assets/{Pane-caYWCiyo.js → Pane-CFoSKOo5.js} +1 -1
- package/web/dist/assets/ProjectView-BBQg-ZxC.js +1 -0
- package/web/dist/assets/ProposalView-BjPHVxj4.js +1 -0
- package/web/dist/assets/ProposalsView-DjyZpZe0.js +1 -0
- package/web/dist/assets/RunnersView-2h5NWUuD.js +1 -0
- package/web/dist/assets/SocietyView-BuaSb_3d.js +1 -0
- package/web/dist/assets/{SubjectThread-D_L3vB-A.js → SubjectThread-C1mHvHMI.js} +1 -1
- package/web/dist/assets/TaskFileView-CeEzbSCL.js +4 -0
- package/web/dist/assets/TaskView-yL9hCM7G.js +3 -0
- package/web/dist/assets/TasksView-DCVU5oZn.js +1 -0
- package/web/dist/assets/ThreadCard-CXXf9qfA.js +1 -0
- package/web/dist/assets/ThreadForms-CTBaAJ57.js +1 -0
- package/web/dist/assets/ThreadView-zg8KqF3t.js +1 -0
- package/web/dist/assets/{citizen-BtOZq62_.js → citizen-CImM6WB6.js} +1 -1
- package/web/dist/assets/compose-BG_-Tp87.js +1 -0
- package/web/dist/assets/{format-EfoceZkm.js → format-BWr3zKWg.js} +1 -1
- package/web/dist/assets/governance-cDTg2_iq.js +1 -0
- package/web/dist/assets/index-Cxqhrg2v.js +23 -0
- package/web/dist/assets/index-VVnGdAXc.css +2 -0
- package/web/dist/assets/link-CFdt17Sp.js +1 -0
- package/web/dist/assets/mentions-BCdV0bMK.js +1 -0
- package/web/dist/assets/{tasks-Duke4U7N.js → tasks-C10zwkED.js} +1 -1
- package/web/dist/assets/useMatch-BpnmzT6s.js +1 -0
- package/web/dist/assets/{useStickyScroll-n5d7sdPZ.js → useStickyScroll-BjqdEt_f.js} +1 -1
- package/web/dist/index.html +10 -4
- package/web/dist/assets/ChannelForms-jNbbNnIC.js +0 -1
- package/web/dist/assets/ChannelView-kLoegYUQ.js +0 -1
- package/web/dist/assets/CitizenView-Cb8f2mB7.js +0 -2
- package/web/dist/assets/CitizensView-Ds2rojfa.js +0 -1
- package/web/dist/assets/KnowledgeView-k10TU0jc.js +0 -1
- package/web/dist/assets/MessageItem-BjM8BdpK.js +0 -1
- package/web/dist/assets/MetricsView-D9N54rV7.js +0 -1
- package/web/dist/assets/NeedsYouView-ML8i7Pl7.js +0 -1
- package/web/dist/assets/Overview-BBXvRhfR.js +0 -1
- package/web/dist/assets/ProjectView-sDpFNjcW.js +0 -1
- package/web/dist/assets/ProposalView-Bc91NjKC.js +0 -1
- package/web/dist/assets/ProposalsView-Dg_bEryX.js +0 -1
- package/web/dist/assets/RunnersView-Dv5nbwYv.js +0 -1
- package/web/dist/assets/SocietyView-ClaEWgOq.js +0 -1
- package/web/dist/assets/TaskFileView-B19YtrtW.js +0 -4
- package/web/dist/assets/TaskView-FYSoCb3T.js +0 -1
- package/web/dist/assets/TasksView-CniXTU3R.js +0 -1
- package/web/dist/assets/ThreadCard-DCpvhSlc.js +0 -1
- package/web/dist/assets/ThreadForms-DUGkAwTp.js +0 -1
- package/web/dist/assets/ThreadView-D8eXlVbC.js +0 -1
- package/web/dist/assets/index-BeOGtAz0.css +0 -2
- 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,39 @@ 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
|
+
});
|
|
189
223
|
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
224
|
var StageIdSchema = z3.string().regex(/^s[1-9][0-9]*$/, "expected a stage id such as s1");
|
|
191
225
|
var stageFields = {
|
|
@@ -300,6 +334,8 @@ var TaskFrontmatterSchema = z3.object({
|
|
|
300
334
|
/** The highest stage number issued, so a removed stage's id is never reused. */
|
|
301
335
|
stageSeq: z3.number().int().positive(),
|
|
302
336
|
onDone: CompletionEffectSchema.default("none"),
|
|
337
|
+
/** The pull request a `ghpr` task lands through. */
|
|
338
|
+
pullRequest: PullRequestLinkSchema.optional(),
|
|
303
339
|
/** The last stage is complete and the completion effect is running. */
|
|
304
340
|
completing: z3.boolean().default(false),
|
|
305
341
|
/** The latest send-back while its rework lasts: cleared once the task reaches that stage again. */
|
|
@@ -392,6 +428,8 @@ var RunnerSchema = z3.object({
|
|
|
392
428
|
os: RunnerOsSchema,
|
|
393
429
|
clis: z3.array(CliKindSchema),
|
|
394
430
|
capabilities: z3.array(z3.string()),
|
|
431
|
+
/** The release the runner reported when it last registered; absent until it has. */
|
|
432
|
+
version: z3.string().optional(),
|
|
395
433
|
status: z3.enum(["connected", "disconnected"]),
|
|
396
434
|
lastSeen: IsoDateTimeSchema.optional()
|
|
397
435
|
});
|
|
@@ -570,6 +608,14 @@ var RemovedKnowledgeSchema = z3.object({
|
|
|
570
608
|
removedBy: NameSchema,
|
|
571
609
|
removedAt: IsoDateTimeSchema
|
|
572
610
|
});
|
|
611
|
+
var DashboardSchema = z3.object({
|
|
612
|
+
project: NameSchema,
|
|
613
|
+
revision: z3.number().int().min(0),
|
|
614
|
+
/** Absent until someone updates the dashboard the project began with. */
|
|
615
|
+
updatedBy: NameSchema.optional(),
|
|
616
|
+
updatedAt: IsoDateTimeSchema.optional(),
|
|
617
|
+
body: z3.string()
|
|
618
|
+
});
|
|
573
619
|
var ProposalFrontmatterSchema = z3.object({
|
|
574
620
|
id: UlidSchema,
|
|
575
621
|
kind: ProposalKindSchema,
|
|
@@ -641,7 +687,16 @@ var VerbInputs = {
|
|
|
641
687
|
status: z4.enum(["abandoned"]).optional(),
|
|
642
688
|
/** Posted to the task's thread as the caller, marked with the step when the task moved. */
|
|
643
689
|
note: z4.string().min(1).optional(),
|
|
644
|
-
blocked_by: z4.array(UlidSchema).optional()
|
|
690
|
+
blocked_by: z4.array(UlidSchema).optional(),
|
|
691
|
+
/**
|
|
692
|
+
* The pull request a `ghpr` task lands through, on its project's repository, with the merge
|
|
693
|
+
* commit's subject and body; null unlinks it.
|
|
694
|
+
*/
|
|
695
|
+
pull_request: z4.object({
|
|
696
|
+
url: PullRequestUrlSchema,
|
|
697
|
+
merge_subject: MergeSubjectSchema.optional(),
|
|
698
|
+
merge_body: z4.string().max(2e4).optional()
|
|
699
|
+
}).nullable().optional()
|
|
645
700
|
}),
|
|
646
701
|
get_task: z4.object({ task_id: UlidSchema }),
|
|
647
702
|
subscribe: z4.object({ channel: ChannelRefSchema }),
|
|
@@ -695,7 +750,14 @@ var VerbInputs = {
|
|
|
695
750
|
name: NameSchema,
|
|
696
751
|
purpose: z4.string().min(1)
|
|
697
752
|
}),
|
|
698
|
-
archive_channel: z4.object({ channel: ChannelRefSchema, reason: z4.string().min(1) })
|
|
753
|
+
archive_channel: z4.object({ channel: ChannelRefSchema, reason: z4.string().min(1) }),
|
|
754
|
+
update_dashboard: z4.object({
|
|
755
|
+
project: NameSchema,
|
|
756
|
+
/** The whole dashboard as markdown; the board keeps its frontmatter. */
|
|
757
|
+
body: z4.string().min(1),
|
|
758
|
+
/** The `revision` in the frontmatter of the dashboard you read, 0 when it names none. */
|
|
759
|
+
revision: z4.number().int().min(0)
|
|
760
|
+
})
|
|
699
761
|
};
|
|
700
762
|
|
|
701
763
|
// packages/shared/dist/events.js
|
|
@@ -735,6 +797,7 @@ var BOARD_EVENT_TYPES = [
|
|
|
735
797
|
"agent.left",
|
|
736
798
|
"knowledge.written",
|
|
737
799
|
"knowledge.removed",
|
|
800
|
+
"dashboard.updated",
|
|
738
801
|
"skill.promoted",
|
|
739
802
|
"runner.added",
|
|
740
803
|
"user.signed_in",
|
|
@@ -898,7 +961,12 @@ var PendingEnrollmentSchema = z6.object({
|
|
|
898
961
|
var ProjectRepoSchema = z6.object({
|
|
899
962
|
slug: NameSchema,
|
|
900
963
|
origin: z6.string().nullable(),
|
|
901
|
-
defaultBranch: z6.string().min(1)
|
|
964
|
+
defaultBranch: z6.string().min(1),
|
|
965
|
+
/**
|
|
966
|
+
* The board lands finished tasks on the default branch, itself or through their pull requests,
|
|
967
|
+
* so the runner lets nothing else move it.
|
|
968
|
+
*/
|
|
969
|
+
boardLands: z6.boolean()
|
|
902
970
|
});
|
|
903
971
|
var TurnWorkspaceSchema = z6.discriminatedUnion("kind", [
|
|
904
972
|
z6.object({
|
|
@@ -972,7 +1040,17 @@ var TurnOutcomeSchema = z6.object({
|
|
|
972
1040
|
var TurnAckSchema = z6.object({ dropWorkspace: z6.boolean() });
|
|
973
1041
|
var LandRequestSchema = z6.object({
|
|
974
1042
|
repo: ProjectRepoSchema,
|
|
975
|
-
branch: z6.string().min(1)
|
|
1043
|
+
branch: z6.string().min(1),
|
|
1044
|
+
/**
|
|
1045
|
+
* For a `ghpr` task, the pull request to merge in place of the branch, which must hold the
|
|
1046
|
+
* branch's files, and the merge commit's subject and body, the pull request's title and an
|
|
1047
|
+
* empty body when left out.
|
|
1048
|
+
*/
|
|
1049
|
+
pullRequest: z6.object({
|
|
1050
|
+
url: z6.string().min(1),
|
|
1051
|
+
subject: z6.string().min(1).optional(),
|
|
1052
|
+
body: z6.string().optional()
|
|
1053
|
+
}).optional()
|
|
976
1054
|
});
|
|
977
1055
|
var MergeOutcomeSchema = z6.object({ ok: z6.boolean(), detail: z6.string() });
|
|
978
1056
|
var ModelListSchema = z6.array(ModelOptionSchema);
|
|
@@ -1385,7 +1463,7 @@ var RunnerConfigSchema = z10.object({
|
|
|
1385
1463
|
});
|
|
1386
1464
|
|
|
1387
1465
|
// packages/shared/dist/version.js
|
|
1388
|
-
var STELLARIS_VERSION = "0.3.
|
|
1466
|
+
var STELLARIS_VERSION = "0.3.12";
|
|
1389
1467
|
|
|
1390
1468
|
// packages/board-core/dist/errors.js
|
|
1391
1469
|
var BoardError = class extends Error {
|
|
@@ -2566,6 +2644,9 @@ var THREAD_CONVERSATIONS = "thread-conversations";
|
|
|
2566
2644
|
var ASKS_CHANNEL_OPENED = "asks-channel-opened";
|
|
2567
2645
|
var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
|
|
2568
2646
|
var CHANNEL_CONVERSATIONS = "channel-conversations";
|
|
2647
|
+
var DASHBOARD_UPDATES = "dashboard-updates-granted";
|
|
2648
|
+
var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
|
|
2649
|
+
var DASHBOARD_LINE = "The project's members keep this page current with update_dashboard.";
|
|
2569
2650
|
var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
|
|
2570
2651
|
var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
|
|
2571
2652
|
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 +2683,11 @@ function memberToAgentInput(member) {
|
|
|
2602
2683
|
...member.seedInstructions === void 0 ? {} : { seedInstructions: member.seedInstructions }
|
|
2603
2684
|
};
|
|
2604
2685
|
}
|
|
2686
|
+
function assertLandsByPullRequest(effect, repo, where) {
|
|
2687
|
+
if (effect === "ghpr" && githubRepository(repo) === null) {
|
|
2688
|
+
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)`);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2605
2691
|
function stageFrom(planned, id) {
|
|
2606
2692
|
return {
|
|
2607
2693
|
id,
|
|
@@ -2759,7 +2845,8 @@ var Board = class _Board {
|
|
|
2759
2845
|
THREAD_CONVERSATIONS,
|
|
2760
2846
|
ASKS_CHANNEL_OPENED,
|
|
2761
2847
|
KNOWLEDGE_REMOVAL,
|
|
2762
|
-
CHANNEL_CONVERSATIONS
|
|
2848
|
+
CHANNEL_CONVERSATIONS,
|
|
2849
|
+
DASHBOARD_UPDATES
|
|
2763
2850
|
].filter((name) => !applied.includes(name));
|
|
2764
2851
|
if (pending.length === 0) {
|
|
2765
2852
|
return;
|
|
@@ -2789,21 +2876,38 @@ var Board = class _Board {
|
|
|
2789
2876
|
await this.openAsksChannel();
|
|
2790
2877
|
}
|
|
2791
2878
|
if (pending.includes(KNOWLEDGE_REMOVAL)) {
|
|
2792
|
-
await this.
|
|
2879
|
+
await this.grantBesideKnowledge("remove_knowledge");
|
|
2793
2880
|
}
|
|
2794
2881
|
if (pending.includes(CHANNEL_CONVERSATIONS)) {
|
|
2795
2882
|
await this.startChannelCursors();
|
|
2796
2883
|
}
|
|
2884
|
+
if (pending.includes(DASHBOARD_UPDATES)) {
|
|
2885
|
+
await this.grantBesideKnowledge("update_dashboard");
|
|
2886
|
+
await this.rewordDashboards();
|
|
2887
|
+
}
|
|
2797
2888
|
await writeJson(file, { applied: [...applied, ...pending] });
|
|
2798
2889
|
}
|
|
2799
2890
|
/**
|
|
2800
|
-
*
|
|
2801
|
-
*
|
|
2891
|
+
* Grants `verb` once to every charter granting `write_knowledge`, the work roles' charters in
|
|
2892
|
+
* practice; seed roles have it from their seed.
|
|
2802
2893
|
*/
|
|
2803
|
-
async
|
|
2894
|
+
async grantBesideKnowledge(verb) {
|
|
2804
2895
|
for (const charter of await this.listRoles()) {
|
|
2805
|
-
if (charter.verbs.includes("write_knowledge") && !charter.verbs.includes(
|
|
2806
|
-
await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs,
|
|
2896
|
+
if (charter.verbs.includes("write_knowledge") && !charter.verbs.includes(verb)) {
|
|
2897
|
+
await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
/** A dashboard still saying agents may edit its file says how it changes now. */
|
|
2902
|
+
async rewordDashboards() {
|
|
2903
|
+
for (const slug of await listDirs(this.paths.projects())) {
|
|
2904
|
+
const file = this.paths.dashboard(slug);
|
|
2905
|
+
if (!await exists(file)) {
|
|
2906
|
+
continue;
|
|
2907
|
+
}
|
|
2908
|
+
const { data, body } = await readLooseMarkdown(file);
|
|
2909
|
+
if (body.includes(OLD_DASHBOARD_LINE)) {
|
|
2910
|
+
await writeMarkdown(file, data, body.replace(OLD_DASHBOARD_LINE, DASHBOARD_LINE));
|
|
2807
2911
|
}
|
|
2808
2912
|
}
|
|
2809
2913
|
}
|
|
@@ -3565,6 +3669,7 @@ ${charter.purpose}
|
|
|
3565
3669
|
if (await exists(file)) {
|
|
3566
3670
|
throw new BoardError("ALREADY_EXISTS", `project ${input.slug} already exists`);
|
|
3567
3671
|
}
|
|
3672
|
+
assertLandsByPullRequest(input.onDone ?? "none", input.repo ?? null, `project ${input.slug}`);
|
|
3568
3673
|
const project2 = ProjectSchema.parse({
|
|
3569
3674
|
slug: input.slug,
|
|
3570
3675
|
name: input.name ?? input.slug,
|
|
@@ -3585,9 +3690,9 @@ ${charter.purpose}
|
|
|
3585
3690
|
await ensureDir(this.paths.tasks(project2.slug));
|
|
3586
3691
|
await ensureDir(this.paths.threads(project2.slug));
|
|
3587
3692
|
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
|
|
3693
|
+
await writeMarkdown(this.paths.dashboard(project2.slug), { project: project2.slug, revision: 0, updatedAt: this.now().toISOString() }, `# ${project2.name} dashboard
|
|
3589
3694
|
|
|
3590
|
-
|
|
3695
|
+
${DASHBOARD_LINE}
|
|
3591
3696
|
`);
|
|
3592
3697
|
await this.updateAgent(USER_NAME, (user2) => ({
|
|
3593
3698
|
...user2,
|
|
@@ -3692,6 +3797,7 @@ Agents may edit this file. It is rendered by the board UI.
|
|
|
3692
3797
|
...patch.os === void 0 ? {} : { os: patch.os },
|
|
3693
3798
|
...patch.clis === void 0 ? {} : { clis: [...patch.clis] },
|
|
3694
3799
|
...patch.capabilities === void 0 ? {} : { capabilities: [...patch.capabilities] },
|
|
3800
|
+
...patch.version === void 0 ? {} : { version: patch.version },
|
|
3695
3801
|
lastSeen: this.now().toISOString()
|
|
3696
3802
|
});
|
|
3697
3803
|
await writeMarkdown(file, next, doc.body);
|
|
@@ -4166,6 +4272,42 @@ ${task2.body}`;
|
|
|
4166
4272
|
throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${project2}`);
|
|
4167
4273
|
}
|
|
4168
4274
|
}
|
|
4275
|
+
/**
|
|
4276
|
+
* Replaces a project's dashboard, by a member or the user. A turn reads the dashboard from the
|
|
4277
|
+
* copy of the projection pulled when it started, so a revision other than the current one means
|
|
4278
|
+
* someone updated it since; the refusal carries the current text, which that copy lacks.
|
|
4279
|
+
*/
|
|
4280
|
+
async updateDashboard(actor, input) {
|
|
4281
|
+
const args = VerbInputs.update_dashboard.parse(input);
|
|
4282
|
+
await this.authorize(actor, "update_dashboard");
|
|
4283
|
+
return this.mutex.run(async () => {
|
|
4284
|
+
const project2 = await this.readActiveProject(args.project);
|
|
4285
|
+
if (actor.role !== USER_ROLE && !project2.members.includes(actor.name)) {
|
|
4286
|
+
throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${args.project}`);
|
|
4287
|
+
}
|
|
4288
|
+
const current = await this.dashboardOf(args.project);
|
|
4289
|
+
if (args.revision !== current.revision) {
|
|
4290
|
+
const by = current.updatedBy === void 0 ? "" : `, updated by ${current.updatedBy} at ${current.updatedAt ?? "an unknown time"}`;
|
|
4291
|
+
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}.
|
|
4292
|
+
|
|
4293
|
+
${current.body}`);
|
|
4294
|
+
}
|
|
4295
|
+
const updated = DashboardSchema.parse({
|
|
4296
|
+
project: args.project,
|
|
4297
|
+
revision: current.revision + 1,
|
|
4298
|
+
updatedBy: actor.name,
|
|
4299
|
+
updatedAt: this.now().toISOString(),
|
|
4300
|
+
body: args.body
|
|
4301
|
+
});
|
|
4302
|
+
const { body, ...frontmatter } = updated;
|
|
4303
|
+
await writeMarkdown(this.paths.dashboard(args.project), frontmatter, body);
|
|
4304
|
+
await this.events.append("dashboard.updated", actor.name, {
|
|
4305
|
+
project: args.project,
|
|
4306
|
+
revision: updated.revision
|
|
4307
|
+
});
|
|
4308
|
+
return updated;
|
|
4309
|
+
});
|
|
4310
|
+
}
|
|
4169
4311
|
async openThread(actor, input) {
|
|
4170
4312
|
const args = VerbInputs.open_thread.parse(input);
|
|
4171
4313
|
await this.authorize(actor, "open_thread");
|
|
@@ -4547,6 +4689,9 @@ ${args.summary}`);
|
|
|
4547
4689
|
completedAt: ts
|
|
4548
4690
|
};
|
|
4549
4691
|
const next = current.stages[index + 1];
|
|
4692
|
+
if (next === void 0 && current.onDone === "ghpr" && current.pullRequest === void 0) {
|
|
4693
|
+
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`);
|
|
4694
|
+
}
|
|
4550
4695
|
if (next === void 0 && current.onDone === "none") {
|
|
4551
4696
|
this.assertNoTurnCut(actor, `task ${current.id} cannot end`, (turn2) => turn2.thread === current.id);
|
|
4552
4697
|
}
|
|
@@ -4663,6 +4808,7 @@ ${args.summary}`);
|
|
|
4663
4808
|
const guarded = gateChanges(ahead, replanned);
|
|
4664
4809
|
if (onDone !== current.onDone) {
|
|
4665
4810
|
guarded.push("change the completion effect");
|
|
4811
|
+
assertLandsByPullRequest(onDone, (await this.readProject(current.project)).repo, `project ${current.project}`);
|
|
4666
4812
|
}
|
|
4667
4813
|
if (guarded.length > 0) {
|
|
4668
4814
|
this.assertMayGate(actor, guarded.join(", "));
|
|
@@ -4751,6 +4897,12 @@ ${args.summary}`);
|
|
|
4751
4897
|
}
|
|
4752
4898
|
next = { ...next, blockedBy: [...args.blocked_by] };
|
|
4753
4899
|
}
|
|
4900
|
+
if (args.pull_request !== void 0) {
|
|
4901
|
+
next = {
|
|
4902
|
+
...next,
|
|
4903
|
+
pullRequest: await this.pullRequestLink(actor, current, args.pull_request)
|
|
4904
|
+
};
|
|
4905
|
+
}
|
|
4754
4906
|
if (args.note !== void 0) {
|
|
4755
4907
|
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
4908
|
await this.postTaskNote(actor.name, current, args.note, step);
|
|
@@ -4776,12 +4928,46 @@ ${args.summary}`);
|
|
|
4776
4928
|
project: location.project,
|
|
4777
4929
|
from: current.status,
|
|
4778
4930
|
to: task2.status,
|
|
4779
|
-
note: args.note ?? null
|
|
4931
|
+
note: args.note ?? null,
|
|
4932
|
+
...args.pull_request === void 0 ? {} : { pullRequest: task2.pullRequest?.url ?? null }
|
|
4780
4933
|
});
|
|
4781
4934
|
}
|
|
4782
4935
|
return task2;
|
|
4783
4936
|
});
|
|
4784
4937
|
}
|
|
4938
|
+
/**
|
|
4939
|
+
* The pull request a task is linked to, or none for `null`: one on the repository of its project,
|
|
4940
|
+
* by a member of the project or a planner, while the task is in play and before it lands.
|
|
4941
|
+
*/
|
|
4942
|
+
async pullRequestLink(actor, task2, link) {
|
|
4943
|
+
this.assertInPlay(task2);
|
|
4944
|
+
if (task2.completing) {
|
|
4945
|
+
throw new BoardError("INVALID_STATE", `task ${task2.id} is landing, so its pull request no longer changes`);
|
|
4946
|
+
}
|
|
4947
|
+
if (!PLANNING_ROLES.includes(actor.role)) {
|
|
4948
|
+
const agent2 = await this.readAgent(actor.name);
|
|
4949
|
+
if (!agent2.memberships.includes(task2.project)) {
|
|
4950
|
+
throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${task2.project}`);
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
if (link === null) {
|
|
4954
|
+
return void 0;
|
|
4955
|
+
}
|
|
4956
|
+
const project2 = await this.readProject(task2.project);
|
|
4957
|
+
const repository = githubRepository(project2.repo);
|
|
4958
|
+
const named = parsePullRequestUrl(link.url);
|
|
4959
|
+
if (repository === null || named === null || !sameRepository(repository, named)) {
|
|
4960
|
+
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}`}`);
|
|
4961
|
+
}
|
|
4962
|
+
return PullRequestLinkSchema.parse({
|
|
4963
|
+
url: link.url,
|
|
4964
|
+
number: named.number,
|
|
4965
|
+
...link.merge_subject === void 0 ? {} : { subject: link.merge_subject },
|
|
4966
|
+
...link.merge_body === void 0 ? {} : { body: link.merge_body },
|
|
4967
|
+
linkedBy: actor.name,
|
|
4968
|
+
linkedAt: this.now().toISOString()
|
|
4969
|
+
});
|
|
4970
|
+
}
|
|
4785
4971
|
async getTask(actor, input) {
|
|
4786
4972
|
const args = VerbInputs.get_task.parse(input);
|
|
4787
4973
|
await this.authorize(actor, "get_task");
|
|
@@ -4892,6 +5078,9 @@ ${rationale}`}`);
|
|
|
4892
5078
|
}
|
|
4893
5079
|
return this.mutex.run(async () => {
|
|
4894
5080
|
const previous = await this.readActiveProject(args.project);
|
|
5081
|
+
if (args.on_done !== void 0) {
|
|
5082
|
+
assertLandsByPullRequest(args.on_done, previous.repo, `project ${previous.slug}`);
|
|
5083
|
+
}
|
|
4895
5084
|
const project2 = await this.updateProject(args.project, (current) => ({
|
|
4896
5085
|
...current,
|
|
4897
5086
|
...args.on_done === void 0 ? {} : { onDone: args.on_done },
|
|
@@ -5073,6 +5262,8 @@ ${rationale}`}`);
|
|
|
5073
5262
|
return this.createChannel(actor, VerbInputs.create_channel.parse(input));
|
|
5074
5263
|
case "archive_channel":
|
|
5075
5264
|
return this.archiveChannel(actor, VerbInputs.archive_channel.parse(input));
|
|
5265
|
+
case "update_dashboard":
|
|
5266
|
+
return this.updateDashboard(actor, VerbInputs.update_dashboard.parse(input));
|
|
5076
5267
|
default:
|
|
5077
5268
|
throw new BoardError("VALIDATION", `unknown verb ${String(verb)}`);
|
|
5078
5269
|
}
|
|
@@ -5377,8 +5568,8 @@ ${rationale}`}`);
|
|
|
5377
5568
|
}
|
|
5378
5569
|
/**
|
|
5379
5570
|
* 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
|
|
5381
|
-
* landed one announced in the
|
|
5571
|
+
* its participants can reshape the plan. A landing's outcome, a merge's or a pull request's, is
|
|
5572
|
+
* posted to the task's thread, and a landed one announced in the task's channel.
|
|
5382
5573
|
*/
|
|
5383
5574
|
async finishCompletion(actor, input) {
|
|
5384
5575
|
return this.mutex.run(async () => {
|
|
@@ -5388,8 +5579,8 @@ ${rationale}`}`);
|
|
|
5388
5579
|
throw new BoardError("INVALID_STATE", `task ${current.id} is not completing`);
|
|
5389
5580
|
}
|
|
5390
5581
|
const ts = this.now().toISOString();
|
|
5391
|
-
const
|
|
5392
|
-
if (
|
|
5582
|
+
const landed = current.onDone !== "none";
|
|
5583
|
+
if (landed) {
|
|
5393
5584
|
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
5585
|
}
|
|
5395
5586
|
if (input.ok) {
|
|
@@ -5409,7 +5600,7 @@ ${rationale}`}`);
|
|
|
5409
5600
|
for (const name of new Set(task3.stages.flatMap((stage) => stage.completedBy ?? []))) {
|
|
5410
5601
|
await this.refreshMember(name);
|
|
5411
5602
|
}
|
|
5412
|
-
if (
|
|
5603
|
+
if (landed) {
|
|
5413
5604
|
await this.appendMessage(actor.name, channelRef(location.project, task3.channel), `Task ${task3.id} "${task3.title}" is done: ${sentence(input.detail)}.`);
|
|
5414
5605
|
}
|
|
5415
5606
|
return task3;
|
|
@@ -5439,15 +5630,31 @@ ${rationale}`}`);
|
|
|
5439
5630
|
async writeState(name, value) {
|
|
5440
5631
|
await this.mutex.run(() => writeJson(path6.join(this.paths.state(), `${name}.json`), value));
|
|
5441
5632
|
}
|
|
5442
|
-
/** The project's
|
|
5633
|
+
/** The project's dashboard, empty at revision 0 when it has none. */
|
|
5443
5634
|
async readDashboard(slug) {
|
|
5444
5635
|
await this.readProject(slug);
|
|
5636
|
+
return this.dashboardOf(slug);
|
|
5637
|
+
}
|
|
5638
|
+
/**
|
|
5639
|
+
* Read loosely, since agents wrote dashboards by hand before they had a verb: a revision or an
|
|
5640
|
+
* author that does not parse counts as none.
|
|
5641
|
+
*/
|
|
5642
|
+
async dashboardOf(slug) {
|
|
5445
5643
|
const file = this.paths.dashboard(slug);
|
|
5446
5644
|
if (!await exists(file)) {
|
|
5447
|
-
return {
|
|
5645
|
+
return { project: slug, revision: 0, body: "" };
|
|
5448
5646
|
}
|
|
5449
5647
|
const { data, body } = await readLooseMarkdown(file);
|
|
5450
|
-
|
|
5648
|
+
const revision = z12.number().int().min(0).safeParse(data["revision"]);
|
|
5649
|
+
const updatedBy = NameSchema.safeParse(data["updatedBy"]);
|
|
5650
|
+
const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
|
|
5651
|
+
return {
|
|
5652
|
+
project: slug,
|
|
5653
|
+
revision: revision.success ? revision.data : 0,
|
|
5654
|
+
...updatedBy.success ? { updatedBy: updatedBy.data } : {},
|
|
5655
|
+
...updatedAt.success ? { updatedAt: updatedAt.data } : {},
|
|
5656
|
+
body
|
|
5657
|
+
};
|
|
5451
5658
|
}
|
|
5452
5659
|
async listRunners() {
|
|
5453
5660
|
const runners = [];
|
|
@@ -6794,7 +7001,7 @@ runner.command("add <name>").description("Register a runner and print its token
|
|
|
6794
7001
|
runner.command("list").description("List runners and what they offer").action(async () => {
|
|
6795
7002
|
const board = await open();
|
|
6796
7003
|
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"));
|
|
7004
|
+
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
7005
|
});
|
|
6799
7006
|
var agent = program.command("agent").description("Manage agents");
|
|
6800
7007
|
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) => {
|