@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/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,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
|
var VERB_DESCRIPTIONS = {
|
|
701
763
|
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 +768,25 @@ var VERB_DESCRIPTIONS = {
|
|
|
706
768
|
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
769
|
claim_task: "Hold the task's current stage. Claims are leases renewed by every turn that touches the task.",
|
|
708
770
|
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,
|
|
771
|
+
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
772
|
get_task: "Read a task with its plan, its brief, and the messages of its thread.",
|
|
711
773
|
subscribe: "Subscribe to a channel. Subscriptions feed your digest; they never wake you.",
|
|
712
774
|
unsubscribe: "Unsubscribe from a channel.",
|
|
713
775
|
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
776
|
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
777
|
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
|
|
778
|
+
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
779
|
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
780
|
leave_project: "Leave a project, or remove another citizen from one when your role allows it.",
|
|
719
781
|
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
782
|
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
783
|
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
784
|
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
|
|
785
|
+
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
786
|
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
787
|
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."
|
|
788
|
+
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.",
|
|
789
|
+
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
790
|
};
|
|
728
791
|
|
|
729
792
|
// packages/shared/dist/events.js
|
|
@@ -763,6 +826,7 @@ var BOARD_EVENT_TYPES = [
|
|
|
763
826
|
"agent.left",
|
|
764
827
|
"knowledge.written",
|
|
765
828
|
"knowledge.removed",
|
|
829
|
+
"dashboard.updated",
|
|
766
830
|
"skill.promoted",
|
|
767
831
|
"runner.added",
|
|
768
832
|
"user.signed_in",
|
|
@@ -934,8 +998,21 @@ var PendingEnrollmentSchema = z6.object({
|
|
|
934
998
|
var ProjectRepoSchema = z6.object({
|
|
935
999
|
slug: NameSchema,
|
|
936
1000
|
origin: z6.string().nullable(),
|
|
937
|
-
defaultBranch: z6.string().min(1)
|
|
1001
|
+
defaultBranch: z6.string().min(1),
|
|
1002
|
+
/**
|
|
1003
|
+
* The board lands finished tasks on the default branch, itself or through their pull requests,
|
|
1004
|
+
* so the runner lets nothing else move it.
|
|
1005
|
+
*/
|
|
1006
|
+
boardLands: z6.boolean()
|
|
938
1007
|
});
|
|
1008
|
+
function projectRepo(project) {
|
|
1009
|
+
return {
|
|
1010
|
+
slug: project.slug,
|
|
1011
|
+
origin: project.repo,
|
|
1012
|
+
defaultBranch: project.defaultBranch,
|
|
1013
|
+
boardLands: project.onDone !== "none"
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
939
1016
|
var TurnWorkspaceSchema = z6.discriminatedUnion("kind", [
|
|
940
1017
|
z6.object({
|
|
941
1018
|
kind: z6.literal("home"),
|
|
@@ -1008,7 +1085,17 @@ var TurnOutcomeSchema = z6.object({
|
|
|
1008
1085
|
var TurnAckSchema = z6.object({ dropWorkspace: z6.boolean() });
|
|
1009
1086
|
var LandRequestSchema = z6.object({
|
|
1010
1087
|
repo: ProjectRepoSchema,
|
|
1011
|
-
branch: z6.string().min(1)
|
|
1088
|
+
branch: z6.string().min(1),
|
|
1089
|
+
/**
|
|
1090
|
+
* For a `ghpr` task, the pull request to merge in place of the branch, which must hold the
|
|
1091
|
+
* branch's files, and the merge commit's subject and body, the pull request's title and an
|
|
1092
|
+
* empty body when left out.
|
|
1093
|
+
*/
|
|
1094
|
+
pullRequest: z6.object({
|
|
1095
|
+
url: z6.string().min(1),
|
|
1096
|
+
subject: z6.string().min(1).optional(),
|
|
1097
|
+
body: z6.string().optional()
|
|
1098
|
+
}).optional()
|
|
1012
1099
|
});
|
|
1013
1100
|
var MergeOutcomeSchema = z6.object({ ok: z6.boolean(), detail: z6.string() });
|
|
1014
1101
|
var ModelListSchema = z6.array(ModelOptionSchema);
|
|
@@ -1485,7 +1572,7 @@ var RunnerConfigSchema = z10.object({
|
|
|
1485
1572
|
});
|
|
1486
1573
|
|
|
1487
1574
|
// packages/shared/dist/version.js
|
|
1488
|
-
var STELLARIS_VERSION = "0.3.
|
|
1575
|
+
var STELLARIS_VERSION = "0.3.12";
|
|
1489
1576
|
|
|
1490
1577
|
// packages/board-core/dist/errors.js
|
|
1491
1578
|
var BoardError = class extends Error {
|
|
@@ -2669,6 +2756,9 @@ var THREAD_CONVERSATIONS = "thread-conversations";
|
|
|
2669
2756
|
var ASKS_CHANNEL_OPENED = "asks-channel-opened";
|
|
2670
2757
|
var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
|
|
2671
2758
|
var CHANNEL_CONVERSATIONS = "channel-conversations";
|
|
2759
|
+
var DASHBOARD_UPDATES = "dashboard-updates-granted";
|
|
2760
|
+
var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
|
|
2761
|
+
var DASHBOARD_LINE = "The project's members keep this page current with update_dashboard.";
|
|
2672
2762
|
var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
|
|
2673
2763
|
var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
|
|
2674
2764
|
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 +2795,11 @@ function memberToAgentInput(member) {
|
|
|
2705
2795
|
...member.seedInstructions === void 0 ? {} : { seedInstructions: member.seedInstructions }
|
|
2706
2796
|
};
|
|
2707
2797
|
}
|
|
2798
|
+
function assertLandsByPullRequest(effect, repo, where) {
|
|
2799
|
+
if (effect === "ghpr" && githubRepository(repo) === null) {
|
|
2800
|
+
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)`);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2708
2803
|
function stageFrom(planned, id) {
|
|
2709
2804
|
return {
|
|
2710
2805
|
id,
|
|
@@ -2862,7 +2957,8 @@ var Board = class _Board {
|
|
|
2862
2957
|
THREAD_CONVERSATIONS,
|
|
2863
2958
|
ASKS_CHANNEL_OPENED,
|
|
2864
2959
|
KNOWLEDGE_REMOVAL,
|
|
2865
|
-
CHANNEL_CONVERSATIONS
|
|
2960
|
+
CHANNEL_CONVERSATIONS,
|
|
2961
|
+
DASHBOARD_UPDATES
|
|
2866
2962
|
].filter((name) => !applied.includes(name));
|
|
2867
2963
|
if (pending.length === 0) {
|
|
2868
2964
|
return;
|
|
@@ -2892,21 +2988,38 @@ var Board = class _Board {
|
|
|
2892
2988
|
await this.openAsksChannel();
|
|
2893
2989
|
}
|
|
2894
2990
|
if (pending.includes(KNOWLEDGE_REMOVAL)) {
|
|
2895
|
-
await this.
|
|
2991
|
+
await this.grantBesideKnowledge("remove_knowledge");
|
|
2896
2992
|
}
|
|
2897
2993
|
if (pending.includes(CHANNEL_CONVERSATIONS)) {
|
|
2898
2994
|
await this.startChannelCursors();
|
|
2899
2995
|
}
|
|
2996
|
+
if (pending.includes(DASHBOARD_UPDATES)) {
|
|
2997
|
+
await this.grantBesideKnowledge("update_dashboard");
|
|
2998
|
+
await this.rewordDashboards();
|
|
2999
|
+
}
|
|
2900
3000
|
await writeJson(file, { applied: [...applied, ...pending] });
|
|
2901
3001
|
}
|
|
2902
3002
|
/**
|
|
2903
|
-
*
|
|
2904
|
-
*
|
|
3003
|
+
* Grants `verb` once to every charter granting `write_knowledge`, the work roles' charters in
|
|
3004
|
+
* practice; seed roles have it from their seed.
|
|
2905
3005
|
*/
|
|
2906
|
-
async
|
|
3006
|
+
async grantBesideKnowledge(verb) {
|
|
2907
3007
|
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,
|
|
3008
|
+
if (charter.verbs.includes("write_knowledge") && !charter.verbs.includes(verb)) {
|
|
3009
|
+
await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
/** A dashboard still saying agents may edit its file says how it changes now. */
|
|
3014
|
+
async rewordDashboards() {
|
|
3015
|
+
for (const slug of await listDirs(this.paths.projects())) {
|
|
3016
|
+
const file = this.paths.dashboard(slug);
|
|
3017
|
+
if (!await exists(file)) {
|
|
3018
|
+
continue;
|
|
3019
|
+
}
|
|
3020
|
+
const { data, body } = await readLooseMarkdown(file);
|
|
3021
|
+
if (body.includes(OLD_DASHBOARD_LINE)) {
|
|
3022
|
+
await writeMarkdown(file, data, body.replace(OLD_DASHBOARD_LINE, DASHBOARD_LINE));
|
|
2910
3023
|
}
|
|
2911
3024
|
}
|
|
2912
3025
|
}
|
|
@@ -3668,6 +3781,7 @@ ${charter.purpose}
|
|
|
3668
3781
|
if (await exists(file)) {
|
|
3669
3782
|
throw new BoardError("ALREADY_EXISTS", `project ${input.slug} already exists`);
|
|
3670
3783
|
}
|
|
3784
|
+
assertLandsByPullRequest(input.onDone ?? "none", input.repo ?? null, `project ${input.slug}`);
|
|
3671
3785
|
const project = ProjectSchema.parse({
|
|
3672
3786
|
slug: input.slug,
|
|
3673
3787
|
name: input.name ?? input.slug,
|
|
@@ -3688,9 +3802,9 @@ ${charter.purpose}
|
|
|
3688
3802
|
await ensureDir(this.paths.tasks(project.slug));
|
|
3689
3803
|
await ensureDir(this.paths.threads(project.slug));
|
|
3690
3804
|
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
|
|
3805
|
+
await writeMarkdown(this.paths.dashboard(project.slug), { project: project.slug, revision: 0, updatedAt: this.now().toISOString() }, `# ${project.name} dashboard
|
|
3692
3806
|
|
|
3693
|
-
|
|
3807
|
+
${DASHBOARD_LINE}
|
|
3694
3808
|
`);
|
|
3695
3809
|
await this.updateAgent(USER_NAME, (user) => ({
|
|
3696
3810
|
...user,
|
|
@@ -3795,6 +3909,7 @@ Agents may edit this file. It is rendered by the board UI.
|
|
|
3795
3909
|
...patch.os === void 0 ? {} : { os: patch.os },
|
|
3796
3910
|
...patch.clis === void 0 ? {} : { clis: [...patch.clis] },
|
|
3797
3911
|
...patch.capabilities === void 0 ? {} : { capabilities: [...patch.capabilities] },
|
|
3912
|
+
...patch.version === void 0 ? {} : { version: patch.version },
|
|
3798
3913
|
lastSeen: this.now().toISOString()
|
|
3799
3914
|
});
|
|
3800
3915
|
await writeMarkdown(file, next, doc.body);
|
|
@@ -4269,6 +4384,42 @@ ${task.body}`;
|
|
|
4269
4384
|
throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${project}`);
|
|
4270
4385
|
}
|
|
4271
4386
|
}
|
|
4387
|
+
/**
|
|
4388
|
+
* Replaces a project's dashboard, by a member or the user. A turn reads the dashboard from the
|
|
4389
|
+
* copy of the projection pulled when it started, so a revision other than the current one means
|
|
4390
|
+
* someone updated it since; the refusal carries the current text, which that copy lacks.
|
|
4391
|
+
*/
|
|
4392
|
+
async updateDashboard(actor, input) {
|
|
4393
|
+
const args = VerbInputs.update_dashboard.parse(input);
|
|
4394
|
+
await this.authorize(actor, "update_dashboard");
|
|
4395
|
+
return this.mutex.run(async () => {
|
|
4396
|
+
const project = await this.readActiveProject(args.project);
|
|
4397
|
+
if (actor.role !== USER_ROLE && !project.members.includes(actor.name)) {
|
|
4398
|
+
throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${args.project}`);
|
|
4399
|
+
}
|
|
4400
|
+
const current = await this.dashboardOf(args.project);
|
|
4401
|
+
if (args.revision !== current.revision) {
|
|
4402
|
+
const by = current.updatedBy === void 0 ? "" : `, updated by ${current.updatedBy} at ${current.updatedAt ?? "an unknown time"}`;
|
|
4403
|
+
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}.
|
|
4404
|
+
|
|
4405
|
+
${current.body}`);
|
|
4406
|
+
}
|
|
4407
|
+
const updated = DashboardSchema.parse({
|
|
4408
|
+
project: args.project,
|
|
4409
|
+
revision: current.revision + 1,
|
|
4410
|
+
updatedBy: actor.name,
|
|
4411
|
+
updatedAt: this.now().toISOString(),
|
|
4412
|
+
body: args.body
|
|
4413
|
+
});
|
|
4414
|
+
const { body, ...frontmatter } = updated;
|
|
4415
|
+
await writeMarkdown(this.paths.dashboard(args.project), frontmatter, body);
|
|
4416
|
+
await this.events.append("dashboard.updated", actor.name, {
|
|
4417
|
+
project: args.project,
|
|
4418
|
+
revision: updated.revision
|
|
4419
|
+
});
|
|
4420
|
+
return updated;
|
|
4421
|
+
});
|
|
4422
|
+
}
|
|
4272
4423
|
async openThread(actor, input) {
|
|
4273
4424
|
const args = VerbInputs.open_thread.parse(input);
|
|
4274
4425
|
await this.authorize(actor, "open_thread");
|
|
@@ -4650,6 +4801,9 @@ ${args.summary}`);
|
|
|
4650
4801
|
completedAt: ts
|
|
4651
4802
|
};
|
|
4652
4803
|
const next = current.stages[index + 1];
|
|
4804
|
+
if (next === void 0 && current.onDone === "ghpr" && current.pullRequest === void 0) {
|
|
4805
|
+
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`);
|
|
4806
|
+
}
|
|
4653
4807
|
if (next === void 0 && current.onDone === "none") {
|
|
4654
4808
|
this.assertNoTurnCut(actor, `task ${current.id} cannot end`, (turn) => turn.thread === current.id);
|
|
4655
4809
|
}
|
|
@@ -4766,6 +4920,7 @@ ${args.summary}`);
|
|
|
4766
4920
|
const guarded = gateChanges(ahead, replanned);
|
|
4767
4921
|
if (onDone !== current.onDone) {
|
|
4768
4922
|
guarded.push("change the completion effect");
|
|
4923
|
+
assertLandsByPullRequest(onDone, (await this.readProject(current.project)).repo, `project ${current.project}`);
|
|
4769
4924
|
}
|
|
4770
4925
|
if (guarded.length > 0) {
|
|
4771
4926
|
this.assertMayGate(actor, guarded.join(", "));
|
|
@@ -4854,6 +5009,12 @@ ${args.summary}`);
|
|
|
4854
5009
|
}
|
|
4855
5010
|
next = { ...next, blockedBy: [...args.blocked_by] };
|
|
4856
5011
|
}
|
|
5012
|
+
if (args.pull_request !== void 0) {
|
|
5013
|
+
next = {
|
|
5014
|
+
...next,
|
|
5015
|
+
pullRequest: await this.pullRequestLink(actor, current, args.pull_request)
|
|
5016
|
+
};
|
|
5017
|
+
}
|
|
4857
5018
|
if (args.note !== void 0) {
|
|
4858
5019
|
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
5020
|
await this.postTaskNote(actor.name, current, args.note, step);
|
|
@@ -4879,12 +5040,46 @@ ${args.summary}`);
|
|
|
4879
5040
|
project: location.project,
|
|
4880
5041
|
from: current.status,
|
|
4881
5042
|
to: task.status,
|
|
4882
|
-
note: args.note ?? null
|
|
5043
|
+
note: args.note ?? null,
|
|
5044
|
+
...args.pull_request === void 0 ? {} : { pullRequest: task.pullRequest?.url ?? null }
|
|
4883
5045
|
});
|
|
4884
5046
|
}
|
|
4885
5047
|
return task;
|
|
4886
5048
|
});
|
|
4887
5049
|
}
|
|
5050
|
+
/**
|
|
5051
|
+
* The pull request a task is linked to, or none for `null`: one on the repository of its project,
|
|
5052
|
+
* by a member of the project or a planner, while the task is in play and before it lands.
|
|
5053
|
+
*/
|
|
5054
|
+
async pullRequestLink(actor, task, link) {
|
|
5055
|
+
this.assertInPlay(task);
|
|
5056
|
+
if (task.completing) {
|
|
5057
|
+
throw new BoardError("INVALID_STATE", `task ${task.id} is landing, so its pull request no longer changes`);
|
|
5058
|
+
}
|
|
5059
|
+
if (!PLANNING_ROLES.includes(actor.role)) {
|
|
5060
|
+
const agent = await this.readAgent(actor.name);
|
|
5061
|
+
if (!agent.memberships.includes(task.project)) {
|
|
5062
|
+
throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${task.project}`);
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
if (link === null) {
|
|
5066
|
+
return void 0;
|
|
5067
|
+
}
|
|
5068
|
+
const project = await this.readProject(task.project);
|
|
5069
|
+
const repository = githubRepository(project.repo);
|
|
5070
|
+
const named = parsePullRequestUrl(link.url);
|
|
5071
|
+
if (repository === null || named === null || !sameRepository(repository, named)) {
|
|
5072
|
+
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}`}`);
|
|
5073
|
+
}
|
|
5074
|
+
return PullRequestLinkSchema.parse({
|
|
5075
|
+
url: link.url,
|
|
5076
|
+
number: named.number,
|
|
5077
|
+
...link.merge_subject === void 0 ? {} : { subject: link.merge_subject },
|
|
5078
|
+
...link.merge_body === void 0 ? {} : { body: link.merge_body },
|
|
5079
|
+
linkedBy: actor.name,
|
|
5080
|
+
linkedAt: this.now().toISOString()
|
|
5081
|
+
});
|
|
5082
|
+
}
|
|
4888
5083
|
async getTask(actor, input) {
|
|
4889
5084
|
const args = VerbInputs.get_task.parse(input);
|
|
4890
5085
|
await this.authorize(actor, "get_task");
|
|
@@ -4995,6 +5190,9 @@ ${rationale}`}`);
|
|
|
4995
5190
|
}
|
|
4996
5191
|
return this.mutex.run(async () => {
|
|
4997
5192
|
const previous = await this.readActiveProject(args.project);
|
|
5193
|
+
if (args.on_done !== void 0) {
|
|
5194
|
+
assertLandsByPullRequest(args.on_done, previous.repo, `project ${previous.slug}`);
|
|
5195
|
+
}
|
|
4998
5196
|
const project = await this.updateProject(args.project, (current) => ({
|
|
4999
5197
|
...current,
|
|
5000
5198
|
...args.on_done === void 0 ? {} : { onDone: args.on_done },
|
|
@@ -5176,6 +5374,8 @@ ${rationale}`}`);
|
|
|
5176
5374
|
return this.createChannel(actor, VerbInputs.create_channel.parse(input));
|
|
5177
5375
|
case "archive_channel":
|
|
5178
5376
|
return this.archiveChannel(actor, VerbInputs.archive_channel.parse(input));
|
|
5377
|
+
case "update_dashboard":
|
|
5378
|
+
return this.updateDashboard(actor, VerbInputs.update_dashboard.parse(input));
|
|
5179
5379
|
default:
|
|
5180
5380
|
throw new BoardError("VALIDATION", `unknown verb ${String(verb)}`);
|
|
5181
5381
|
}
|
|
@@ -5480,8 +5680,8 @@ ${rationale}`}`);
|
|
|
5480
5680
|
}
|
|
5481
5681
|
/**
|
|
5482
5682
|
* 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
|
|
5683
|
+
* its participants can reshape the plan. A landing's outcome, a merge's or a pull request's, is
|
|
5684
|
+
* posted to the task's thread, and a landed one announced in the task's channel.
|
|
5485
5685
|
*/
|
|
5486
5686
|
async finishCompletion(actor, input) {
|
|
5487
5687
|
return this.mutex.run(async () => {
|
|
@@ -5491,8 +5691,8 @@ ${rationale}`}`);
|
|
|
5491
5691
|
throw new BoardError("INVALID_STATE", `task ${current.id} is not completing`);
|
|
5492
5692
|
}
|
|
5493
5693
|
const ts = this.now().toISOString();
|
|
5494
|
-
const
|
|
5495
|
-
if (
|
|
5694
|
+
const landed = current.onDone !== "none";
|
|
5695
|
+
if (landed) {
|
|
5496
5696
|
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
5697
|
}
|
|
5498
5698
|
if (input.ok) {
|
|
@@ -5512,7 +5712,7 @@ ${rationale}`}`);
|
|
|
5512
5712
|
for (const name of new Set(task2.stages.flatMap((stage) => stage.completedBy ?? []))) {
|
|
5513
5713
|
await this.refreshMember(name);
|
|
5514
5714
|
}
|
|
5515
|
-
if (
|
|
5715
|
+
if (landed) {
|
|
5516
5716
|
await this.appendMessage(actor.name, channelRef(location.project, task2.channel), `Task ${task2.id} "${task2.title}" is done: ${sentence(input.detail)}.`);
|
|
5517
5717
|
}
|
|
5518
5718
|
return task2;
|
|
@@ -5542,15 +5742,31 @@ ${rationale}`}`);
|
|
|
5542
5742
|
async writeState(name, value) {
|
|
5543
5743
|
await this.mutex.run(() => writeJson(path6.join(this.paths.state(), `${name}.json`), value));
|
|
5544
5744
|
}
|
|
5545
|
-
/** The project's
|
|
5745
|
+
/** The project's dashboard, empty at revision 0 when it has none. */
|
|
5546
5746
|
async readDashboard(slug) {
|
|
5547
5747
|
await this.readProject(slug);
|
|
5748
|
+
return this.dashboardOf(slug);
|
|
5749
|
+
}
|
|
5750
|
+
/**
|
|
5751
|
+
* Read loosely, since agents wrote dashboards by hand before they had a verb: a revision or an
|
|
5752
|
+
* author that does not parse counts as none.
|
|
5753
|
+
*/
|
|
5754
|
+
async dashboardOf(slug) {
|
|
5548
5755
|
const file = this.paths.dashboard(slug);
|
|
5549
5756
|
if (!await exists(file)) {
|
|
5550
|
-
return {
|
|
5757
|
+
return { project: slug, revision: 0, body: "" };
|
|
5551
5758
|
}
|
|
5552
5759
|
const { data, body } = await readLooseMarkdown(file);
|
|
5553
|
-
|
|
5760
|
+
const revision = z12.number().int().min(0).safeParse(data["revision"]);
|
|
5761
|
+
const updatedBy = NameSchema.safeParse(data["updatedBy"]);
|
|
5762
|
+
const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
|
|
5763
|
+
return {
|
|
5764
|
+
project: slug,
|
|
5765
|
+
revision: revision.success ? revision.data : 0,
|
|
5766
|
+
...updatedBy.success ? { updatedBy: updatedBy.data } : {},
|
|
5767
|
+
...updatedAt.success ? { updatedAt: updatedAt.data } : {},
|
|
5768
|
+
body
|
|
5769
|
+
};
|
|
5554
5770
|
}
|
|
5555
5771
|
async listRunners() {
|
|
5556
5772
|
const runners2 = [];
|
|
@@ -8305,7 +8521,7 @@ function renderInstructions(input) {
|
|
|
8305
8521
|
if (norms.length > 0) {
|
|
8306
8522
|
sections.push("", "## Society norms", "", norms);
|
|
8307
8523
|
}
|
|
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
|
|
8524
|
+
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
8525
|
if (input.onboarding !== void 0) {
|
|
8310
8526
|
sections.push("", "## First turn", "", renderOnboardingPreamble(input.onboarding));
|
|
8311
8527
|
}
|
|
@@ -8352,7 +8568,7 @@ function planLine(task, whose) {
|
|
|
8352
8568
|
const index = stageIndex(task, task.stage);
|
|
8353
8569
|
const stage = task.stages[index];
|
|
8354
8570
|
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(", ")}`;
|
|
8571
|
+
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
8572
|
const gated = stage?.gate === true ? ", gated" : "";
|
|
8357
8573
|
return `- ${task.id} "${task.title}": ${stage?.name ?? task.stage} (${whose}, ${index + 1} of ${task.stages.length}${gated}); ${then}`;
|
|
8358
8574
|
}
|
|
@@ -8450,7 +8666,8 @@ function buildTurnPrompt(input) {
|
|
|
8450
8666
|
} else {
|
|
8451
8667
|
const { thread, task } = conversation;
|
|
8452
8668
|
const about = thread.subject === void 0 ? "a topic" : `${thread.subject.kind} ${thread.subject.id}`;
|
|
8453
|
-
|
|
8669
|
+
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}.`;
|
|
8670
|
+
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
8671
|
if (task !== null) {
|
|
8455
8672
|
lines.push("", `Task ${task.id} "${task.title}", ${task.status}:`, planLine(task, "now"));
|
|
8456
8673
|
if (task.body.trim() !== "") {
|
|
@@ -8514,8 +8731,13 @@ function buildTurnPrompt(input) {
|
|
|
8514
8731
|
}
|
|
8515
8732
|
}
|
|
8516
8733
|
const project = input.project;
|
|
8517
|
-
|
|
8518
|
-
|
|
8734
|
+
const landing = conversation?.task?.onDone ?? project?.onDone;
|
|
8735
|
+
if (project !== void 0 && project !== null && landing === "merge") {
|
|
8736
|
+
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.`);
|
|
8737
|
+
}
|
|
8738
|
+
if (project !== void 0 && project !== null && landing === "ghpr") {
|
|
8739
|
+
const base = project.defaultBranch;
|
|
8740
|
+
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.`);
|
|
8519
8741
|
}
|
|
8520
8742
|
if (conflicts.length > 0) {
|
|
8521
8743
|
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 +8941,12 @@ var TurnHost = class {
|
|
|
8719
8941
|
}
|
|
8720
8942
|
}
|
|
8721
8943
|
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 : {
|
|
8944
|
+
const conversation = threadRecord === void 0 ? null : {
|
|
8945
|
+
thread: threadRecord,
|
|
8946
|
+
task,
|
|
8947
|
+
fresh: threadSoFar !== null,
|
|
8948
|
+
...threadRecord.state === "closed" ? { replyIn: await this.replyIn(threadRecord) } : {}
|
|
8949
|
+
};
|
|
8723
8950
|
const ending = dispatch.trigger.kind === "closing" ? await this.endingOf(dispatch.project, threadRecord, task, channel) : null;
|
|
8724
8951
|
const prompt = buildTurnPrompt({
|
|
8725
8952
|
dispatch,
|
|
@@ -9122,11 +9349,7 @@ var TurnHost = class {
|
|
|
9122
9349
|
if (project === null) {
|
|
9123
9350
|
return { kind: "home", ...own };
|
|
9124
9351
|
}
|
|
9125
|
-
const repo =
|
|
9126
|
-
slug: project.slug,
|
|
9127
|
-
origin: project.repo,
|
|
9128
|
-
defaultBranch: project.defaultBranch
|
|
9129
|
-
};
|
|
9352
|
+
const repo = projectRepo(project);
|
|
9130
9353
|
if (thread?.task === true) {
|
|
9131
9354
|
return { kind: "task", repo, taskId: thread.id };
|
|
9132
9355
|
}
|
|
@@ -9141,6 +9364,10 @@ var TurnHost = class {
|
|
|
9141
9364
|
const conversation = workspaceConversationOf(job);
|
|
9142
9365
|
return conversation !== null && await this.board.conversationEnded(conversation);
|
|
9143
9366
|
}
|
|
9367
|
+
/** Where a closed thread's citizens say what still needs saying: its channel, else its place's general. */
|
|
9368
|
+
async replyIn(thread) {
|
|
9369
|
+
return await this.board.channelOpen(thread.channel) ? thread.channel : channelRef(parseChannelRef(thread.channel).project, "general");
|
|
9370
|
+
}
|
|
9144
9371
|
/**
|
|
9145
9372
|
* How a closing turn's conversation ended, and where its citizen asks someone else to take over
|
|
9146
9373
|
* work that is not its own: the thread's channel while it is open, else the place's general.
|
|
@@ -9149,11 +9376,7 @@ var TurnHost = class {
|
|
|
9149
9376
|
const place = scope === SOCIETY_SCOPE ? null : scope;
|
|
9150
9377
|
if (thread !== void 0) {
|
|
9151
9378
|
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
|
-
};
|
|
9379
|
+
return { how: closed, askIn: await this.replyIn(thread) };
|
|
9157
9380
|
}
|
|
9158
9381
|
if (channel === void 0) {
|
|
9159
9382
|
return null;
|
|
@@ -9246,12 +9469,14 @@ var DEFAULT_REQUEST_TIMEOUT_MS = 10 * 6e4;
|
|
|
9246
9469
|
var CONTROL_TIMEOUT_MS = 15e3;
|
|
9247
9470
|
var FILE_TIMEOUT_MS = 6e4;
|
|
9248
9471
|
var SWEEP_TIMEOUT_MS = 2 * 6e4;
|
|
9472
|
+
var DEFAULT_ACK_TIMEOUT_MS = 3e4;
|
|
9249
9473
|
var RunnerHub = class {
|
|
9250
9474
|
board;
|
|
9251
9475
|
host;
|
|
9252
9476
|
version;
|
|
9253
9477
|
graceMs;
|
|
9254
9478
|
requestTimeoutMs;
|
|
9479
|
+
ackTimeoutMs;
|
|
9255
9480
|
now;
|
|
9256
9481
|
log;
|
|
9257
9482
|
seats = /* @__PURE__ */ new Map();
|
|
@@ -9267,6 +9492,7 @@ var RunnerHub = class {
|
|
|
9267
9492
|
this.version = options.version;
|
|
9268
9493
|
this.graceMs = options.graceMs ?? DEFAULT_GRACE_MS;
|
|
9269
9494
|
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
9495
|
+
this.ackTimeoutMs = options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS;
|
|
9270
9496
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
9271
9497
|
this.log = options.log ?? SILENT2;
|
|
9272
9498
|
this.startedAt = this.now().getTime();
|
|
@@ -9282,9 +9508,15 @@ var RunnerHub = class {
|
|
|
9282
9508
|
// -------------------------------------------------------------------------------------------
|
|
9283
9509
|
// The protocol, as the server's routes call it
|
|
9284
9510
|
// -------------------------------------------------------------------------------------------
|
|
9511
|
+
/** The jobs sent to a runner that it has not acknowledged yet. */
|
|
9512
|
+
unacknowledged(runner) {
|
|
9513
|
+
return [...this.seats.get(runner)?.unacked.keys() ?? []];
|
|
9514
|
+
}
|
|
9285
9515
|
/**
|
|
9286
|
-
* A runner registers, first or again.
|
|
9287
|
-
*
|
|
9516
|
+
* A runner registers, first or again. A turn in flight it still runs, or whose job it never
|
|
9517
|
+
* acknowledged, carries on, the latter sent again once its stream opens, since a job written
|
|
9518
|
+
* into a stream that had stopped delivering never arrived; one it took and no longer runs,
|
|
9519
|
+
* because it restarted, ends as failed.
|
|
9288
9520
|
*/
|
|
9289
9521
|
async register(name, hello) {
|
|
9290
9522
|
if (hello.protocol !== RUNNER_PROTOCOL) {
|
|
@@ -9299,25 +9531,31 @@ var RunnerHub = class {
|
|
|
9299
9531
|
inUse: 0,
|
|
9300
9532
|
warm: /* @__PURE__ */ new Set(),
|
|
9301
9533
|
disconnectedAt: null,
|
|
9302
|
-
grace: null
|
|
9534
|
+
grace: null,
|
|
9535
|
+
unacked: /* @__PURE__ */ new Map(),
|
|
9536
|
+
unconfirmed: false
|
|
9303
9537
|
});
|
|
9304
9538
|
} else {
|
|
9305
9539
|
seat.hello = hello;
|
|
9306
9540
|
seat.warm = /* @__PURE__ */ new Set();
|
|
9307
9541
|
}
|
|
9308
9542
|
const still = new Set(hello.turns);
|
|
9543
|
+
const unacked = this.seats.get(name)?.unacked ?? /* @__PURE__ */ new Map();
|
|
9309
9544
|
for (const turnId of this.host.turnsOn(name)) {
|
|
9310
|
-
if (
|
|
9545
|
+
if (still.has(turnId)) {
|
|
9546
|
+
this.settle(unacked, turnId);
|
|
9547
|
+
} else if (!unacked.has(turnId)) {
|
|
9311
9548
|
await this.host.abortTurn(turnId, `runner ${name} restarted during the turn`);
|
|
9312
9549
|
}
|
|
9313
9550
|
}
|
|
9314
9551
|
await this.board.markRunner(name, {
|
|
9315
9552
|
os: hello.os,
|
|
9316
9553
|
clis: hello.clis,
|
|
9317
|
-
capabilities: hello.capabilities
|
|
9554
|
+
capabilities: hello.capabilities,
|
|
9555
|
+
version: hello.version
|
|
9318
9556
|
});
|
|
9319
9557
|
this.runnerChanged(name);
|
|
9320
|
-
this.log.info({ runner: name, clis: hello.clis, slots: hello.slots }, "runner registered");
|
|
9558
|
+
this.log.info({ runner: name, version: hello.version, clis: hello.clis, slots: hello.slots }, "runner registered");
|
|
9321
9559
|
return { name, version: this.version };
|
|
9322
9560
|
}
|
|
9323
9561
|
/** A registered runner's stream opened. Returns what to call when it closes. */
|
|
@@ -9328,10 +9566,17 @@ var RunnerHub = class {
|
|
|
9328
9566
|
}
|
|
9329
9567
|
seat.send = send;
|
|
9330
9568
|
seat.disconnectedAt = null;
|
|
9569
|
+
seat.unconfirmed = false;
|
|
9331
9570
|
if (seat.grace !== null) {
|
|
9332
9571
|
clearTimeout(seat.grace);
|
|
9333
9572
|
seat.grace = null;
|
|
9334
9573
|
}
|
|
9574
|
+
for (const { job } of seat.unacked.values()) {
|
|
9575
|
+
if (send({ type: "turn", job })) {
|
|
9576
|
+
this.expectAck(seat, job);
|
|
9577
|
+
this.log.info({ runner: name, turnId: job.turnId }, "sent again a job the runner never took");
|
|
9578
|
+
}
|
|
9579
|
+
}
|
|
9335
9580
|
await this.board.markRunner(name, { status: "connected" });
|
|
9336
9581
|
this.runnerChanged(name);
|
|
9337
9582
|
return async () => {
|
|
@@ -9356,6 +9601,9 @@ var RunnerHub = class {
|
|
|
9356
9601
|
clearTimeout(seat.grace);
|
|
9357
9602
|
seat.grace = null;
|
|
9358
9603
|
}
|
|
9604
|
+
for (const { timer } of seat.unacked.values()) {
|
|
9605
|
+
clearTimeout(timer);
|
|
9606
|
+
}
|
|
9359
9607
|
}
|
|
9360
9608
|
await Promise.allSettled(this.detaching);
|
|
9361
9609
|
}
|
|
@@ -9363,6 +9611,18 @@ var RunnerHub = class {
|
|
|
9363
9611
|
this.assertOwns(runner, turnId);
|
|
9364
9612
|
await this.host.addEvents(turnId, entries);
|
|
9365
9613
|
}
|
|
9614
|
+
/** A runner's word that a turn's job arrived, which also shows its stream delivers again. */
|
|
9615
|
+
received(runner, turnId) {
|
|
9616
|
+
const seat = this.seats.get(runner);
|
|
9617
|
+
if (seat === void 0 || !seat.unacked.has(turnId)) {
|
|
9618
|
+
return;
|
|
9619
|
+
}
|
|
9620
|
+
this.settle(seat.unacked, turnId);
|
|
9621
|
+
if (seat.unconfirmed) {
|
|
9622
|
+
seat.unconfirmed = false;
|
|
9623
|
+
this.log.info({ runner }, "runner is taking jobs again");
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
9366
9626
|
async turnOutcome(runner, turnId, outcome) {
|
|
9367
9627
|
this.assertOwns(runner, turnId);
|
|
9368
9628
|
const ack = await this.host.closeTurn(turnId, outcome);
|
|
@@ -9506,7 +9766,7 @@ var RunnerHub = class {
|
|
|
9506
9766
|
return { refused: `${agent.name} has no CLI binding` };
|
|
9507
9767
|
}
|
|
9508
9768
|
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);
|
|
9769
|
+
const free = (seat) => seat.send !== null && !seat.unconfirmed && seat.hello.clis.includes(cli) && (seat.hello.slots === null || seat.inUse < seat.hello.slots);
|
|
9510
9770
|
let chosen;
|
|
9511
9771
|
if (dispatch.project === SOCIETY_SCOPE) {
|
|
9512
9772
|
chosen = await this.pinnedSeat(agent.name, agent.homeRunner, cli, free);
|
|
@@ -9545,6 +9805,7 @@ var RunnerHub = class {
|
|
|
9545
9805
|
return this.host.refuseTurn(dispatch, "none", assignment.refused);
|
|
9546
9806
|
}
|
|
9547
9807
|
const seat = this.seats.get(assignment.runner);
|
|
9808
|
+
let turnId = null;
|
|
9548
9809
|
try {
|
|
9549
9810
|
if (seat === void 0) {
|
|
9550
9811
|
return await this.host.refuseTurn(dispatch, assignment.runner, "the runner is gone");
|
|
@@ -9555,11 +9816,17 @@ var RunnerHub = class {
|
|
|
9555
9816
|
}
|
|
9556
9817
|
if (seat.send === null || !seat.send({ type: "turn", job: opened.job })) {
|
|
9557
9818
|
await this.host.abortTurn(opened.job.turnId, `runner ${seat.name} went away before the turn`);
|
|
9819
|
+
} else {
|
|
9820
|
+
this.expectAck(seat, opened.job);
|
|
9558
9821
|
}
|
|
9822
|
+
turnId = opened.job.turnId;
|
|
9559
9823
|
return await opened.ended;
|
|
9560
9824
|
} finally {
|
|
9561
9825
|
if (seat !== void 0) {
|
|
9562
9826
|
seat.inUse = Math.max(0, seat.inUse - 1);
|
|
9827
|
+
if (turnId !== null) {
|
|
9828
|
+
this.settle(seat.unacked, turnId);
|
|
9829
|
+
}
|
|
9563
9830
|
}
|
|
9564
9831
|
}
|
|
9565
9832
|
}
|
|
@@ -9568,7 +9835,7 @@ var RunnerHub = class {
|
|
|
9568
9835
|
if (!task.completing) {
|
|
9569
9836
|
return "done";
|
|
9570
9837
|
}
|
|
9571
|
-
if (task.onDone
|
|
9838
|
+
if (task.onDone === "none") {
|
|
9572
9839
|
await this.board.finishCompletion(SYSTEM_ACTOR, {
|
|
9573
9840
|
taskId,
|
|
9574
9841
|
ok: true,
|
|
@@ -9578,6 +9845,15 @@ var RunnerHub = class {
|
|
|
9578
9845
|
}
|
|
9579
9846
|
const record = await this.board.readProject(project);
|
|
9580
9847
|
const branch = `task/${taskId}`;
|
|
9848
|
+
const pull = task.onDone === "ghpr" ? task.pullRequest : void 0;
|
|
9849
|
+
if (task.onDone === "ghpr" && pull === void 0) {
|
|
9850
|
+
await this.board.finishCompletion(SYSTEM_ACTOR, {
|
|
9851
|
+
taskId,
|
|
9852
|
+
ok: false,
|
|
9853
|
+
detail: "no pull request is linked to the task"
|
|
9854
|
+
});
|
|
9855
|
+
return "done";
|
|
9856
|
+
}
|
|
9581
9857
|
let outcome = { ok: true, detail: "nothing to land, since the task left no branch" };
|
|
9582
9858
|
if (record.runner !== void 0) {
|
|
9583
9859
|
const seat = this.seats.get(record.runner);
|
|
@@ -9589,8 +9865,15 @@ var RunnerHub = class {
|
|
|
9589
9865
|
type: "land",
|
|
9590
9866
|
request,
|
|
9591
9867
|
land: {
|
|
9592
|
-
repo:
|
|
9593
|
-
branch
|
|
9868
|
+
repo: projectRepo(record),
|
|
9869
|
+
branch,
|
|
9870
|
+
...pull === void 0 ? {} : {
|
|
9871
|
+
pullRequest: {
|
|
9872
|
+
url: pull.url,
|
|
9873
|
+
...pull.subject === void 0 ? {} : { subject: pull.subject },
|
|
9874
|
+
...pull.body === void 0 ? {} : { body: pull.body }
|
|
9875
|
+
}
|
|
9876
|
+
}
|
|
9594
9877
|
}
|
|
9595
9878
|
})));
|
|
9596
9879
|
} catch (error) {
|
|
@@ -9666,6 +9949,34 @@ var RunnerHub = class {
|
|
|
9666
9949
|
}
|
|
9667
9950
|
}
|
|
9668
9951
|
// -------------------------------------------------------------------------------------------
|
|
9952
|
+
/**
|
|
9953
|
+
* Waits for a runner to acknowledge a job. One that does not in time may be behind a stream that
|
|
9954
|
+
* looks open but delivers nothing, as when its machine's network stops passing traffic, so no
|
|
9955
|
+
* more turns go to it until it acknowledges or connects again; the turns it has carry on, since
|
|
9956
|
+
* they report by posts.
|
|
9957
|
+
*/
|
|
9958
|
+
expectAck(seat, job) {
|
|
9959
|
+
const previous = seat.unacked.get(job.turnId);
|
|
9960
|
+
if (previous !== void 0) {
|
|
9961
|
+
clearTimeout(previous.timer);
|
|
9962
|
+
}
|
|
9963
|
+
const timer = setTimeout(() => {
|
|
9964
|
+
if (!seat.unacked.has(job.turnId) || seat.unconfirmed || seat.send === null) {
|
|
9965
|
+
return;
|
|
9966
|
+
}
|
|
9967
|
+
seat.unconfirmed = true;
|
|
9968
|
+
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");
|
|
9969
|
+
}, this.ackTimeoutMs);
|
|
9970
|
+
timer.unref?.();
|
|
9971
|
+
seat.unacked.set(job.turnId, { job, timer });
|
|
9972
|
+
}
|
|
9973
|
+
settle(unacked, turnId) {
|
|
9974
|
+
const entry = unacked.get(turnId);
|
|
9975
|
+
if (entry !== void 0) {
|
|
9976
|
+
clearTimeout(entry.timer);
|
|
9977
|
+
unacked.delete(turnId);
|
|
9978
|
+
}
|
|
9979
|
+
}
|
|
9669
9980
|
async detach(seat) {
|
|
9670
9981
|
seat.send = null;
|
|
9671
9982
|
this.runnerChanged(seat.name);
|
|
@@ -9686,7 +9997,7 @@ var RunnerHub = class {
|
|
|
9686
9997
|
return;
|
|
9687
9998
|
}
|
|
9688
9999
|
for (const turnId of this.host.turnsOn(seat.name)) {
|
|
9689
|
-
void this.host.abortTurn(turnId, `runner ${seat.name} went away during the turn`);
|
|
10000
|
+
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
10001
|
}
|
|
9691
10002
|
}, this.graceMs);
|
|
9692
10003
|
seat.grace.unref?.();
|
|
@@ -9763,7 +10074,7 @@ var RunnerHub = class {
|
|
|
9763
10074
|
}
|
|
9764
10075
|
return {
|
|
9765
10076
|
seat,
|
|
9766
|
-
repo:
|
|
10077
|
+
repo: projectRepo(record)
|
|
9767
10078
|
};
|
|
9768
10079
|
}
|
|
9769
10080
|
assertOwns(runner, turnId) {
|
|
@@ -10228,6 +10539,10 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
10228
10539
|
await hub.turnEvents(c.get("runner"), UlidSchema.parse(c.req.param("id")), entries);
|
|
10229
10540
|
return c.json({ ok: true });
|
|
10230
10541
|
});
|
|
10542
|
+
routes.post("/turns/:id/received", (c) => {
|
|
10543
|
+
hub.received(c.get("runner"), UlidSchema.parse(c.req.param("id")));
|
|
10544
|
+
return c.json({ ok: true });
|
|
10545
|
+
});
|
|
10231
10546
|
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
10547
|
routes.post("/requests/:id", async (c) => {
|
|
10233
10548
|
hub.answer(c.get("runner"), c.req.param("id"), RunnerAnswerSchema.parse(await c.req.json()));
|
|
@@ -10402,6 +10717,7 @@ function createApp(deps) {
|
|
|
10402
10717
|
});
|
|
10403
10718
|
api.delete("/sign-in", async (c) => c.json({ signedOut: await board2.signOut(bearer2(c) ?? "") }));
|
|
10404
10719
|
api.get("/society", async (c) => c.json(await board2.society()));
|
|
10720
|
+
api.get("/version", (c) => c.json({ version }));
|
|
10405
10721
|
api.get("/agents", async (c) => c.json((await board2.listAgents()).map(({ tokenHash: _hash, ...agent }) => agent)));
|
|
10406
10722
|
api.get("/members", async (c) => c.json(await board2.listMembers()));
|
|
10407
10723
|
api.get("/agents/:name/turns", async (c) => c.json(await board2.listTurns(c.req.param("name"), Number(c.req.query("limit") ?? "50"))));
|