@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.
- package/dist/cli/index.js +287 -28
- package/dist/server/index.js +542 -63
- package/package.json +1 -1
- package/web/dist/assets/{BackLink-C-vad6K2.js → BackLink-d0xz7ZLb.js} +1 -1
- package/web/dist/assets/ChannelForms-BIRwT9CY.js +1 -0
- package/web/dist/assets/ChannelView-BvMlzMiN.js +1 -0
- package/web/dist/assets/CitizenView-DNQwjpIp.js +2 -0
- package/web/dist/assets/CitizensView-Bv04iH-y.js +1 -0
- package/web/dist/assets/CliIcon-tYTwI_RQ.js +1 -0
- package/web/dist/assets/KnowledgeView-L0pdaM_2.js +1 -0
- package/web/dist/assets/{Markdown-J2pAPfCa.js → Markdown-BLiIulgZ.js} +4 -4
- package/web/dist/assets/MessageItem-CHrmjF7P.js +1 -0
- package/web/dist/assets/MetricsView-DG1alTgs.js +1 -0
- package/web/dist/assets/NeedsYouView-Doc0fKPi.js +1 -0
- package/web/dist/assets/Overview-ChsOgIEW.js +1 -0
- package/web/dist/assets/{Pane-caYWCiyo.js → Pane-MJgGXQBb.js} +1 -1
- package/web/dist/assets/ProjectView-CQTn7yDo.js +1 -0
- package/web/dist/assets/ProposalView-C92OJ_w1.js +1 -0
- package/web/dist/assets/ProposalsView-CSA8neXR.js +1 -0
- package/web/dist/assets/RunnersView-Dx21LDFu.js +1 -0
- package/web/dist/assets/SocietyView-CdVW2DgG.js +1 -0
- package/web/dist/assets/{SubjectThread-D_L3vB-A.js → SubjectThread-egXeFhT1.js} +1 -1
- package/web/dist/assets/TaskFileView-BqqzxJKb.js +4 -0
- package/web/dist/assets/TaskView-rks26vuF.js +3 -0
- package/web/dist/assets/TasksView-Ba5fPAvy.js +1 -0
- package/web/dist/assets/ThreadCard-D6i69_15.js +1 -0
- package/web/dist/assets/ThreadForms-ChTFRu-P.js +1 -0
- package/web/dist/assets/ThreadView-CnJPNS_8.js +1 -0
- package/web/dist/assets/{citizen-BtOZq62_.js → citizen-D-dxX00V.js} +1 -1
- package/web/dist/assets/compose-BgpUWGYE.js +1 -0
- package/web/dist/assets/{format-EfoceZkm.js → format-C3aawMM6.js} +1 -1
- package/web/dist/assets/governance-Di4MijNG.js +1 -0
- package/web/dist/assets/index-VVnGdAXc.css +2 -0
- package/web/dist/assets/index-krHkeIbX.js +23 -0
- package/web/dist/assets/link-DAMxBItI.js +1 -0
- package/web/dist/assets/mentions-BCdV0bMK.js +1 -0
- package/web/dist/assets/{tasks-Duke4U7N.js → tasks-bwj_EdwF.js} +1 -1
- package/web/dist/assets/useMatch-B-8IcuU6.js +1 -0
- package/web/dist/assets/{useStickyScroll-n5d7sdPZ.js → useStickyScroll-BILPUgGj.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/server/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
|
var VERB_DESCRIPTIONS = {
|
|
701
775
|
post_message: "Post a markdown message to a channel, or into a thread when thread_id is given; a thread's message goes to the thread's channel.",
|
|
@@ -706,24 +780,25 @@ var VERB_DESCRIPTIONS = {
|
|
|
706
780
|
create_task: "Create a task in a project with its plan: stages of {name, role or agent, gate}. Without stages it gets one stage, work, that anyone in the project may take. File it in a channel of the project, such as a release's, or general by default; its thread opens there with it, under the task's id, for everything said about the work.",
|
|
707
781
|
claim_task: "Hold the task's current stage. Claims are leases renewed by every turn that touches the task.",
|
|
708
782
|
release_task: "Let go of the stage you hold so someone else can take it.",
|
|
709
|
-
update_task: "Move a task back to an earlier stage, abandon it,
|
|
783
|
+
update_task: "Move a task back to an earlier stage, abandon it, set what it is blocked by, or link its pull request (pull_request: url, with the merge commit's merge_subject and merge_body), which a ghpr task needs before its last stage completes. A note is posted to the task's thread.",
|
|
710
784
|
get_task: "Read a task with its plan, its brief, and the messages of its thread.",
|
|
711
785
|
subscribe: "Subscribe to a channel. Subscriptions feed your digest; they never wake you.",
|
|
712
786
|
unsubscribe: "Unsubscribe from a channel.",
|
|
713
787
|
propose: "Propose a member, role, channel, reallocation, retirement, skill, or a project's archive. The charter shape per kind is in your instructions. Opens the proposal's thread in governance with your rationale; approval provisions it.",
|
|
714
788
|
approve: "Approve a proposal; the board then provisions it, posts the decision in the proposal's thread, and closes it. User and steward only, never on your own proposal.",
|
|
715
789
|
reject: "Reject a proposal with a reason, posted in the proposal's thread, which closes. User and steward only, never on your own proposal.",
|
|
716
|
-
create_project: "Create a project with its general channel: a slug, a display name, a git remote when one exists, and optionally a completion effect (none or
|
|
790
|
+
create_project: "Create a project with its general channel: a slug, a display name, a git remote when one exists, and optionally a completion effect (none, merge, or ghpr for a GitHub remote).",
|
|
717
791
|
join_project: "Join a project, or add another citizen to one when your role allows it. Membership gives the pair a worktree and an onboarding turn, and follows the project's general; the result lists its other open channels, to subscribe to when the work needs them.",
|
|
718
792
|
leave_project: "Leave a project, or remove another citizen from one when your role allows it.",
|
|
719
793
|
write_knowledge: "Write or replace a knowledge topic: durable facts every member of a project should know, or with project null the society's shared knowledge (steward and user). Not a message; use post_message for those.",
|
|
720
794
|
remove_knowledge: "Remove a knowledge topic that is wrong, superseded, or folded into another, so no turn or search finds it again. Whoever may write the topic may remove it; the board keeps its text aside for the user to restore.",
|
|
721
795
|
plan_task: "Reshape a task's plan from the current stage onward, or after it while someone holds it: keep a stage by passing its id, drop it by leaving it out, add one without an id. Gated stages and on_done: user, steward, concierge.",
|
|
722
796
|
advance_task: "Finish the stage you hold, with a note for the task's thread on what you did. The next stage becomes current; past the last one the task is done.",
|
|
723
|
-
configure_project: "Set a project's completion effect (none
|
|
797
|
+
configure_project: "Set a project's completion effect (none; merge to land each finished task's branch on the default branch; ghpr, for a project on GitHub, to merge each finished task's linked pull request), its display name, or its default branch, any of them at once. Tasks already filed keep theirs. The slug never changes.",
|
|
724
798
|
archive_project: "Archive a project whose work is finished or has moved: its members leave, its open threads close, and nothing more is posted, filed, or joined there, while its files and history stay. Refused while a task there is in play.",
|
|
725
799
|
create_channel: "Open a channel in a project, or with project null in the society, for a workstream such as a release, whose tasks are filed there. You follow it; a notice in the place's general announces it, and members subscribe when it concerns them. User, steward, concierge.",
|
|
726
|
-
archive_channel: "Archive a channel whose workstream is done: its open threads close, its followers stop following it, and nothing more is posted or filed there, while its history stays readable. Refused while a task filed there is in play, and for the channels the board itself uses. User, steward, concierge."
|
|
800
|
+
archive_channel: "Archive a channel whose workstream is done: its open threads close, its followers stop following it, and nothing more is posted or filed there, while its history stays readable. Refused while a task filed there is in play, and for the channels the board itself uses. User, steward, concierge.",
|
|
801
|
+
update_dashboard: "Replace the dashboard of a project you are a member of, the status page the user reads on its view, with a whole markdown body. Pass the revision from the frontmatter of the dashboard you read; if it was updated since, the call is refused with the current text to merge yours into. dashboard.md in the board projection is a copy, so editing it changes nothing."
|
|
727
802
|
};
|
|
728
803
|
|
|
729
804
|
// packages/shared/dist/events.js
|
|
@@ -763,6 +838,7 @@ var BOARD_EVENT_TYPES = [
|
|
|
763
838
|
"agent.left",
|
|
764
839
|
"knowledge.written",
|
|
765
840
|
"knowledge.removed",
|
|
841
|
+
"dashboard.updated",
|
|
766
842
|
"skill.promoted",
|
|
767
843
|
"runner.added",
|
|
768
844
|
"user.signed_in",
|
|
@@ -779,6 +855,8 @@ var BOARD_EVENT_TYPES = [
|
|
|
779
855
|
"turn.failed",
|
|
780
856
|
"merge.completed",
|
|
781
857
|
"merge.failed",
|
|
858
|
+
"pull_request.closed",
|
|
859
|
+
"pull_request.close_failed",
|
|
782
860
|
"workspace.leftovers"
|
|
783
861
|
];
|
|
784
862
|
var BoardEventTypeSchema = z5.enum(BOARD_EVENT_TYPES);
|
|
@@ -934,8 +1012,21 @@ var PendingEnrollmentSchema = z6.object({
|
|
|
934
1012
|
var ProjectRepoSchema = z6.object({
|
|
935
1013
|
slug: NameSchema,
|
|
936
1014
|
origin: z6.string().nullable(),
|
|
937
|
-
defaultBranch: z6.string().min(1)
|
|
1015
|
+
defaultBranch: z6.string().min(1),
|
|
1016
|
+
/**
|
|
1017
|
+
* The board lands finished tasks on the default branch, itself or through their pull requests,
|
|
1018
|
+
* so the runner lets nothing else move it.
|
|
1019
|
+
*/
|
|
1020
|
+
boardLands: z6.boolean()
|
|
938
1021
|
});
|
|
1022
|
+
function projectRepo(project) {
|
|
1023
|
+
return {
|
|
1024
|
+
slug: project.slug,
|
|
1025
|
+
origin: project.repo,
|
|
1026
|
+
defaultBranch: project.defaultBranch,
|
|
1027
|
+
boardLands: project.onDone !== "none"
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
939
1030
|
var TurnWorkspaceSchema = z6.discriminatedUnion("kind", [
|
|
940
1031
|
z6.object({
|
|
941
1032
|
kind: z6.literal("home"),
|
|
@@ -1008,7 +1099,22 @@ var TurnOutcomeSchema = z6.object({
|
|
|
1008
1099
|
var TurnAckSchema = z6.object({ dropWorkspace: z6.boolean() });
|
|
1009
1100
|
var LandRequestSchema = z6.object({
|
|
1010
1101
|
repo: ProjectRepoSchema,
|
|
1011
|
-
branch: z6.string().min(1)
|
|
1102
|
+
branch: z6.string().min(1),
|
|
1103
|
+
/**
|
|
1104
|
+
* For a `ghpr` task, the pull request to merge in place of the branch, which must hold the
|
|
1105
|
+
* branch's files, and the merge commit's subject and body, the pull request's title and an
|
|
1106
|
+
* empty body when left out.
|
|
1107
|
+
*/
|
|
1108
|
+
pullRequest: z6.object({
|
|
1109
|
+
url: z6.string().min(1),
|
|
1110
|
+
subject: z6.string().min(1).optional(),
|
|
1111
|
+
body: z6.string().optional()
|
|
1112
|
+
}).optional()
|
|
1113
|
+
});
|
|
1114
|
+
var CloseRequestSchema = z6.object({
|
|
1115
|
+
repo: ProjectRepoSchema,
|
|
1116
|
+
url: z6.string().min(1),
|
|
1117
|
+
comment: z6.string().min(1)
|
|
1012
1118
|
});
|
|
1013
1119
|
var MergeOutcomeSchema = z6.object({ ok: z6.boolean(), detail: z6.string() });
|
|
1014
1120
|
var ModelListSchema = z6.array(ModelOptionSchema);
|
|
@@ -1083,6 +1189,8 @@ var TurnSteerSchema = z6.object({ id: z6.uuid(), text: z6.string().min(1) });
|
|
|
1083
1189
|
var RunnerMessageSchema = z6.discriminatedUnion("type", [
|
|
1084
1190
|
z6.object({ type: z6.literal("turn"), job: TurnJobSchema }),
|
|
1085
1191
|
z6.object({ type: z6.literal("land"), request: z6.string().min(1), land: LandRequestSchema }),
|
|
1192
|
+
/** Answered with a `MergeOutcome`. */
|
|
1193
|
+
z6.object({ type: z6.literal("close"), request: z6.string().min(1), close: CloseRequestSchema }),
|
|
1086
1194
|
z6.object({ type: z6.literal("models"), request: z6.string().min(1), cli: CliKindSchema }),
|
|
1087
1195
|
/** Answered with a `BranchFile`, or null when the branch or the path does not exist. */
|
|
1088
1196
|
z6.object({ type: z6.literal("file"), request: z6.string().min(1), read: BranchReadSchema }),
|
|
@@ -1485,7 +1593,7 @@ var RunnerConfigSchema = z10.object({
|
|
|
1485
1593
|
});
|
|
1486
1594
|
|
|
1487
1595
|
// packages/shared/dist/version.js
|
|
1488
|
-
var STELLARIS_VERSION = "0.3.
|
|
1596
|
+
var STELLARIS_VERSION = "0.3.13";
|
|
1489
1597
|
|
|
1490
1598
|
// packages/board-core/dist/errors.js
|
|
1491
1599
|
var BoardError = class extends Error {
|
|
@@ -2669,6 +2777,9 @@ var THREAD_CONVERSATIONS = "thread-conversations";
|
|
|
2669
2777
|
var ASKS_CHANNEL_OPENED = "asks-channel-opened";
|
|
2670
2778
|
var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
|
|
2671
2779
|
var CHANNEL_CONVERSATIONS = "channel-conversations";
|
|
2780
|
+
var DASHBOARD_UPDATES = "dashboard-updates-granted";
|
|
2781
|
+
var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
|
|
2782
|
+
var DASHBOARD_LINE = "The project's members keep this page current with update_dashboard.";
|
|
2672
2783
|
var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
|
|
2673
2784
|
var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
|
|
2674
2785
|
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";
|
|
@@ -2705,6 +2816,11 @@ function memberToAgentInput(member) {
|
|
|
2705
2816
|
...member.seedInstructions === void 0 ? {} : { seedInstructions: member.seedInstructions }
|
|
2706
2817
|
};
|
|
2707
2818
|
}
|
|
2819
|
+
function assertLandsByPullRequest(effect, repo, where) {
|
|
2820
|
+
if (effect === "ghpr" && githubRepository(repo) === null) {
|
|
2821
|
+
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)`);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2708
2824
|
function stageFrom(planned, id) {
|
|
2709
2825
|
return {
|
|
2710
2826
|
id,
|
|
@@ -2862,7 +2978,8 @@ var Board = class _Board {
|
|
|
2862
2978
|
THREAD_CONVERSATIONS,
|
|
2863
2979
|
ASKS_CHANNEL_OPENED,
|
|
2864
2980
|
KNOWLEDGE_REMOVAL,
|
|
2865
|
-
CHANNEL_CONVERSATIONS
|
|
2981
|
+
CHANNEL_CONVERSATIONS,
|
|
2982
|
+
DASHBOARD_UPDATES
|
|
2866
2983
|
].filter((name) => !applied.includes(name));
|
|
2867
2984
|
if (pending.length === 0) {
|
|
2868
2985
|
return;
|
|
@@ -2892,21 +3009,38 @@ var Board = class _Board {
|
|
|
2892
3009
|
await this.openAsksChannel();
|
|
2893
3010
|
}
|
|
2894
3011
|
if (pending.includes(KNOWLEDGE_REMOVAL)) {
|
|
2895
|
-
await this.
|
|
3012
|
+
await this.grantBesideKnowledge("remove_knowledge");
|
|
2896
3013
|
}
|
|
2897
3014
|
if (pending.includes(CHANNEL_CONVERSATIONS)) {
|
|
2898
3015
|
await this.startChannelCursors();
|
|
2899
3016
|
}
|
|
3017
|
+
if (pending.includes(DASHBOARD_UPDATES)) {
|
|
3018
|
+
await this.grantBesideKnowledge("update_dashboard");
|
|
3019
|
+
await this.rewordDashboards();
|
|
3020
|
+
}
|
|
2900
3021
|
await writeJson(file, { applied: [...applied, ...pending] });
|
|
2901
3022
|
}
|
|
2902
3023
|
/**
|
|
2903
|
-
*
|
|
2904
|
-
*
|
|
3024
|
+
* Grants `verb` once to every charter granting `write_knowledge`, the work roles' charters in
|
|
3025
|
+
* practice; seed roles have it from their seed.
|
|
2905
3026
|
*/
|
|
2906
|
-
async
|
|
3027
|
+
async grantBesideKnowledge(verb) {
|
|
2907
3028
|
for (const charter of await this.listRoles()) {
|
|
2908
|
-
if (charter.verbs.includes("write_knowledge") && !charter.verbs.includes(
|
|
2909
|
-
await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs,
|
|
3029
|
+
if (charter.verbs.includes("write_knowledge") && !charter.verbs.includes(verb)) {
|
|
3030
|
+
await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
/** A dashboard still saying agents may edit its file says how it changes now. */
|
|
3035
|
+
async rewordDashboards() {
|
|
3036
|
+
for (const slug of await listDirs(this.paths.projects())) {
|
|
3037
|
+
const file = this.paths.dashboard(slug);
|
|
3038
|
+
if (!await exists(file)) {
|
|
3039
|
+
continue;
|
|
3040
|
+
}
|
|
3041
|
+
const { data, body } = await readLooseMarkdown(file);
|
|
3042
|
+
if (body.includes(OLD_DASHBOARD_LINE)) {
|
|
3043
|
+
await writeMarkdown(file, data, body.replace(OLD_DASHBOARD_LINE, DASHBOARD_LINE));
|
|
2910
3044
|
}
|
|
2911
3045
|
}
|
|
2912
3046
|
}
|
|
@@ -3668,6 +3802,7 @@ ${charter.purpose}
|
|
|
3668
3802
|
if (await exists(file)) {
|
|
3669
3803
|
throw new BoardError("ALREADY_EXISTS", `project ${input.slug} already exists`);
|
|
3670
3804
|
}
|
|
3805
|
+
assertLandsByPullRequest(input.onDone ?? "none", input.repo ?? null, `project ${input.slug}`);
|
|
3671
3806
|
const project = ProjectSchema.parse({
|
|
3672
3807
|
slug: input.slug,
|
|
3673
3808
|
name: input.name ?? input.slug,
|
|
@@ -3688,9 +3823,9 @@ ${charter.purpose}
|
|
|
3688
3823
|
await ensureDir(this.paths.tasks(project.slug));
|
|
3689
3824
|
await ensureDir(this.paths.threads(project.slug));
|
|
3690
3825
|
await ensureDir(this.paths.projectKnowledge(project.slug));
|
|
3691
|
-
await writeMarkdown(this.paths.dashboard(project.slug), { project: project.slug, updatedAt: this.now().toISOString() }, `# ${project.name} dashboard
|
|
3826
|
+
await writeMarkdown(this.paths.dashboard(project.slug), { project: project.slug, revision: 0, updatedAt: this.now().toISOString() }, `# ${project.name} dashboard
|
|
3692
3827
|
|
|
3693
|
-
|
|
3828
|
+
${DASHBOARD_LINE}
|
|
3694
3829
|
`);
|
|
3695
3830
|
await this.updateAgent(USER_NAME, (user) => ({
|
|
3696
3831
|
...user,
|
|
@@ -3795,6 +3930,7 @@ Agents may edit this file. It is rendered by the board UI.
|
|
|
3795
3930
|
...patch.os === void 0 ? {} : { os: patch.os },
|
|
3796
3931
|
...patch.clis === void 0 ? {} : { clis: [...patch.clis] },
|
|
3797
3932
|
...patch.capabilities === void 0 ? {} : { capabilities: [...patch.capabilities] },
|
|
3933
|
+
...patch.version === void 0 ? {} : { version: patch.version },
|
|
3798
3934
|
lastSeen: this.now().toISOString()
|
|
3799
3935
|
});
|
|
3800
3936
|
await writeMarkdown(file, next, doc.body);
|
|
@@ -4269,6 +4405,42 @@ ${task.body}`;
|
|
|
4269
4405
|
throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${project}`);
|
|
4270
4406
|
}
|
|
4271
4407
|
}
|
|
4408
|
+
/**
|
|
4409
|
+
* Replaces a project's dashboard, by a member or the user. A turn reads the dashboard from the
|
|
4410
|
+
* copy of the projection pulled when it started, so a revision other than the current one means
|
|
4411
|
+
* someone updated it since; the refusal carries the current text, which that copy lacks.
|
|
4412
|
+
*/
|
|
4413
|
+
async updateDashboard(actor, input) {
|
|
4414
|
+
const args = VerbInputs.update_dashboard.parse(input);
|
|
4415
|
+
await this.authorize(actor, "update_dashboard");
|
|
4416
|
+
return this.mutex.run(async () => {
|
|
4417
|
+
const project = await this.readActiveProject(args.project);
|
|
4418
|
+
if (actor.role !== USER_ROLE && !project.members.includes(actor.name)) {
|
|
4419
|
+
throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${args.project}`);
|
|
4420
|
+
}
|
|
4421
|
+
const current = await this.dashboardOf(args.project);
|
|
4422
|
+
if (args.revision !== current.revision) {
|
|
4423
|
+
const by = current.updatedBy === void 0 ? "" : `, updated by ${current.updatedBy} at ${current.updatedAt ?? "an unknown time"}`;
|
|
4424
|
+
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}.
|
|
4425
|
+
|
|
4426
|
+
${current.body}`);
|
|
4427
|
+
}
|
|
4428
|
+
const updated = DashboardSchema.parse({
|
|
4429
|
+
project: args.project,
|
|
4430
|
+
revision: current.revision + 1,
|
|
4431
|
+
updatedBy: actor.name,
|
|
4432
|
+
updatedAt: this.now().toISOString(),
|
|
4433
|
+
body: args.body
|
|
4434
|
+
});
|
|
4435
|
+
const { body, ...frontmatter } = updated;
|
|
4436
|
+
await writeMarkdown(this.paths.dashboard(args.project), frontmatter, body);
|
|
4437
|
+
await this.events.append("dashboard.updated", actor.name, {
|
|
4438
|
+
project: args.project,
|
|
4439
|
+
revision: updated.revision
|
|
4440
|
+
});
|
|
4441
|
+
return updated;
|
|
4442
|
+
});
|
|
4443
|
+
}
|
|
4272
4444
|
async openThread(actor, input) {
|
|
4273
4445
|
const args = VerbInputs.open_thread.parse(input);
|
|
4274
4446
|
await this.authorize(actor, "open_thread");
|
|
@@ -4650,6 +4822,9 @@ ${args.summary}`);
|
|
|
4650
4822
|
completedAt: ts
|
|
4651
4823
|
};
|
|
4652
4824
|
const next = current.stages[index + 1];
|
|
4825
|
+
if (next === void 0 && current.onDone === "ghpr" && current.pullRequest === void 0) {
|
|
4826
|
+
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`);
|
|
4827
|
+
}
|
|
4653
4828
|
if (next === void 0 && current.onDone === "none") {
|
|
4654
4829
|
this.assertNoTurnCut(actor, `task ${current.id} cannot end`, (turn) => turn.thread === current.id);
|
|
4655
4830
|
}
|
|
@@ -4766,6 +4941,7 @@ ${args.summary}`);
|
|
|
4766
4941
|
const guarded = gateChanges(ahead, replanned);
|
|
4767
4942
|
if (onDone !== current.onDone) {
|
|
4768
4943
|
guarded.push("change the completion effect");
|
|
4944
|
+
assertLandsByPullRequest(onDone, (await this.readProject(current.project)).repo, `project ${current.project}`);
|
|
4769
4945
|
}
|
|
4770
4946
|
if (guarded.length > 0) {
|
|
4771
4947
|
this.assertMayGate(actor, guarded.join(", "));
|
|
@@ -4854,6 +5030,12 @@ ${args.summary}`);
|
|
|
4854
5030
|
}
|
|
4855
5031
|
next = { ...next, blockedBy: [...args.blocked_by] };
|
|
4856
5032
|
}
|
|
5033
|
+
if (args.pull_request !== void 0) {
|
|
5034
|
+
next = {
|
|
5035
|
+
...next,
|
|
5036
|
+
pullRequest: await this.pullRequestLink(actor, current, args.pull_request)
|
|
5037
|
+
};
|
|
5038
|
+
}
|
|
4857
5039
|
if (args.note !== void 0) {
|
|
4858
5040
|
const step = moved !== null ? { action: "returned", stage: moved.from, to: moved.to } : next.status === "abandoned" ? { action: "abandoned", stage: current.stage, to: null } : void 0;
|
|
4859
5041
|
await this.postTaskNote(actor.name, current, args.note, step);
|
|
@@ -4879,12 +5061,46 @@ ${args.summary}`);
|
|
|
4879
5061
|
project: location.project,
|
|
4880
5062
|
from: current.status,
|
|
4881
5063
|
to: task.status,
|
|
4882
|
-
note: args.note ?? null
|
|
5064
|
+
note: args.note ?? null,
|
|
5065
|
+
...args.pull_request === void 0 ? {} : { pullRequest: task.pullRequest?.url ?? null }
|
|
4883
5066
|
});
|
|
4884
5067
|
}
|
|
4885
5068
|
return task;
|
|
4886
5069
|
});
|
|
4887
5070
|
}
|
|
5071
|
+
/**
|
|
5072
|
+
* The pull request a task is linked to, or none for `null`: one on the repository of its project,
|
|
5073
|
+
* by a member of the project or a planner, while the task is in play and before it lands.
|
|
5074
|
+
*/
|
|
5075
|
+
async pullRequestLink(actor, task, link) {
|
|
5076
|
+
this.assertInPlay(task);
|
|
5077
|
+
if (task.completing) {
|
|
5078
|
+
throw new BoardError("INVALID_STATE", `task ${task.id} is landing, so its pull request no longer changes`);
|
|
5079
|
+
}
|
|
5080
|
+
if (!PLANNING_ROLES.includes(actor.role)) {
|
|
5081
|
+
const agent = await this.readAgent(actor.name);
|
|
5082
|
+
if (!agent.memberships.includes(task.project)) {
|
|
5083
|
+
throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${task.project}`);
|
|
5084
|
+
}
|
|
5085
|
+
}
|
|
5086
|
+
if (link === null) {
|
|
5087
|
+
return void 0;
|
|
5088
|
+
}
|
|
5089
|
+
const project = await this.readProject(task.project);
|
|
5090
|
+
const repository = githubRepository(project.repo);
|
|
5091
|
+
const named = parsePullRequestUrl(link.url);
|
|
5092
|
+
if (repository === null || named === null || !sameRepository(repository, named)) {
|
|
5093
|
+
throw new BoardError("VALIDATION", `${link.url} is not a pull request of ${project.slug}'s repository${project.repo === null ? ", since it has no remote" : ` at ${project.repo}`}`);
|
|
5094
|
+
}
|
|
5095
|
+
return PullRequestLinkSchema.parse({
|
|
5096
|
+
url: link.url,
|
|
5097
|
+
number: named.number,
|
|
5098
|
+
...link.merge_subject === void 0 ? {} : { subject: link.merge_subject },
|
|
5099
|
+
...link.merge_body === void 0 ? {} : { body: link.merge_body },
|
|
5100
|
+
linkedBy: actor.name,
|
|
5101
|
+
linkedAt: this.now().toISOString()
|
|
5102
|
+
});
|
|
5103
|
+
}
|
|
4888
5104
|
async getTask(actor, input) {
|
|
4889
5105
|
const args = VerbInputs.get_task.parse(input);
|
|
4890
5106
|
await this.authorize(actor, "get_task");
|
|
@@ -4942,6 +5158,13 @@ ${args.summary}`);
|
|
|
4942
5158
|
proposedBy: actor.name,
|
|
4943
5159
|
status: "proposed",
|
|
4944
5160
|
createdAt: this.now().toISOString(),
|
|
5161
|
+
...actor.scope === void 0 ? {} : {
|
|
5162
|
+
filedFrom: {
|
|
5163
|
+
scope: actor.scope,
|
|
5164
|
+
...actor.thread === void 0 ? {} : { thread: actor.thread },
|
|
5165
|
+
...actor.channel === void 0 ? {} : { channel: actor.channel }
|
|
5166
|
+
}
|
|
5167
|
+
},
|
|
4945
5168
|
charter: parsed.data
|
|
4946
5169
|
}),
|
|
4947
5170
|
body: args.rationale
|
|
@@ -4995,6 +5218,9 @@ ${rationale}`}`);
|
|
|
4995
5218
|
}
|
|
4996
5219
|
return this.mutex.run(async () => {
|
|
4997
5220
|
const previous = await this.readActiveProject(args.project);
|
|
5221
|
+
if (args.on_done !== void 0) {
|
|
5222
|
+
assertLandsByPullRequest(args.on_done, previous.repo, `project ${previous.slug}`);
|
|
5223
|
+
}
|
|
4998
5224
|
const project = await this.updateProject(args.project, (current) => ({
|
|
4999
5225
|
...current,
|
|
5000
5226
|
...args.on_done === void 0 ? {} : { onDone: args.on_done },
|
|
@@ -5176,6 +5402,8 @@ ${rationale}`}`);
|
|
|
5176
5402
|
return this.createChannel(actor, VerbInputs.create_channel.parse(input));
|
|
5177
5403
|
case "archive_channel":
|
|
5178
5404
|
return this.archiveChannel(actor, VerbInputs.archive_channel.parse(input));
|
|
5405
|
+
case "update_dashboard":
|
|
5406
|
+
return this.updateDashboard(actor, VerbInputs.update_dashboard.parse(input));
|
|
5179
5407
|
default:
|
|
5180
5408
|
throw new BoardError("VALIDATION", `unknown verb ${String(verb)}`);
|
|
5181
5409
|
}
|
|
@@ -5437,6 +5665,29 @@ ${rationale}`}`);
|
|
|
5437
5665
|
});
|
|
5438
5666
|
});
|
|
5439
5667
|
}
|
|
5668
|
+
/**
|
|
5669
|
+
* Records how closing an abandoned `ghpr` task's pull request on GitHub went, once: the task's
|
|
5670
|
+
* link keeps it, so a restart does not try again.
|
|
5671
|
+
*/
|
|
5672
|
+
async recordPullRequestClosed(actor, input) {
|
|
5673
|
+
return this.mutex.run(async () => {
|
|
5674
|
+
const location = await this.findTask(input.taskId);
|
|
5675
|
+
const current = location.task;
|
|
5676
|
+
const pull = current.pullRequest;
|
|
5677
|
+
if (pull === void 0 || pull.closed !== void 0) {
|
|
5678
|
+
return current;
|
|
5679
|
+
}
|
|
5680
|
+
const task = await this.writeTask(location.project, {
|
|
5681
|
+
...current,
|
|
5682
|
+
pullRequest: {
|
|
5683
|
+
...pull,
|
|
5684
|
+
closed: { at: this.now().toISOString(), ok: input.ok, detail: input.detail }
|
|
5685
|
+
}
|
|
5686
|
+
});
|
|
5687
|
+
await this.events.append(input.ok ? "pull_request.closed" : "pull_request.close_failed", actor.name, { project: location.project, taskId: task.id, url: pull.url, detail: input.detail });
|
|
5688
|
+
return task;
|
|
5689
|
+
});
|
|
5690
|
+
}
|
|
5440
5691
|
/**
|
|
5441
5692
|
* Records that a citizen's workspace in a conversation that has ended holds work on no branch, so
|
|
5442
5693
|
* the scheduler gives the citizen a closing turn there to decide about it. False, recording
|
|
@@ -5480,8 +5731,8 @@ ${rationale}`}`);
|
|
|
5480
5731
|
}
|
|
5481
5732
|
/**
|
|
5482
5733
|
* Records the outcome of a completing task's effect: done, or waiting again at its last stage so
|
|
5483
|
-
* its participants can reshape the plan. A
|
|
5484
|
-
* landed one announced in the
|
|
5734
|
+
* its participants can reshape the plan. A landing's outcome, a merge's or a pull request's, is
|
|
5735
|
+
* posted to the task's thread, and a landed one announced in the task's channel.
|
|
5485
5736
|
*/
|
|
5486
5737
|
async finishCompletion(actor, input) {
|
|
5487
5738
|
return this.mutex.run(async () => {
|
|
@@ -5491,8 +5742,8 @@ ${rationale}`}`);
|
|
|
5491
5742
|
throw new BoardError("INVALID_STATE", `task ${current.id} is not completing`);
|
|
5492
5743
|
}
|
|
5493
5744
|
const ts = this.now().toISOString();
|
|
5494
|
-
const
|
|
5495
|
-
if (
|
|
5745
|
+
const landed = current.onDone !== "none";
|
|
5746
|
+
if (landed) {
|
|
5496
5747
|
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 });
|
|
5497
5748
|
}
|
|
5498
5749
|
if (input.ok) {
|
|
@@ -5512,7 +5763,7 @@ ${rationale}`}`);
|
|
|
5512
5763
|
for (const name of new Set(task2.stages.flatMap((stage) => stage.completedBy ?? []))) {
|
|
5513
5764
|
await this.refreshMember(name);
|
|
5514
5765
|
}
|
|
5515
|
-
if (
|
|
5766
|
+
if (landed) {
|
|
5516
5767
|
await this.appendMessage(actor.name, channelRef(location.project, task2.channel), `Task ${task2.id} "${task2.title}" is done: ${sentence(input.detail)}.`);
|
|
5517
5768
|
}
|
|
5518
5769
|
return task2;
|
|
@@ -5542,15 +5793,31 @@ ${rationale}`}`);
|
|
|
5542
5793
|
async writeState(name, value) {
|
|
5543
5794
|
await this.mutex.run(() => writeJson(path6.join(this.paths.state(), `${name}.json`), value));
|
|
5544
5795
|
}
|
|
5545
|
-
/** The project's
|
|
5796
|
+
/** The project's dashboard, empty at revision 0 when it has none. */
|
|
5546
5797
|
async readDashboard(slug) {
|
|
5547
5798
|
await this.readProject(slug);
|
|
5799
|
+
return this.dashboardOf(slug);
|
|
5800
|
+
}
|
|
5801
|
+
/**
|
|
5802
|
+
* Read loosely, since agents wrote dashboards by hand before they had a verb: a revision or an
|
|
5803
|
+
* author that does not parse counts as none.
|
|
5804
|
+
*/
|
|
5805
|
+
async dashboardOf(slug) {
|
|
5548
5806
|
const file = this.paths.dashboard(slug);
|
|
5549
5807
|
if (!await exists(file)) {
|
|
5550
|
-
return {
|
|
5808
|
+
return { project: slug, revision: 0, body: "" };
|
|
5551
5809
|
}
|
|
5552
5810
|
const { data, body } = await readLooseMarkdown(file);
|
|
5553
|
-
|
|
5811
|
+
const revision = z12.number().int().min(0).safeParse(data["revision"]);
|
|
5812
|
+
const updatedBy = NameSchema.safeParse(data["updatedBy"]);
|
|
5813
|
+
const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
|
|
5814
|
+
return {
|
|
5815
|
+
project: slug,
|
|
5816
|
+
revision: revision.success ? revision.data : 0,
|
|
5817
|
+
...updatedBy.success ? { updatedBy: updatedBy.data } : {},
|
|
5818
|
+
...updatedAt.success ? { updatedAt: updatedAt.data } : {},
|
|
5819
|
+
body
|
|
5820
|
+
};
|
|
5554
5821
|
}
|
|
5555
5822
|
async listRunners() {
|
|
5556
5823
|
const runners2 = [];
|
|
@@ -6653,7 +6920,8 @@ ${charter.purpose}
|
|
|
6653
6920
|
proposalId,
|
|
6654
6921
|
outcome,
|
|
6655
6922
|
kind: proposal.kind,
|
|
6656
|
-
proposedBy: proposal.proposedBy
|
|
6923
|
+
proposedBy: proposal.proposedBy,
|
|
6924
|
+
...proposal.filedFrom === void 0 ? {} : { filedFrom: proposal.filedFrom }
|
|
6657
6925
|
});
|
|
6658
6926
|
if (provision !== void 0) {
|
|
6659
6927
|
await this.events.append("proposal.provisioned", actor.name, {
|
|
@@ -6934,6 +7202,9 @@ function stringArray(value) {
|
|
|
6934
7202
|
function stringOf(value) {
|
|
6935
7203
|
return typeof value === "string" ? value : null;
|
|
6936
7204
|
}
|
|
7205
|
+
function awaitsClosing(task) {
|
|
7206
|
+
return task.status === "abandoned" && task.onDone === "ghpr" && task.pullRequest !== void 0 && task.pullRequest.closed === void 0;
|
|
7207
|
+
}
|
|
6937
7208
|
function inPlay(task) {
|
|
6938
7209
|
return (task.status === "open" || task.status === "claimed") && !task.completing;
|
|
6939
7210
|
}
|
|
@@ -6966,6 +7237,9 @@ var Scheduler = class {
|
|
|
6966
7237
|
steering = /* @__PURE__ */ new Map();
|
|
6967
7238
|
/** Tasks whose completion effect waits for the turn that finished their last stage to end. */
|
|
6968
7239
|
completions = /* @__PURE__ */ new Map();
|
|
7240
|
+
/** Abandoned `ghpr` tasks whose pull request waits to be closed, by task id, with its project. */
|
|
7241
|
+
closings = /* @__PURE__ */ new Map();
|
|
7242
|
+
/** Completions and closings under way. */
|
|
6969
7243
|
completing = /* @__PURE__ */ new Set();
|
|
6970
7244
|
/** Running turns that must run alone among their citizen's, by session key: see `heldBack`. */
|
|
6971
7245
|
exclusive = /* @__PURE__ */ new Map();
|
|
@@ -7065,6 +7339,7 @@ var Scheduler = class {
|
|
|
7065
7339
|
this.lastSweep = now;
|
|
7066
7340
|
}
|
|
7067
7341
|
this.runCompletions();
|
|
7342
|
+
this.runClosings();
|
|
7068
7343
|
if (!paused) {
|
|
7069
7344
|
await this.dispatchReady(now);
|
|
7070
7345
|
}
|
|
@@ -7092,6 +7367,9 @@ var Scheduler = class {
|
|
|
7092
7367
|
if (task.completing) {
|
|
7093
7368
|
this.completions.set(task.id, { project: project.slug, actor: USER_NAME });
|
|
7094
7369
|
}
|
|
7370
|
+
if (awaitsClosing(task)) {
|
|
7371
|
+
this.closings.set(task.id, project.slug);
|
|
7372
|
+
}
|
|
7095
7373
|
}
|
|
7096
7374
|
}
|
|
7097
7375
|
this.loaded = true;
|
|
@@ -7200,6 +7478,7 @@ var Scheduler = class {
|
|
|
7200
7478
|
const taskId = stringOf(payload["taskId"]);
|
|
7201
7479
|
if (taskId !== null && payload["to"] === "abandoned") {
|
|
7202
7480
|
delete this.state.releaseCounts[taskId];
|
|
7481
|
+
await this.queueClosing(taskId);
|
|
7203
7482
|
}
|
|
7204
7483
|
if (taskId !== null) {
|
|
7205
7484
|
await this.pruneStageWakes(taskId);
|
|
@@ -7539,13 +7818,16 @@ var Scheduler = class {
|
|
|
7539
7818
|
reason: "a conversation you took part in has ended, and its workspace holds work on no branch"
|
|
7540
7819
|
}, now, beside);
|
|
7541
7820
|
}
|
|
7542
|
-
/**
|
|
7821
|
+
/**
|
|
7822
|
+
* A decided proposal wakes the citizen that made it, to carry on with what it enables, in the
|
|
7823
|
+
* conversation it was filed from, where that work waits on the decision.
|
|
7824
|
+
*/
|
|
7543
7825
|
async wakeProposer(proposalId, proposer, event, now) {
|
|
7544
7826
|
const agent = await this.tryReadAgent(proposer);
|
|
7545
7827
|
if (agent === null) {
|
|
7546
7828
|
return;
|
|
7547
7829
|
}
|
|
7548
|
-
const scope = this.
|
|
7830
|
+
const { scope, beside } = await this.filedFrom(agent, event.payload["filedFrom"]);
|
|
7549
7831
|
const kind = stringOf(event.payload["kind"]) ?? "";
|
|
7550
7832
|
const outcome = stringOf(event.payload["outcome"]) ?? "decided";
|
|
7551
7833
|
this.enqueue(agent.name, scope, {
|
|
@@ -7553,7 +7835,27 @@ var Scheduler = class {
|
|
|
7553
7835
|
from: event.actor,
|
|
7554
7836
|
fromUser: event.actor === USER_NAME,
|
|
7555
7837
|
reason: `your ${kind} proposal ${proposalId} was ${outcome} by ${event.actor}`
|
|
7556
|
-
}, now);
|
|
7838
|
+
}, now, beside);
|
|
7839
|
+
}
|
|
7840
|
+
/**
|
|
7841
|
+
* The conversation a proposal was filed from while it is still open; its scope's home once it
|
|
7842
|
+
* has ended; and the home outside projects when the citizen has left that scope, or the proposal
|
|
7843
|
+
* names none, filed outside a turn or before proposals recorded it.
|
|
7844
|
+
*/
|
|
7845
|
+
async filedFrom(agent, recorded) {
|
|
7846
|
+
const origin = ProposalOriginSchema.safeParse(recorded);
|
|
7847
|
+
if (!origin.success || origin.data.scope !== SOCIETY_SCOPE && !agent.memberships.includes(origin.data.scope)) {
|
|
7848
|
+
return { scope: this.scopeForProject(agent, null) };
|
|
7849
|
+
}
|
|
7850
|
+
const scope = origin.data.scope;
|
|
7851
|
+
const conversation = workspaceConversationOf(origin.data);
|
|
7852
|
+
if (conversation === null || await this.board.conversationEnded(conversation)) {
|
|
7853
|
+
return { scope };
|
|
7854
|
+
}
|
|
7855
|
+
return {
|
|
7856
|
+
scope,
|
|
7857
|
+
beside: conversation.kind === "thread" ? { thread: await this.conversationOf(conversation.id, scope) } : { channel: conversation.name }
|
|
7858
|
+
};
|
|
7557
7859
|
}
|
|
7558
7860
|
/**
|
|
7559
7861
|
* A thread's conversation for a turn in `scope`, and whether it is a task's in that scope, whose
|
|
@@ -8203,6 +8505,40 @@ var Scheduler = class {
|
|
|
8203
8505
|
}
|
|
8204
8506
|
return others.some((other) => this.exclusive.get(other) === "reflection");
|
|
8205
8507
|
}
|
|
8508
|
+
/** Queues closing an abandoned task's pull request, when it is a `ghpr` task with one open. */
|
|
8509
|
+
async queueClosing(taskId) {
|
|
8510
|
+
if (this.runner.closePullRequest === void 0) {
|
|
8511
|
+
return;
|
|
8512
|
+
}
|
|
8513
|
+
try {
|
|
8514
|
+
const { task } = await this.board.findTask(taskId);
|
|
8515
|
+
if (awaitsClosing(task)) {
|
|
8516
|
+
this.closings.set(taskId, task.project);
|
|
8517
|
+
}
|
|
8518
|
+
} catch {
|
|
8519
|
+
}
|
|
8520
|
+
}
|
|
8521
|
+
/** Starts closing each queued pull request; one whose runner is away waits in the queue. */
|
|
8522
|
+
runClosings() {
|
|
8523
|
+
const close = this.runner.closePullRequest?.bind(this.runner);
|
|
8524
|
+
if (close === void 0) {
|
|
8525
|
+
return;
|
|
8526
|
+
}
|
|
8527
|
+
for (const [taskId, project] of this.closings) {
|
|
8528
|
+
this.closings.delete(taskId);
|
|
8529
|
+
const promise = close(project, taskId).then((outcome) => {
|
|
8530
|
+
if (outcome === "deferred" && !this.closings.has(taskId)) {
|
|
8531
|
+
this.closings.set(taskId, project);
|
|
8532
|
+
}
|
|
8533
|
+
return void 0;
|
|
8534
|
+
}).catch((error) => {
|
|
8535
|
+
this.log.error({ project, taskId, error: String(error) }, "closing a pull request failed");
|
|
8536
|
+
}).finally(() => {
|
|
8537
|
+
this.completing.delete(promise);
|
|
8538
|
+
});
|
|
8539
|
+
this.completing.add(promise);
|
|
8540
|
+
}
|
|
8541
|
+
}
|
|
8206
8542
|
/** Starts each queued completion once the turn that finished the task's last stage has ended. */
|
|
8207
8543
|
runCompletions() {
|
|
8208
8544
|
for (const [taskId, item] of this.completions) {
|
|
@@ -8305,7 +8641,7 @@ function renderInstructions(input) {
|
|
|
8305
8641
|
if (norms.length > 0) {
|
|
8306
8642
|
sections.push("", "## Society norms", "", norms);
|
|
8307
8643
|
}
|
|
8308
|
-
sections.push("", "## Core memory", "", memory.length === 0 ? "(empty)" : memory, "", "## Skills", "", ...renderSkillsIndex(input.skills ?? []), "", "## Where things are", "", `- Your home directory: ${input.homeDir}. memory/core.md is loaded every turn; memory/<topic>.md is your archive; skills/<name>/SKILL.md are your skills; projects/<slug>/notes.md are your notes. You may read and write all of it, and the board's search covers your archive and skills for you alone. The runner keeps it in git and shares it with the board after every turn, so whatever machine your next turn runs on has it; leave git there to the runner. Files over ${HOME_FILE_LIMIT_BYTES / (1024 * 1024)} MB are not kept.`, `- ${HOME_SCRATCH}/ in your home is your working folder in turns outside any project, and a thread's or a channel's conversation there works in a folder of its own inside it, which goes when the thread closes or the channel is archived: none of it is kept or shared, so put downloads, environments, and other working files there, and keep the rest of your home for what you mean to remember.`, `- The board projection: ${input.boardDir} (read-only markdown: society and project channels, tasks, threads; shared knowledge under projects/<slug>/knowledge/ and society/knowledge/; the society's skills under society/skills
|
|
8644
|
+
sections.push("", "## Core memory", "", memory.length === 0 ? "(empty)" : memory, "", "## Skills", "", ...renderSkillsIndex(input.skills ?? []), "", "## Where things are", "", `- Your home directory: ${input.homeDir}. memory/core.md is loaded every turn; memory/<topic>.md is your archive; skills/<name>/SKILL.md are your skills; projects/<slug>/notes.md are your notes. You may read and write all of it, and the board's search covers your archive and skills for you alone. The runner keeps it in git and shares it with the board after every turn, so whatever machine your next turn runs on has it; leave git there to the runner. Files over ${HOME_FILE_LIMIT_BYTES / (1024 * 1024)} MB are not kept.`, `- ${HOME_SCRATCH}/ in your home is your working folder in turns outside any project, and a thread's or a channel's conversation there works in a folder of its own inside it, which goes when the thread closes or the channel is archived: none of it is kept or shared, so put downloads, environments, and other working files there, and keep the rest of your home for what you mean to remember.`, `- The board projection: ${input.boardDir} (read-only markdown: society and project channels, tasks, threads; shared knowledge under projects/<slug>/knowledge/ and society/knowledge/; the society's skills under society/skills/; each project's dashboard, the status page the user reads on its view, at projects/<slug>/dashboard.md, which its members change with update_dashboard). Search it with your file tools; act through the board tools.`, "", "## Turn contract", "", TURN_CONTRACT, "", "## Planning", "", PLANNING, "", "## Governance", "", GOVERNANCE);
|
|
8309
8645
|
if (input.onboarding !== void 0) {
|
|
8310
8646
|
sections.push("", "## First turn", "", renderOnboardingPreamble(input.onboarding));
|
|
8311
8647
|
}
|
|
@@ -8352,7 +8688,7 @@ function planLine(task, whose) {
|
|
|
8352
8688
|
const index = stageIndex(task, task.stage);
|
|
8353
8689
|
const stage = task.stages[index];
|
|
8354
8690
|
const rest = task.stages.slice(index + 1).map((next) => `${next.name} (${next.gate ? "gate, " : ""}${assignee(next)})`);
|
|
8355
|
-
const then = rest.length === 0 ? `then ${task.onDone === "merge" ? "the board merges it" : "done"}` : `next: ${rest.join(", ")}`;
|
|
8691
|
+
const then = rest.length === 0 ? `then ${task.onDone === "merge" ? "the board merges it" : task.onDone === "ghpr" ? "the board merges its pull request" : "done"}` : `next: ${rest.join(", ")}`;
|
|
8356
8692
|
const gated = stage?.gate === true ? ", gated" : "";
|
|
8357
8693
|
return `- ${task.id} "${task.title}": ${stage?.name ?? task.stage} (${whose}, ${index + 1} of ${task.stages.length}${gated}); ${then}`;
|
|
8358
8694
|
}
|
|
@@ -8435,7 +8771,7 @@ function buildTurnPrompt(input) {
|
|
|
8435
8771
|
lines.push("A task you created is done. Tell whoever asked for it, if anyone did.");
|
|
8436
8772
|
}
|
|
8437
8773
|
if (dispatch.trigger.kind === "proposal_decided") {
|
|
8438
|
-
lines.push("A proposal you made was decided; the decision is the last post in
|
|
8774
|
+
lines.push("A proposal you made was decided; the decision is the last post in the proposal's thread. A decision wakes you in the conversation you filed the proposal from, or at home once that has ended. Carry on with what an approval enables or a rejection asks, and stay silent if nothing follows.");
|
|
8439
8775
|
}
|
|
8440
8776
|
if (dispatch.trigger.kind === "user_post") {
|
|
8441
8777
|
lines.push("The user posted. Route it: answer where it was posted, in its thread when it came in one, or create what it needs: a task with a plan whose stages name who does them, a thread, or a project, adding citizens to a project first when they are not members. Stay silent when the user already addressed a citizen and nothing else is needed.");
|
|
@@ -8450,7 +8786,8 @@ function buildTurnPrompt(input) {
|
|
|
8450
8786
|
} else {
|
|
8451
8787
|
const { thread, task } = conversation;
|
|
8452
8788
|
const about = thread.subject === void 0 ? "a topic" : `${thread.subject.kind} ${thread.subject.id}`;
|
|
8453
|
-
|
|
8789
|
+
const talk = thread.state === "closed" ? `This thread is closed, so posts to it are refused: if something still needs saying, post it in ${conversation.replyIn ?? thread.channel}.` : `Talk here with post_message and thread_id ${thread.id}.`;
|
|
8790
|
+
lines.push(`This turn is in the thread "${thread.title}" on ${thread.channel}, about ${about}. Only this conversation is below: read anything else you need with get_task, search, or the board's files, and leave other tasks and threads to their own turns. ${talk}`);
|
|
8454
8791
|
if (task !== null) {
|
|
8455
8792
|
lines.push("", `Task ${task.id} "${task.title}", ${task.status}:`, planLine(task, "now"));
|
|
8456
8793
|
if (task.body.trim() !== "") {
|
|
@@ -8514,8 +8851,13 @@ function buildTurnPrompt(input) {
|
|
|
8514
8851
|
}
|
|
8515
8852
|
}
|
|
8516
8853
|
const project = input.project;
|
|
8517
|
-
|
|
8518
|
-
|
|
8854
|
+
const landing = conversation?.task?.onDone ?? project?.onDone;
|
|
8855
|
+
if (project !== void 0 && project !== null && landing === "merge") {
|
|
8856
|
+
lines.push("", `## Project ${project.slug}`, "", `A finished task lands by the board merging its branch task/<id> into ${project.defaultBranch}. Never merge or fast-forward ${project.defaultBranch} yourself, or push other work to it on the remote; the runner refuses both.`);
|
|
8857
|
+
}
|
|
8858
|
+
if (project !== void 0 && project !== null && landing === "ghpr") {
|
|
8859
|
+
const base = project.defaultBranch;
|
|
8860
|
+
lines.push("", `## Project ${project.slug}`, "", `A finished task lands by the board merging its pull request on GitHub into ${base}. Open the pull request yourself, from a branch named for what it does that holds the same files as task/<id>, as \`git push origin task/<id>:<branch>\` gives, and link it to the task with update_task pull_request before its last stage completes, with the merge commit's merge_subject and merge_body, which you may change by linking again until it lands. Never merge the pull request or ${base} yourself, nor push other work to ${base}: the board merges once the task's stages are done, and the runner refuses the pushes. Abandoning the task closes its linked pull request and deletes its branch.`);
|
|
8519
8861
|
}
|
|
8520
8862
|
if (conflicts.length > 0) {
|
|
8521
8863
|
lines.push("", "## Edits to reconcile in your home", "", "Two of your turns changed these files at once on different runners. The file in place kept the other turn's version; yours is the copy named here, beside it. Merge what still holds into the file, then delete the copy.", "", ...conflicts.map((conflict) => `- ${conflict.path}, beside ${conflict.file}, since ${conflict.since}`));
|
|
@@ -8719,7 +9061,12 @@ var TurnHost = class {
|
|
|
8719
9061
|
}
|
|
8720
9062
|
}
|
|
8721
9063
|
const threadRecord = thread === void 0 ? void 0 : threads.get(thread.id) ?? await this.board.readThread(thread.id).catch(() => void 0);
|
|
8722
|
-
const conversation = threadRecord === void 0 ? null : {
|
|
9064
|
+
const conversation = threadRecord === void 0 ? null : {
|
|
9065
|
+
thread: threadRecord,
|
|
9066
|
+
task,
|
|
9067
|
+
fresh: threadSoFar !== null,
|
|
9068
|
+
...threadRecord.state === "closed" ? { replyIn: await this.replyIn(threadRecord) } : {}
|
|
9069
|
+
};
|
|
8723
9070
|
const ending = dispatch.trigger.kind === "closing" ? await this.endingOf(dispatch.project, threadRecord, task, channel) : null;
|
|
8724
9071
|
const prompt = buildTurnPrompt({
|
|
8725
9072
|
dispatch,
|
|
@@ -9122,11 +9469,7 @@ var TurnHost = class {
|
|
|
9122
9469
|
if (project === null) {
|
|
9123
9470
|
return { kind: "home", ...own };
|
|
9124
9471
|
}
|
|
9125
|
-
const repo =
|
|
9126
|
-
slug: project.slug,
|
|
9127
|
-
origin: project.repo,
|
|
9128
|
-
defaultBranch: project.defaultBranch
|
|
9129
|
-
};
|
|
9472
|
+
const repo = projectRepo(project);
|
|
9130
9473
|
if (thread?.task === true) {
|
|
9131
9474
|
return { kind: "task", repo, taskId: thread.id };
|
|
9132
9475
|
}
|
|
@@ -9141,6 +9484,10 @@ var TurnHost = class {
|
|
|
9141
9484
|
const conversation = workspaceConversationOf(job);
|
|
9142
9485
|
return conversation !== null && await this.board.conversationEnded(conversation);
|
|
9143
9486
|
}
|
|
9487
|
+
/** Where a closed thread's citizens say what still needs saying: its channel, else its place's general. */
|
|
9488
|
+
async replyIn(thread) {
|
|
9489
|
+
return await this.board.channelOpen(thread.channel) ? thread.channel : channelRef(parseChannelRef(thread.channel).project, "general");
|
|
9490
|
+
}
|
|
9144
9491
|
/**
|
|
9145
9492
|
* How a closing turn's conversation ended, and where its citizen asks someone else to take over
|
|
9146
9493
|
* work that is not its own: the thread's channel while it is open, else the place's general.
|
|
@@ -9149,11 +9496,7 @@ var TurnHost = class {
|
|
|
9149
9496
|
const place = scope === SOCIETY_SCOPE ? null : scope;
|
|
9150
9497
|
if (thread !== void 0) {
|
|
9151
9498
|
const closed = `the thread "${thread.title}" on ${thread.channel} was closed${thread.closedBy === void 0 ? "" : ` by ${thread.closedBy}`}${task === null ? "" : `, its task ${task.status}`}`;
|
|
9152
|
-
|
|
9153
|
-
return {
|
|
9154
|
-
how: closed,
|
|
9155
|
-
askIn: open ? thread.channel : channelRef(parseChannelRef(thread.channel).project, "general")
|
|
9156
|
-
};
|
|
9499
|
+
return { how: closed, askIn: await this.replyIn(thread) };
|
|
9157
9500
|
}
|
|
9158
9501
|
if (channel === void 0) {
|
|
9159
9502
|
return null;
|
|
@@ -9246,12 +9589,14 @@ var DEFAULT_REQUEST_TIMEOUT_MS = 10 * 6e4;
|
|
|
9246
9589
|
var CONTROL_TIMEOUT_MS = 15e3;
|
|
9247
9590
|
var FILE_TIMEOUT_MS = 6e4;
|
|
9248
9591
|
var SWEEP_TIMEOUT_MS = 2 * 6e4;
|
|
9592
|
+
var DEFAULT_ACK_TIMEOUT_MS = 3e4;
|
|
9249
9593
|
var RunnerHub = class {
|
|
9250
9594
|
board;
|
|
9251
9595
|
host;
|
|
9252
9596
|
version;
|
|
9253
9597
|
graceMs;
|
|
9254
9598
|
requestTimeoutMs;
|
|
9599
|
+
ackTimeoutMs;
|
|
9255
9600
|
now;
|
|
9256
9601
|
log;
|
|
9257
9602
|
seats = /* @__PURE__ */ new Map();
|
|
@@ -9267,6 +9612,7 @@ var RunnerHub = class {
|
|
|
9267
9612
|
this.version = options.version;
|
|
9268
9613
|
this.graceMs = options.graceMs ?? DEFAULT_GRACE_MS;
|
|
9269
9614
|
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
9615
|
+
this.ackTimeoutMs = options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS;
|
|
9270
9616
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
9271
9617
|
this.log = options.log ?? SILENT2;
|
|
9272
9618
|
this.startedAt = this.now().getTime();
|
|
@@ -9282,9 +9628,15 @@ var RunnerHub = class {
|
|
|
9282
9628
|
// -------------------------------------------------------------------------------------------
|
|
9283
9629
|
// The protocol, as the server's routes call it
|
|
9284
9630
|
// -------------------------------------------------------------------------------------------
|
|
9631
|
+
/** The jobs sent to a runner that it has not acknowledged yet. */
|
|
9632
|
+
unacknowledged(runner) {
|
|
9633
|
+
return [...this.seats.get(runner)?.unacked.keys() ?? []];
|
|
9634
|
+
}
|
|
9285
9635
|
/**
|
|
9286
|
-
* A runner registers, first or again.
|
|
9287
|
-
*
|
|
9636
|
+
* A runner registers, first or again. A turn in flight it still runs, or whose job it never
|
|
9637
|
+
* acknowledged, carries on, the latter sent again once its stream opens, since a job written
|
|
9638
|
+
* into a stream that had stopped delivering never arrived; one it took and no longer runs,
|
|
9639
|
+
* because it restarted, ends as failed.
|
|
9288
9640
|
*/
|
|
9289
9641
|
async register(name, hello) {
|
|
9290
9642
|
if (hello.protocol !== RUNNER_PROTOCOL) {
|
|
@@ -9299,25 +9651,31 @@ var RunnerHub = class {
|
|
|
9299
9651
|
inUse: 0,
|
|
9300
9652
|
warm: /* @__PURE__ */ new Set(),
|
|
9301
9653
|
disconnectedAt: null,
|
|
9302
|
-
grace: null
|
|
9654
|
+
grace: null,
|
|
9655
|
+
unacked: /* @__PURE__ */ new Map(),
|
|
9656
|
+
unconfirmed: false
|
|
9303
9657
|
});
|
|
9304
9658
|
} else {
|
|
9305
9659
|
seat.hello = hello;
|
|
9306
9660
|
seat.warm = /* @__PURE__ */ new Set();
|
|
9307
9661
|
}
|
|
9308
9662
|
const still = new Set(hello.turns);
|
|
9663
|
+
const unacked = this.seats.get(name)?.unacked ?? /* @__PURE__ */ new Map();
|
|
9309
9664
|
for (const turnId of this.host.turnsOn(name)) {
|
|
9310
|
-
if (
|
|
9665
|
+
if (still.has(turnId)) {
|
|
9666
|
+
this.settle(unacked, turnId);
|
|
9667
|
+
} else if (!unacked.has(turnId)) {
|
|
9311
9668
|
await this.host.abortTurn(turnId, `runner ${name} restarted during the turn`);
|
|
9312
9669
|
}
|
|
9313
9670
|
}
|
|
9314
9671
|
await this.board.markRunner(name, {
|
|
9315
9672
|
os: hello.os,
|
|
9316
9673
|
clis: hello.clis,
|
|
9317
|
-
capabilities: hello.capabilities
|
|
9674
|
+
capabilities: hello.capabilities,
|
|
9675
|
+
version: hello.version
|
|
9318
9676
|
});
|
|
9319
9677
|
this.runnerChanged(name);
|
|
9320
|
-
this.log.info({ runner: name, clis: hello.clis, slots: hello.slots }, "runner registered");
|
|
9678
|
+
this.log.info({ runner: name, version: hello.version, clis: hello.clis, slots: hello.slots }, "runner registered");
|
|
9321
9679
|
return { name, version: this.version };
|
|
9322
9680
|
}
|
|
9323
9681
|
/** A registered runner's stream opened. Returns what to call when it closes. */
|
|
@@ -9328,10 +9686,17 @@ var RunnerHub = class {
|
|
|
9328
9686
|
}
|
|
9329
9687
|
seat.send = send;
|
|
9330
9688
|
seat.disconnectedAt = null;
|
|
9689
|
+
seat.unconfirmed = false;
|
|
9331
9690
|
if (seat.grace !== null) {
|
|
9332
9691
|
clearTimeout(seat.grace);
|
|
9333
9692
|
seat.grace = null;
|
|
9334
9693
|
}
|
|
9694
|
+
for (const { job } of seat.unacked.values()) {
|
|
9695
|
+
if (send({ type: "turn", job })) {
|
|
9696
|
+
this.expectAck(seat, job);
|
|
9697
|
+
this.log.info({ runner: name, turnId: job.turnId }, "sent again a job the runner never took");
|
|
9698
|
+
}
|
|
9699
|
+
}
|
|
9335
9700
|
await this.board.markRunner(name, { status: "connected" });
|
|
9336
9701
|
this.runnerChanged(name);
|
|
9337
9702
|
return async () => {
|
|
@@ -9356,6 +9721,9 @@ var RunnerHub = class {
|
|
|
9356
9721
|
clearTimeout(seat.grace);
|
|
9357
9722
|
seat.grace = null;
|
|
9358
9723
|
}
|
|
9724
|
+
for (const { timer } of seat.unacked.values()) {
|
|
9725
|
+
clearTimeout(timer);
|
|
9726
|
+
}
|
|
9359
9727
|
}
|
|
9360
9728
|
await Promise.allSettled(this.detaching);
|
|
9361
9729
|
}
|
|
@@ -9363,6 +9731,18 @@ var RunnerHub = class {
|
|
|
9363
9731
|
this.assertOwns(runner, turnId);
|
|
9364
9732
|
await this.host.addEvents(turnId, entries);
|
|
9365
9733
|
}
|
|
9734
|
+
/** A runner's word that a turn's job arrived, which also shows its stream delivers again. */
|
|
9735
|
+
received(runner, turnId) {
|
|
9736
|
+
const seat = this.seats.get(runner);
|
|
9737
|
+
if (seat === void 0 || !seat.unacked.has(turnId)) {
|
|
9738
|
+
return;
|
|
9739
|
+
}
|
|
9740
|
+
this.settle(seat.unacked, turnId);
|
|
9741
|
+
if (seat.unconfirmed) {
|
|
9742
|
+
seat.unconfirmed = false;
|
|
9743
|
+
this.log.info({ runner }, "runner is taking jobs again");
|
|
9744
|
+
}
|
|
9745
|
+
}
|
|
9366
9746
|
async turnOutcome(runner, turnId, outcome) {
|
|
9367
9747
|
this.assertOwns(runner, turnId);
|
|
9368
9748
|
const ack = await this.host.closeTurn(turnId, outcome);
|
|
@@ -9506,7 +9886,7 @@ var RunnerHub = class {
|
|
|
9506
9886
|
return { refused: `${agent.name} has no CLI binding` };
|
|
9507
9887
|
}
|
|
9508
9888
|
const cli = agent.cli;
|
|
9509
|
-
const free = (seat) => seat.send !== null && seat.hello.clis.includes(cli) && (seat.hello.slots === null || seat.inUse < seat.hello.slots);
|
|
9889
|
+
const free = (seat) => seat.send !== null && !seat.unconfirmed && seat.hello.clis.includes(cli) && (seat.hello.slots === null || seat.inUse < seat.hello.slots);
|
|
9510
9890
|
let chosen;
|
|
9511
9891
|
if (dispatch.project === SOCIETY_SCOPE) {
|
|
9512
9892
|
chosen = await this.pinnedSeat(agent.name, agent.homeRunner, cli, free);
|
|
@@ -9545,6 +9925,7 @@ var RunnerHub = class {
|
|
|
9545
9925
|
return this.host.refuseTurn(dispatch, "none", assignment.refused);
|
|
9546
9926
|
}
|
|
9547
9927
|
const seat = this.seats.get(assignment.runner);
|
|
9928
|
+
let turnId = null;
|
|
9548
9929
|
try {
|
|
9549
9930
|
if (seat === void 0) {
|
|
9550
9931
|
return await this.host.refuseTurn(dispatch, assignment.runner, "the runner is gone");
|
|
@@ -9555,11 +9936,17 @@ var RunnerHub = class {
|
|
|
9555
9936
|
}
|
|
9556
9937
|
if (seat.send === null || !seat.send({ type: "turn", job: opened.job })) {
|
|
9557
9938
|
await this.host.abortTurn(opened.job.turnId, `runner ${seat.name} went away before the turn`);
|
|
9939
|
+
} else {
|
|
9940
|
+
this.expectAck(seat, opened.job);
|
|
9558
9941
|
}
|
|
9942
|
+
turnId = opened.job.turnId;
|
|
9559
9943
|
return await opened.ended;
|
|
9560
9944
|
} finally {
|
|
9561
9945
|
if (seat !== void 0) {
|
|
9562
9946
|
seat.inUse = Math.max(0, seat.inUse - 1);
|
|
9947
|
+
if (turnId !== null) {
|
|
9948
|
+
this.settle(seat.unacked, turnId);
|
|
9949
|
+
}
|
|
9563
9950
|
}
|
|
9564
9951
|
}
|
|
9565
9952
|
}
|
|
@@ -9568,7 +9955,7 @@ var RunnerHub = class {
|
|
|
9568
9955
|
if (!task.completing) {
|
|
9569
9956
|
return "done";
|
|
9570
9957
|
}
|
|
9571
|
-
if (task.onDone
|
|
9958
|
+
if (task.onDone === "none") {
|
|
9572
9959
|
await this.board.finishCompletion(SYSTEM_ACTOR, {
|
|
9573
9960
|
taskId,
|
|
9574
9961
|
ok: true,
|
|
@@ -9578,6 +9965,15 @@ var RunnerHub = class {
|
|
|
9578
9965
|
}
|
|
9579
9966
|
const record = await this.board.readProject(project);
|
|
9580
9967
|
const branch = `task/${taskId}`;
|
|
9968
|
+
const pull = task.onDone === "ghpr" ? task.pullRequest : void 0;
|
|
9969
|
+
if (task.onDone === "ghpr" && pull === void 0) {
|
|
9970
|
+
await this.board.finishCompletion(SYSTEM_ACTOR, {
|
|
9971
|
+
taskId,
|
|
9972
|
+
ok: false,
|
|
9973
|
+
detail: "no pull request is linked to the task"
|
|
9974
|
+
});
|
|
9975
|
+
return "done";
|
|
9976
|
+
}
|
|
9581
9977
|
let outcome = { ok: true, detail: "nothing to land, since the task left no branch" };
|
|
9582
9978
|
if (record.runner !== void 0) {
|
|
9583
9979
|
const seat = this.seats.get(record.runner);
|
|
@@ -9589,8 +9985,15 @@ var RunnerHub = class {
|
|
|
9589
9985
|
type: "land",
|
|
9590
9986
|
request,
|
|
9591
9987
|
land: {
|
|
9592
|
-
repo:
|
|
9593
|
-
branch
|
|
9988
|
+
repo: projectRepo(record),
|
|
9989
|
+
branch,
|
|
9990
|
+
...pull === void 0 ? {} : {
|
|
9991
|
+
pullRequest: {
|
|
9992
|
+
url: pull.url,
|
|
9993
|
+
...pull.subject === void 0 ? {} : { subject: pull.subject },
|
|
9994
|
+
...pull.body === void 0 ? {} : { body: pull.body }
|
|
9995
|
+
}
|
|
9996
|
+
}
|
|
9594
9997
|
}
|
|
9595
9998
|
})));
|
|
9596
9999
|
} catch (error) {
|
|
@@ -9612,6 +10015,49 @@ var RunnerHub = class {
|
|
|
9612
10015
|
});
|
|
9613
10016
|
return "done";
|
|
9614
10017
|
}
|
|
10018
|
+
/**
|
|
10019
|
+
* Closes the pull request of a `ghpr` task abandoned with one linked, on the runner its project
|
|
10020
|
+
* lives on, whose `gh` login reaches GitHub, and records how it went on the task; `deferred`
|
|
10021
|
+
* while that runner is away.
|
|
10022
|
+
*/
|
|
10023
|
+
async closePullRequest(project, taskId) {
|
|
10024
|
+
const { task } = await this.board.findTask(taskId);
|
|
10025
|
+
const pull = task.pullRequest;
|
|
10026
|
+
if (task.status !== "abandoned" || task.onDone !== "ghpr" || pull === void 0 || pull.closed !== void 0) {
|
|
10027
|
+
return "done";
|
|
10028
|
+
}
|
|
10029
|
+
const record = await this.board.readProject(project);
|
|
10030
|
+
let outcome = {
|
|
10031
|
+
ok: false,
|
|
10032
|
+
detail: `${project} lives on no runner yet, so none could close ${pull.url}`
|
|
10033
|
+
};
|
|
10034
|
+
if (record.runner !== void 0) {
|
|
10035
|
+
const seat = this.seats.get(record.runner);
|
|
10036
|
+
if (seat === void 0 || seat.send === null) {
|
|
10037
|
+
return "deferred";
|
|
10038
|
+
}
|
|
10039
|
+
try {
|
|
10040
|
+
outcome = MergeOutcomeSchema.parse(await this.request(seat, (request) => ({
|
|
10041
|
+
type: "close",
|
|
10042
|
+
request,
|
|
10043
|
+
close: {
|
|
10044
|
+
repo: projectRepo(record),
|
|
10045
|
+
url: pull.url,
|
|
10046
|
+
comment: `Closed by Stellaris: the task this pull request was opened for, "${task.title}", was abandoned.`
|
|
10047
|
+
}
|
|
10048
|
+
})));
|
|
10049
|
+
} catch (error) {
|
|
10050
|
+
this.log.warn({ project, taskId, error: String(error) }, "closing a pull request deferred");
|
|
10051
|
+
return "deferred";
|
|
10052
|
+
}
|
|
10053
|
+
}
|
|
10054
|
+
await this.board.recordPullRequestClosed(SYSTEM_ACTOR, {
|
|
10055
|
+
taskId,
|
|
10056
|
+
ok: outcome.ok,
|
|
10057
|
+
detail: outcome.detail
|
|
10058
|
+
});
|
|
10059
|
+
return "done";
|
|
10060
|
+
}
|
|
9615
10061
|
/**
|
|
9616
10062
|
* One path on a branch of a project's repository, read on the runner the project lives on.
|
|
9617
10063
|
* NOT_FOUND when the project has no runner yet or the branch or the path does not exist, and
|
|
@@ -9666,6 +10112,34 @@ var RunnerHub = class {
|
|
|
9666
10112
|
}
|
|
9667
10113
|
}
|
|
9668
10114
|
// -------------------------------------------------------------------------------------------
|
|
10115
|
+
/**
|
|
10116
|
+
* Waits for a runner to acknowledge a job. One that does not in time may be behind a stream that
|
|
10117
|
+
* looks open but delivers nothing, as when its machine's network stops passing traffic, so no
|
|
10118
|
+
* more turns go to it until it acknowledges or connects again; the turns it has carry on, since
|
|
10119
|
+
* they report by posts.
|
|
10120
|
+
*/
|
|
10121
|
+
expectAck(seat, job) {
|
|
10122
|
+
const previous = seat.unacked.get(job.turnId);
|
|
10123
|
+
if (previous !== void 0) {
|
|
10124
|
+
clearTimeout(previous.timer);
|
|
10125
|
+
}
|
|
10126
|
+
const timer = setTimeout(() => {
|
|
10127
|
+
if (!seat.unacked.has(job.turnId) || seat.unconfirmed || seat.send === null) {
|
|
10128
|
+
return;
|
|
10129
|
+
}
|
|
10130
|
+
seat.unconfirmed = true;
|
|
10131
|
+
this.log.warn({ runner: seat.name, turnId: job.turnId, waitedMs: this.ackTimeoutMs }, "runner has not acknowledged a job; no more turns go to it until it does or reconnects");
|
|
10132
|
+
}, this.ackTimeoutMs);
|
|
10133
|
+
timer.unref?.();
|
|
10134
|
+
seat.unacked.set(job.turnId, { job, timer });
|
|
10135
|
+
}
|
|
10136
|
+
settle(unacked, turnId) {
|
|
10137
|
+
const entry = unacked.get(turnId);
|
|
10138
|
+
if (entry !== void 0) {
|
|
10139
|
+
clearTimeout(entry.timer);
|
|
10140
|
+
unacked.delete(turnId);
|
|
10141
|
+
}
|
|
10142
|
+
}
|
|
9669
10143
|
async detach(seat) {
|
|
9670
10144
|
seat.send = null;
|
|
9671
10145
|
this.runnerChanged(seat.name);
|
|
@@ -9686,7 +10160,7 @@ var RunnerHub = class {
|
|
|
9686
10160
|
return;
|
|
9687
10161
|
}
|
|
9688
10162
|
for (const turnId of this.host.turnsOn(seat.name)) {
|
|
9689
|
-
void this.host.abortTurn(turnId, `runner ${seat.name} went away during the turn`);
|
|
10163
|
+
void this.host.abortTurn(turnId, seat.unacked.has(turnId) ? `the turn never reached runner ${seat.name}, which went away` : `runner ${seat.name} went away during the turn`);
|
|
9690
10164
|
}
|
|
9691
10165
|
}, this.graceMs);
|
|
9692
10166
|
seat.grace.unref?.();
|
|
@@ -9763,7 +10237,7 @@ var RunnerHub = class {
|
|
|
9763
10237
|
}
|
|
9764
10238
|
return {
|
|
9765
10239
|
seat,
|
|
9766
|
-
repo:
|
|
10240
|
+
repo: projectRepo(record)
|
|
9767
10241
|
};
|
|
9768
10242
|
}
|
|
9769
10243
|
assertOwns(runner, turnId) {
|
|
@@ -10228,6 +10702,10 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
10228
10702
|
await hub.turnEvents(c.get("runner"), UlidSchema.parse(c.req.param("id")), entries);
|
|
10229
10703
|
return c.json({ ok: true });
|
|
10230
10704
|
});
|
|
10705
|
+
routes.post("/turns/:id/received", (c) => {
|
|
10706
|
+
hub.received(c.get("runner"), UlidSchema.parse(c.req.param("id")));
|
|
10707
|
+
return c.json({ ok: true });
|
|
10708
|
+
});
|
|
10231
10709
|
routes.post("/turns/:id/outcome", async (c) => c.json(await hub.turnOutcome(c.get("runner"), UlidSchema.parse(c.req.param("id")), TurnOutcomeSchema.parse(await c.req.json()))));
|
|
10232
10710
|
routes.post("/requests/:id", async (c) => {
|
|
10233
10711
|
hub.answer(c.get("runner"), c.req.param("id"), RunnerAnswerSchema.parse(await c.req.json()));
|
|
@@ -10402,6 +10880,7 @@ function createApp(deps) {
|
|
|
10402
10880
|
});
|
|
10403
10881
|
api.delete("/sign-in", async (c) => c.json({ signedOut: await board2.signOut(bearer2(c) ?? "") }));
|
|
10404
10882
|
api.get("/society", async (c) => c.json(await board2.society()));
|
|
10883
|
+
api.get("/version", (c) => c.json({ version }));
|
|
10405
10884
|
api.get("/agents", async (c) => c.json((await board2.listAgents()).map(({ tokenHash: _hash, ...agent }) => agent)));
|
|
10406
10885
|
api.get("/members", async (c) => c.json(await board2.listMembers()));
|
|
10407
10886
|
api.get("/agents/:name/turns", async (c) => c.json(await board2.listTurns(c.req.param("name"), Number(c.req.query("limit") ?? "50"))));
|