@kubitnodes/stellaris 0.3.26 → 0.4.0
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 +752 -184
- package/dist/server/index.js +1528 -276
- package/package.json +2 -2
- package/web/dist/assets/{BackLink-UTiLF1J4.js → BackLink-BLiSCuKv.js} +1 -1
- package/web/dist/assets/{Button-Cp4cNRUX.js → Button-D4-iu6aX.js} +1 -1
- package/web/dist/assets/ChannelForms-DjviQfVm.js +1 -0
- package/web/dist/assets/{ChannelView-CY3oG9hZ.js → ChannelView-B-0H7Ps8.js} +2 -2
- package/web/dist/assets/CitizenView-gE5GgCBa.js +2 -0
- package/web/dist/assets/{CitizensView-WPQ6Jhl3.js → CitizensView-DXD3qkq1.js} +1 -1
- package/web/dist/assets/{CliIcon-C7bwChMP.js → CliIcon-DL4ZwgZv.js} +1 -1
- package/web/dist/assets/{CronsView-_pdgwX1I.js → CronsView-CX_G5D3h.js} +1 -1
- package/web/dist/assets/{FileTree-jyDUSu7g.js → FileTree-BFa_KVI8.js} +1 -1
- package/web/dist/assets/{FileView-DcQvlc8V.js → FileView-Cd9Ijarf.js} +2 -2
- package/web/dist/assets/KnowledgeView-D_a4pjGV.js +1 -0
- package/web/dist/assets/{Markdown-DT3V-EVe.js → Markdown-DPqvK--4.js} +4 -4
- package/web/dist/assets/{MessageItem-agtEugfG.js → MessageItem-WBTzWwyj.js} +1 -1
- package/web/dist/assets/MetricsView-YUkYixqT.js +1 -0
- package/web/dist/assets/NeedsYouView-CE_B9bZQ.js +1 -0
- package/web/dist/assets/Overview-BGUnf6K1.js +1 -0
- package/web/dist/assets/{Pane-BaM0TdE6.js → Pane-C9TbEME4.js} +1 -1
- package/web/dist/assets/{Picker-C2ig-0dT.js → Picker-BK48MjLY.js} +1 -1
- package/web/dist/assets/ProjectView-sL3aGAC6.js +1 -0
- package/web/dist/assets/ProposalView-BhZR4MIR.js +1 -0
- package/web/dist/assets/{ProposalsView-p4KjwV9p.js → ProposalsView-DTGx8NDn.js} +1 -1
- package/web/dist/assets/{Quota-CfFGIql9.js → Quota-BkF7pa0u.js} +1 -1
- package/web/dist/assets/RunnersView-Bl62pXNw.js +1 -0
- package/web/dist/assets/{SkillFileList-CgrCnuaC.js → SkillFileList-BUpKUkaG.js} +1 -1
- package/web/dist/assets/{SkillView-Di5Z9h2L.js → SkillView-BQJApSh7.js} +1 -1
- package/web/dist/assets/{SocietyView-BDiVxUC5.js → SocietyView-JSdVE50m.js} +1 -1
- package/web/dist/assets/{StickyList-BaV_KZ8x.js → StickyList-DafbJutb.js} +1 -1
- package/web/dist/assets/{SubjectThread-DD_ZCCnr.js → SubjectThread-Jyl5YCFL.js} +1 -1
- package/web/dist/assets/{TaskView-CKcs-MOC.js → TaskView-Jke2pVGT.js} +2 -2
- package/web/dist/assets/{TasksView-oZMjncOU.js → TasksView-zJTIaqEA.js} +1 -1
- package/web/dist/assets/{ThreadForms-CG5Y926b.js → ThreadForms-DwUOktyP.js} +1 -1
- package/web/dist/assets/{ThreadItem-D-n2arFA.js → ThreadItem-Blx8gSvO.js} +1 -1
- package/web/dist/assets/{ThreadView-CT9vd0P4.js → ThreadView-CdQpBb7M.js} +1 -1
- package/web/dist/assets/citizen-BOn5BBRL.js +1 -0
- package/web/dist/assets/compose-D-X3BQgY.js +1 -0
- package/web/dist/assets/format-Dhd2NUB3.js +55 -0
- package/web/dist/assets/{governance-C6ez5MLh.js → governance-CWzi7hzX.js} +1 -1
- package/web/dist/assets/index-CA_njyMe.css +2 -0
- package/web/dist/assets/index-iplooQqq.js +24 -0
- package/web/dist/assets/{mentions-BEL5U6ra.js → mentions-S-r32HKM.js} +1 -1
- package/web/dist/assets/{tasks-CAI8Eci7.js → tasks-D_Fy7dtN.js} +1 -1
- package/web/dist/assets/useMatch-DZXCHHaG.js +1 -0
- package/web/dist/index.html +9 -9
- package/web/dist/assets/ChannelForms-DD2B0tey.js +0 -1
- package/web/dist/assets/CitizenView-BNK5ro-6.js +0 -2
- package/web/dist/assets/KnowledgeView-DDl0HQPG.js +0 -1
- package/web/dist/assets/MetricsView-DiXyYeWP.js +0 -1
- package/web/dist/assets/NeedsYouView-CKmuYan_.js +0 -1
- package/web/dist/assets/Overview-CKcTqsMM.js +0 -1
- package/web/dist/assets/ProjectView-T2OHTh5j.js +0 -1
- package/web/dist/assets/ProposalView-B5yjTAcn.js +0 -1
- package/web/dist/assets/RunnersView-DdvwoOur.js +0 -1
- package/web/dist/assets/citizen-BiHfph2A.js +0 -1
- package/web/dist/assets/compose-DY1WL8zB.js +0 -1
- package/web/dist/assets/format-CENiwkJC.js +0 -55
- package/web/dist/assets/index-CPXALUV6.js +0 -23
- package/web/dist/assets/index-DIa8Dlav.css +0 -2
- package/web/dist/assets/useMatch-CINzyDWa.js +0 -1
package/dist/server/index.js
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// apps/server/dist/index.js
|
|
4
4
|
import { access as access2 } from "node:fs/promises";
|
|
5
|
-
import
|
|
6
|
-
import { serve } from "@hono/node-server";
|
|
5
|
+
import path11 from "node:path";
|
|
7
6
|
|
|
8
7
|
// packages/board-core/dist/board.js
|
|
9
|
-
import { chmod as chmod2, cp, mkdir as
|
|
10
|
-
import
|
|
8
|
+
import { chmod as chmod2, cp, mkdir as mkdir5, readdir as readdir4, readFile as readFile4, rename as rename3, rm as rm3, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
|
|
9
|
+
import path8 from "node:path";
|
|
11
10
|
import { decodeTime, monotonicFactory } from "ulid";
|
|
12
11
|
import { z as z16 } from "zod";
|
|
13
12
|
|
|
@@ -65,7 +64,8 @@ var VERB_NAMES = [
|
|
|
65
64
|
"update_cron",
|
|
66
65
|
"remove_cron",
|
|
67
66
|
"remove_skill",
|
|
68
|
-
"upload_image"
|
|
67
|
+
"upload_image",
|
|
68
|
+
"read_image"
|
|
69
69
|
];
|
|
70
70
|
var VerbNameSchema = z2.enum(VERB_NAMES);
|
|
71
71
|
var CharterTriggerSchema = z2.enum(["user_post", "ops_event", "heartbeat"]);
|
|
@@ -95,6 +95,7 @@ var USER_NAME = "user";
|
|
|
95
95
|
var MEMBER_VERBS = [
|
|
96
96
|
"post_message",
|
|
97
97
|
"upload_image",
|
|
98
|
+
"read_image",
|
|
98
99
|
"read_inbox",
|
|
99
100
|
"search",
|
|
100
101
|
"open_thread",
|
|
@@ -226,7 +227,12 @@ var CronScheduleSchema = z3.union([
|
|
|
226
227
|
var CronRunSchema = z3.object({
|
|
227
228
|
turnId: UlidSchema,
|
|
228
229
|
at: IsoDateTimeSchema,
|
|
229
|
-
outcome: z3.enum(["completed", "failed", "stopped"])
|
|
230
|
+
outcome: z3.enum(["completed", "failed", "stopped"]),
|
|
231
|
+
/**
|
|
232
|
+
* For a failed one, the first turn of the cron's citizen in its conversation that was shown the
|
|
233
|
+
* failure and completed, and when it ended.
|
|
234
|
+
*/
|
|
235
|
+
caughtUp: z3.object({ turnId: UlidSchema, at: IsoDateTimeSchema }).optional()
|
|
230
236
|
});
|
|
231
237
|
var CronFrontmatterSchema = z3.object({
|
|
232
238
|
id: UlidSchema,
|
|
@@ -273,6 +279,16 @@ function shortestGapMs(schedule, societyZone, after) {
|
|
|
273
279
|
}
|
|
274
280
|
return shortest;
|
|
275
281
|
}
|
|
282
|
+
function firesInTask(cron, task) {
|
|
283
|
+
return cron.scope === task.project && cron.thread === task.id;
|
|
284
|
+
}
|
|
285
|
+
function cronFailure(cron) {
|
|
286
|
+
return cron.lastRun?.outcome === "failed" && cron.lastRun.caughtUp === void 0 ? cron.lastRun : null;
|
|
287
|
+
}
|
|
288
|
+
function cronStoppedAt(cron) {
|
|
289
|
+
const times = [cron.paused?.at, cron.ended?.at, cronFailure(cron)?.at].filter((at) => at !== void 0);
|
|
290
|
+
return times.reduce((first, at) => first === null || Date.parse(at) < Date.parse(first) ? at : first, null);
|
|
291
|
+
}
|
|
276
292
|
function nextFire(cron, societyZone, floorMs = CRON_FLOOR_MS) {
|
|
277
293
|
if (cron.ended !== void 0 || cron.paused !== void 0) {
|
|
278
294
|
return null;
|
|
@@ -704,6 +720,7 @@ var MemberSchema = z5.object({
|
|
|
704
720
|
});
|
|
705
721
|
var RunnerOsSchema = z5.enum(["linux", "windows", "darwin"]);
|
|
706
722
|
var GitIdentityPartSchema = z5.enum(["name", "email"]);
|
|
723
|
+
var RunnerPauseSchema = z5.object({ at: IsoDateTimeSchema, by: NameSchema });
|
|
707
724
|
var RunnerSchema = z5.object({
|
|
708
725
|
name: NameSchema,
|
|
709
726
|
os: RunnerOsSchema,
|
|
@@ -722,8 +739,25 @@ var RunnerSchema = z5.object({
|
|
|
722
739
|
* Each CLI's plan quota as its login on this runner last reported it, from a turn's stream or a
|
|
723
740
|
* read; absent until one has.
|
|
724
741
|
*/
|
|
725
|
-
quotas: z5.partialRecord(CliKindSchema, CliQuotaSchema).optional()
|
|
742
|
+
quotas: z5.partialRecord(CliKindSchema, CliQuotaSchema).optional(),
|
|
743
|
+
/** The user's pause of the whole runner: no turn starts on it until the user resumes it. */
|
|
744
|
+
paused: RunnerPauseSchema.optional(),
|
|
745
|
+
/**
|
|
746
|
+
* The user's pauses of single CLIs on the runner, each apart from the runner's own: no turn of
|
|
747
|
+
* that CLI starts on it until the user resumes it.
|
|
748
|
+
*/
|
|
749
|
+
pausedClis: z5.partialRecord(CliKindSchema, RunnerPauseSchema).optional()
|
|
726
750
|
});
|
|
751
|
+
function runnerPause(runner, cli) {
|
|
752
|
+
return runner.paused ?? runner.pausedClis?.[cli];
|
|
753
|
+
}
|
|
754
|
+
function heldOn(runners2, runner, cli) {
|
|
755
|
+
if (runner === void 0 || cli === null) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
const record = runners2.find((each) => each.name === runner);
|
|
759
|
+
return record !== void 0 && runnerPause(record, cli) !== void 0;
|
|
760
|
+
}
|
|
727
761
|
var ProposalKindSchema = z5.enum([
|
|
728
762
|
"role",
|
|
729
763
|
"member",
|
|
@@ -1073,7 +1107,21 @@ function imageTypeOf(bytes) {
|
|
|
1073
1107
|
return null;
|
|
1074
1108
|
}
|
|
1075
1109
|
var ImagePathSchema = z6.string().min(1).max(4096).refine((value) => !value.includes("\0"), { message: "a path with no NUL character" });
|
|
1110
|
+
var ImageNameSchema = z6.string().max(1024);
|
|
1111
|
+
function imageDescription(name) {
|
|
1112
|
+
return name.replaceAll(/[[\]\\\r\n`]/g, "");
|
|
1113
|
+
}
|
|
1076
1114
|
var IMAGE_SCHEME = "image:";
|
|
1115
|
+
var ImageRefSchema = z6.string().regex(/^(?:image:)?[0-9A-HJKMNP-TV-Z]{26}$/, "expected an image's id or image:<id>");
|
|
1116
|
+
function imageIdOf(ref) {
|
|
1117
|
+
return ref.startsWith(IMAGE_SCHEME) ? ref.slice(IMAGE_SCHEME.length) : ref;
|
|
1118
|
+
}
|
|
1119
|
+
var ImageFileSchema = z6.object({
|
|
1120
|
+
id: UlidSchema,
|
|
1121
|
+
type: ImageTypeSchema,
|
|
1122
|
+
size: z6.number().int().positive(),
|
|
1123
|
+
path: ImagePathSchema
|
|
1124
|
+
});
|
|
1077
1125
|
var UploadedImageSchema = z6.object({
|
|
1078
1126
|
id: UlidSchema,
|
|
1079
1127
|
type: ImageTypeSchema,
|
|
@@ -1238,6 +1286,10 @@ var VerbInputs = {
|
|
|
1238
1286
|
upload_image: z7.object({
|
|
1239
1287
|
/** The image file on the machine your turn runs on: absolute, or relative to your working directory. */
|
|
1240
1288
|
path: ImagePathSchema
|
|
1289
|
+
}),
|
|
1290
|
+
read_image: z7.object({
|
|
1291
|
+
/** The image's id, or its `image:<id>` address as a post shows it. */
|
|
1292
|
+
image_id: ImageRefSchema
|
|
1241
1293
|
})
|
|
1242
1294
|
};
|
|
1243
1295
|
var VERB_DESCRIPTIONS = {
|
|
@@ -1249,7 +1301,7 @@ var VERB_DESCRIPTIONS = {
|
|
|
1249
1301
|
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.",
|
|
1250
1302
|
claim_task: "Hold the task's current stage. Claims are leases renewed by every turn that touches the task.",
|
|
1251
1303
|
release_task: "Let go of the stage you hold so someone else can take it.",
|
|
1252
|
-
update_task: "Move a task back to an earlier stage (the current stage's holder, the user, the steward, or the concierge; anyone else asks the concierge in the task's thread), abandon it, set the tasks it is blocked by (blocked_by: its stage wakes nobody until they have all ended), 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.",
|
|
1304
|
+
update_task: "Move a task back to an earlier stage (the current stage's holder, the user, the steward, or the concierge; anyone else asks the concierge in the task's thread, with a mention), abandon it, set the tasks it is blocked by (blocked_by: its stage wakes nobody until they have all ended), 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.",
|
|
1253
1305
|
get_task: "Read a task with its plan, its brief, and the messages of its thread.",
|
|
1254
1306
|
subscribe: "Subscribe to a channel. Subscriptions feed your digest; they never wake you.",
|
|
1255
1307
|
unsubscribe: "Unsubscribe from a channel.",
|
|
@@ -1272,7 +1324,8 @@ var VERB_DESCRIPTIONS = {
|
|
|
1272
1324
|
update_cron: "Pause or resume a cron, or change its title, note, or schedule; resuming or rescheduling starts it afresh from now. Yours, or anyone's for the user, the steward, and the concierge.",
|
|
1273
1325
|
remove_cron: "End a cron for good, with a reason. Yours, or anyone's for the user, the steward, and the concierge. A cron also ends with its conversation, and a one-time cron once it fires.",
|
|
1274
1326
|
remove_skill: "Remove a skill from the society's skills, with a reason, when it is wrong, superseded, or no longer used: no skills index lists it from the next turn, and a notice in general says so with the reason. Its proposer's own copy stays in its home. Steward and user only; the board keeps its text aside for the user to restore.",
|
|
1275
|
-
upload_image: "Upload an image file, such as a screenshot or a chart, from the machine your turn runs on: a PNG, JPEG, GIF, or WebP of at most 8 MB, in your worktree, your home, or the system temp folder. Returns its id and the markdown that shows it in a post, ``; write what it shows as the alt text."
|
|
1327
|
+
upload_image: "Upload an image file, such as a screenshot or a chart, from the machine your turn runs on: a PNG, JPEG, GIF, or WebP of at most 8 MB, in your worktree, your home, or the system temp folder. Returns its id and the markdown that shows it in a post, ``; write what it shows as the alt text.",
|
|
1328
|
+
read_image: "Open an image the board keeps, as a post shows it, ``, by its id: the board writes it to a file of this turn's own on the machine it runs on, removed when the turn ends, and returns its path, type, and size. Open the path with your file tools to see the image."
|
|
1276
1329
|
};
|
|
1277
1330
|
|
|
1278
1331
|
// packages/shared/dist/events.js
|
|
@@ -1283,6 +1336,7 @@ var BOARD_EVENT_TYPES = [
|
|
|
1283
1336
|
"project.configured",
|
|
1284
1337
|
"project.archived",
|
|
1285
1338
|
"project.placed",
|
|
1339
|
+
"project.moved",
|
|
1286
1340
|
"agent.added",
|
|
1287
1341
|
"agent.configured",
|
|
1288
1342
|
"agent.placed",
|
|
@@ -1322,6 +1376,8 @@ var BOARD_EVENT_TYPES = [
|
|
|
1322
1376
|
"user.token_rotated",
|
|
1323
1377
|
"runner.changed",
|
|
1324
1378
|
"runner.quota",
|
|
1379
|
+
"runner.paused",
|
|
1380
|
+
"runner.resumed",
|
|
1325
1381
|
"ops.signal",
|
|
1326
1382
|
"paused.changed",
|
|
1327
1383
|
"wake.requested",
|
|
@@ -1471,6 +1527,16 @@ var RunnerHelloSchema = z9.object({
|
|
|
1471
1527
|
quotaReads: z9.boolean().default(false),
|
|
1472
1528
|
/** Whether the runner reads an image a turn running there uploads; one from before never does. */
|
|
1473
1529
|
imageReads: z9.boolean().default(false),
|
|
1530
|
+
/**
|
|
1531
|
+
* Whether the runner writes an image the board sends for a turn running there to open, for
|
|
1532
|
+
* `read_image`; one from before never does.
|
|
1533
|
+
*/
|
|
1534
|
+
imageWrites: z9.boolean().default(false),
|
|
1535
|
+
/**
|
|
1536
|
+
* Whether the runner carries a project's repository out to another runner and in from one, and
|
|
1537
|
+
* removes its copy once the project has moved; one from before never does.
|
|
1538
|
+
*/
|
|
1539
|
+
projectMoves: z9.boolean().default(false),
|
|
1474
1540
|
capabilities: z9.array(z9.string()).default([]),
|
|
1475
1541
|
/**
|
|
1476
1542
|
* What the runner's account lacks of the git identity citizens' own commits carry, checked
|
|
@@ -1580,8 +1646,19 @@ var TurnJobSchema = z9.object({
|
|
|
1580
1646
|
session: z9.string().min(1).nullable(),
|
|
1581
1647
|
/** The running total the session reported after its last turn, which a resumed session starts from. */
|
|
1582
1648
|
costSoFarUsd: z9.number().nonnegative(),
|
|
1649
|
+
/**
|
|
1650
|
+
* The session's running usage after its last turn, which a resumed session's own usage is
|
|
1651
|
+
* measured from. Absent when the board holds none for the session, and from a server from before.
|
|
1652
|
+
*/
|
|
1653
|
+
usageSoFar: UsageSchema.optional(),
|
|
1583
1654
|
/** The rendered charter, norms, memory core, and skills index, with `PATH_TOKENS` for places. */
|
|
1584
1655
|
instructions: z9.string(),
|
|
1656
|
+
/**
|
|
1657
|
+
* The hash of `instructions` without the first turn's section (`instructionsHash` in
|
|
1658
|
+
* `turn-host`): a warm process started on other instructions starts afresh, so a compaction in
|
|
1659
|
+
* it puts back the current ones. Absent from a server from before, when the text is compared.
|
|
1660
|
+
*/
|
|
1661
|
+
instructionsHash: z9.string().optional(),
|
|
1585
1662
|
prompt: z9.string(),
|
|
1586
1663
|
mcp: z9.object({ url: z9.string().min(1), token: z9.string().min(1) }),
|
|
1587
1664
|
limits: z9.object({
|
|
@@ -1608,6 +1685,17 @@ var TurnOutcomeSchema = z9.object({
|
|
|
1608
1685
|
costUsd: z9.number().nonnegative(),
|
|
1609
1686
|
/** The session's running total after this turn, for CLIs that report one. */
|
|
1610
1687
|
sessionCostUsd: z9.number().nonnegative().optional(),
|
|
1688
|
+
/**
|
|
1689
|
+
* The session's running usage after this turn, for CLIs that report one, else the job's
|
|
1690
|
+
* `usageSoFar`. A runner from before never sends it.
|
|
1691
|
+
*/
|
|
1692
|
+
sessionUsage: UsageSchema.optional(),
|
|
1693
|
+
/**
|
|
1694
|
+
* The whole input of the turn's latest model call in the session's own thread, cached or not:
|
|
1695
|
+
* how large the session had grown. Sub-agents' calls are left out. A turn that made no call,
|
|
1696
|
+
* and a runner from before, never send it.
|
|
1697
|
+
*/
|
|
1698
|
+
contextTokens: z9.number().int().nonnegative().optional(),
|
|
1611
1699
|
/**
|
|
1612
1700
|
* The session the CLI used, which differs from the job's for CLIs that assign their own ids. A
|
|
1613
1701
|
* turn whose CLI never started, because it failed or was stopped first, reports the job's own,
|
|
@@ -1755,6 +1843,13 @@ var TurnImageSchema = z9.discriminatedUnion("kind", [
|
|
|
1755
1843
|
}),
|
|
1756
1844
|
z9.object({ kind: z9.literal("refused"), why: ImageRefusalSchema, detail: z9.string() })
|
|
1757
1845
|
]);
|
|
1846
|
+
var TurnImageWriteSchema = z9.object({
|
|
1847
|
+
turnId: UlidSchema,
|
|
1848
|
+
id: UlidSchema,
|
|
1849
|
+
type: ImageTypeSchema,
|
|
1850
|
+
content: z9.string()
|
|
1851
|
+
});
|
|
1852
|
+
var TurnImageFileSchema = z9.object({ path: ImagePathSchema });
|
|
1758
1853
|
var WorkspaceConversationSchema = z9.discriminatedUnion("kind", [
|
|
1759
1854
|
z9.object({ kind: z9.literal("thread"), id: UlidSchema }),
|
|
1760
1855
|
z9.object({ kind: z9.literal("channel"), scope: NameSchema, name: NameSchema }),
|
|
@@ -1776,6 +1871,49 @@ var HeldWorkspaceSchema = z9.object({
|
|
|
1776
1871
|
});
|
|
1777
1872
|
var HeldWorkspacesSchema = z9.object({ workspaces: z9.array(HeldWorkspaceSchema) });
|
|
1778
1873
|
var SweepResultSchema = z9.object({ kept: z9.array(HeldWorkspaceSchema) });
|
|
1874
|
+
var MOVE_PACK_LIMIT_BYTES = 256 * 1024 * 1024;
|
|
1875
|
+
var MOVE_FLOOR_BYTES_PER_SECOND = 16 * 1024;
|
|
1876
|
+
function moveTransferMs(bytes) {
|
|
1877
|
+
return 10 * 6e4 + Math.ceil(bytes / MOVE_FLOOR_BYTES_PER_SECOND) * 1e3;
|
|
1878
|
+
}
|
|
1879
|
+
var RepoRefsSchema = z9.record(z9.string().regex(/^refs\/(heads|tags)\/\S+$/), z9.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/));
|
|
1880
|
+
var ProjectExportSchema = z9.object({ move: UlidSchema, repo: ProjectRepoSchema });
|
|
1881
|
+
var HeldWorkSchema = z9.object({
|
|
1882
|
+
agent: NameSchema,
|
|
1883
|
+
conversation: WorkspaceConversationSchema.nullable(),
|
|
1884
|
+
uncommitted: z9.number().int().nonnegative(),
|
|
1885
|
+
onNoBranch: z9.number().int().nonnegative()
|
|
1886
|
+
});
|
|
1887
|
+
var ProjectExportedSchema = z9.discriminatedUnion("ok", [
|
|
1888
|
+
z9.object({
|
|
1889
|
+
ok: z9.literal(true),
|
|
1890
|
+
refs: RepoRefsSchema,
|
|
1891
|
+
bytes: z9.number().int().nonnegative()
|
|
1892
|
+
}),
|
|
1893
|
+
z9.object({
|
|
1894
|
+
ok: z9.literal(false),
|
|
1895
|
+
held: z9.array(HeldWorkSchema),
|
|
1896
|
+
bytes: z9.number().int().nonnegative()
|
|
1897
|
+
})
|
|
1898
|
+
]);
|
|
1899
|
+
var ProjectImportSchema = z9.object({
|
|
1900
|
+
move: UlidSchema,
|
|
1901
|
+
repo: ProjectRepoSchema,
|
|
1902
|
+
pullRequests: z9.boolean(),
|
|
1903
|
+
/** The packs' size the old runner measured, which the clone's clock is sized from; the cap if unsaid. */
|
|
1904
|
+
bytes: z9.number().int().nonnegative().optional()
|
|
1905
|
+
});
|
|
1906
|
+
var ProjectImportedSchema = z9.object({ refs: RepoRefsSchema });
|
|
1907
|
+
var ProjectReleaseSchema = z9.object({ slug: NameSchema });
|
|
1908
|
+
var ProjectReleasedSchema = z9.object({
|
|
1909
|
+
worktrees: z9.number().int().nonnegative(),
|
|
1910
|
+
clone: z9.boolean()
|
|
1911
|
+
});
|
|
1912
|
+
var ProjectMovedSchema = z9.object({
|
|
1913
|
+
project: ProjectSchema,
|
|
1914
|
+
carried: z9.number().int().nonnegative().nullable(),
|
|
1915
|
+
released: z9.object({ ok: z9.boolean(), detail: z9.string() }).nullable()
|
|
1916
|
+
});
|
|
1779
1917
|
var TurnSteerSchema = z9.object({ id: z9.uuid(), text: z9.string().min(1) });
|
|
1780
1918
|
var RunnerMessageSchema = z9.discriminatedUnion("type", [
|
|
1781
1919
|
z9.object({ type: z9.literal("turn"), job: TurnJobSchema }),
|
|
@@ -1803,6 +1941,15 @@ var RunnerMessageSchema = z9.discriminatedUnion("type", [
|
|
|
1803
1941
|
* whose hello says `imageReads`.
|
|
1804
1942
|
*/
|
|
1805
1943
|
z9.object({ type: z9.literal("image"), request: z9.string().min(1), read: TurnImageReadSchema }),
|
|
1944
|
+
/**
|
|
1945
|
+
* Answered with a `TurnImageFile`, or null when the turn is not running there. Sent only to
|
|
1946
|
+
* runners whose hello says `imageWrites`.
|
|
1947
|
+
*/
|
|
1948
|
+
z9.object({
|
|
1949
|
+
type: z9.literal("image_write"),
|
|
1950
|
+
request: z9.string().min(1),
|
|
1951
|
+
write: TurnImageWriteSchema
|
|
1952
|
+
}),
|
|
1806
1953
|
/** Answered with whether the CLI accepted it; that it took it comes as a `steered` event. */
|
|
1807
1954
|
z9.object({
|
|
1808
1955
|
type: z9.literal("steer"),
|
|
@@ -1812,6 +1959,19 @@ var RunnerMessageSchema = z9.discriminatedUnion("type", [
|
|
|
1812
1959
|
}),
|
|
1813
1960
|
/** Answered with whether the turn was there to stop; it ends `stopped`. */
|
|
1814
1961
|
z9.object({ type: z9.literal("stop"), request: z9.string().min(1), turnId: UlidSchema }),
|
|
1962
|
+
/**
|
|
1963
|
+
* Answered with a `ProjectExported`. Sent only to runners whose hello says `projectMoves`, as are
|
|
1964
|
+
* the import and the release.
|
|
1965
|
+
*/
|
|
1966
|
+
z9.object({ type: z9.literal("export"), request: z9.string().min(1), export: ProjectExportSchema }),
|
|
1967
|
+
/** Answered with a `ProjectImported`. */
|
|
1968
|
+
z9.object({ type: z9.literal("import"), request: z9.string().min(1), import: ProjectImportSchema }),
|
|
1969
|
+
/** Answered with a `ProjectReleased`. */
|
|
1970
|
+
z9.object({
|
|
1971
|
+
type: z9.literal("release"),
|
|
1972
|
+
request: z9.string().min(1),
|
|
1973
|
+
release: ProjectReleaseSchema
|
|
1974
|
+
}),
|
|
1815
1975
|
/**
|
|
1816
1976
|
* A conversation ended: the runner removes every citizen's workspace for it but those a turn
|
|
1817
1977
|
* still uses, and answers with a `SweepResult` naming those it kept for holding work on no branch.
|
|
@@ -1855,6 +2015,7 @@ var TriggerKindSchema = z10.enum([
|
|
|
1855
2015
|
"closing",
|
|
1856
2016
|
"cron"
|
|
1857
2017
|
]);
|
|
2018
|
+
var HeartbeatCauseSchema = z10.enum(["unread", "held", "waiting"]);
|
|
1858
2019
|
var TriggerSchema = z10.object({
|
|
1859
2020
|
kind: TriggerKindSchema,
|
|
1860
2021
|
from: NameSchema.optional(),
|
|
@@ -1865,7 +2026,9 @@ var TriggerSchema = z10.object({
|
|
|
1865
2026
|
/** The channel the waking message was posted in. */
|
|
1866
2027
|
channel: ChannelRefSchema.optional(),
|
|
1867
2028
|
/** The cron that came due, for a `cron` wake. */
|
|
1868
|
-
cronId: UlidSchema.optional()
|
|
2029
|
+
cronId: UlidSchema.optional(),
|
|
2030
|
+
/** What a `heartbeat` woke for; absent on records from before the board kept it. */
|
|
2031
|
+
heartbeat: HeartbeatCauseSchema.optional()
|
|
1869
2032
|
});
|
|
1870
2033
|
var TurnThreadSchema = z10.object({ id: UlidSchema, task: z10.boolean() });
|
|
1871
2034
|
var TurnDispatchSchema = z10.object({
|
|
@@ -1942,6 +2105,22 @@ var TurnRecordSchema = z10.object({
|
|
|
1942
2105
|
* resumed session starts from it, so the next turn's own cost is measured from here.
|
|
1943
2106
|
*/
|
|
1944
2107
|
sessionCostUsd: z10.number().nonnegative().optional(),
|
|
2108
|
+
/**
|
|
2109
|
+
* The running usage the CLI reported for the session after this turn, when the runner reports
|
|
2110
|
+
* one. A resumed session's next turn measures its own usage from here.
|
|
2111
|
+
*/
|
|
2112
|
+
sessionUsage: UsageSchema.optional(),
|
|
2113
|
+
/**
|
|
2114
|
+
* The SHA-256 of the instructions the session holds after this turn, the first turn's section
|
|
2115
|
+
* left out (`instructionsHash` in `turn-host`): those it began with, or the latest a prompt
|
|
2116
|
+
* carried. Absent when the board does not know them, as for records from before it kept them.
|
|
2117
|
+
*/
|
|
2118
|
+
sessionInstructionsHash: z10.string().optional(),
|
|
2119
|
+
/**
|
|
2120
|
+
* The whole input of the turn's latest model call in the session's own thread, as the runner
|
|
2121
|
+
* reported it: how large the session had grown. Absent for a turn that made no call.
|
|
2122
|
+
*/
|
|
2123
|
+
contextTokens: z10.number().int().nonnegative().optional(),
|
|
1945
2124
|
toolCalls: z10.number().int().nonnegative().default(0),
|
|
1946
2125
|
/** The model the CLI reported for this turn, else the model configured for the agent, else null. */
|
|
1947
2126
|
model: z10.string().nullable().default(null),
|
|
@@ -1955,10 +2134,19 @@ var TurnRecordSchema = z10.object({
|
|
|
1955
2134
|
transient: z10.boolean().optional(),
|
|
1956
2135
|
/** How many turns in a row, this one included, could not resume the session for a `retry`. */
|
|
1957
2136
|
resumeRetries: z10.number().int().positive().optional(),
|
|
1958
|
-
/**
|
|
2137
|
+
/**
|
|
2138
|
+
* Whether the server dropped the conversation's session after this turn, which the next starts
|
|
2139
|
+
* anew: one its CLI could not resume, or a society role's home after a reflection completed.
|
|
2140
|
+
*/
|
|
1959
2141
|
sessionDropped: z10.boolean().optional(),
|
|
1960
2142
|
/** The crons whose fire started the turn, which record how it ended. */
|
|
1961
|
-
crons: z10.array(UlidSchema).optional()
|
|
2143
|
+
crons: z10.array(UlidSchema).optional(),
|
|
2144
|
+
/**
|
|
2145
|
+
* The failed fires its prompt named: each of its citizen's crons in its conversation whose latest
|
|
2146
|
+
* fire's turn failed with nothing catching it up (`cronFailure`), but those it carries, with that
|
|
2147
|
+
* turn. Completing it catches each up.
|
|
2148
|
+
*/
|
|
2149
|
+
cronFailures: z10.array(z10.object({ cron: UlidSchema, turnId: UlidSchema })).optional()
|
|
1962
2150
|
});
|
|
1963
2151
|
var RunningTurnSchema = z10.object({
|
|
1964
2152
|
turnId: UlidSchema,
|
|
@@ -2000,6 +2188,8 @@ var TurnHistoryEntrySchema = z10.object({
|
|
|
2000
2188
|
/** The project whose channels' conversation the turn was in, for a turn in the society scope. */
|
|
2001
2189
|
place: NameSchema.optional(),
|
|
2002
2190
|
trigger: z10.string(),
|
|
2191
|
+
/** What a `heartbeat` woke for; absent on turns logged before the board kept it. */
|
|
2192
|
+
heartbeat: HeartbeatCauseSchema.optional(),
|
|
2003
2193
|
exitReason: z10.string().nullable(),
|
|
2004
2194
|
costUsd: z10.number(),
|
|
2005
2195
|
/** The tokens the turn used; absent for turns logged before turns recorded them. */
|
|
@@ -2073,7 +2263,7 @@ var METRICS_WINDOW_MS = {
|
|
|
2073
2263
|
"7d": 7 * 24 * 36e5,
|
|
2074
2264
|
all: null
|
|
2075
2265
|
};
|
|
2076
|
-
var READ_VERBS = ["read_inbox", "search", "get_task"];
|
|
2266
|
+
var READ_VERBS = ["read_inbox", "search", "get_task", "read_image"];
|
|
2077
2267
|
function isBoardAction(tool) {
|
|
2078
2268
|
const verb = /^mcp__board__(.+)$/.exec(tool)?.[1];
|
|
2079
2269
|
return verb !== void 0 && !READ_VERBS.some((read2) => read2 === verb);
|
|
@@ -2087,12 +2277,19 @@ var MetricsSchema = z12.object({
|
|
|
2087
2277
|
/**
|
|
2088
2278
|
* Finished turns that changed nothing on the board. Reflections are left out, since they write
|
|
2089
2279
|
* memory, not the board; turns without a transcript are counted apart, their actions unknown.
|
|
2280
|
+
* Heartbeats are split by what each woke for, and those recorded before the board kept it are a
|
|
2281
|
+
* row without one.
|
|
2090
2282
|
*/
|
|
2091
2283
|
idle: z12.object({
|
|
2092
2284
|
turns: CountSchema,
|
|
2093
2285
|
idle: CountSchema,
|
|
2094
2286
|
unknown: CountSchema,
|
|
2095
|
-
byTrigger: z12.array(z12.object({
|
|
2287
|
+
byTrigger: z12.array(z12.object({
|
|
2288
|
+
trigger: z12.string(),
|
|
2289
|
+
heartbeat: HeartbeatCauseSchema.optional(),
|
|
2290
|
+
turns: CountSchema,
|
|
2291
|
+
idle: CountSchema
|
|
2292
|
+
})),
|
|
2096
2293
|
byAgent: z12.array(z12.object({
|
|
2097
2294
|
agent: NameSchema,
|
|
2098
2295
|
role: NameSchema.nullable(),
|
|
@@ -2171,11 +2368,17 @@ var TokenUseSchema = z12.object({
|
|
|
2171
2368
|
// packages/shared/dist/config.js
|
|
2172
2369
|
import { z as z13 } from "zod";
|
|
2173
2370
|
var LogLevelSchema = z13.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
|
|
2371
|
+
var AppleAppIdSchema = z13.string().regex(/^[A-Z0-9]{10}\.[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*$/, "not <team id>.<bundle id>");
|
|
2174
2372
|
var GithubSignInSchema = z13.object({
|
|
2175
2373
|
clientId: z13.string().min(1),
|
|
2176
2374
|
clientSecret: z13.string().min(1),
|
|
2177
2375
|
/** Logins, lowercased; GitHub logins are case-insensitive. */
|
|
2178
|
-
users: z13.array(z13.string().min(1)).min(1)
|
|
2376
|
+
users: z13.array(z13.string().min(1)).min(1),
|
|
2377
|
+
/**
|
|
2378
|
+
* The iOS app that may sign in, which Apple's association file names so that only it can take the
|
|
2379
|
+
* board's `https` landing; null when no app signs in.
|
|
2380
|
+
*/
|
|
2381
|
+
appleAppId: AppleAppIdSchema.nullable()
|
|
2179
2382
|
});
|
|
2180
2383
|
var ServerConfigSchema = z13.object({
|
|
2181
2384
|
dataDir: z13.string().min(1),
|
|
@@ -2216,13 +2419,20 @@ function githubSignIn(env) {
|
|
|
2216
2419
|
const clientId = env["STELLARIS_GITHUB_CLIENT_ID"] ?? "";
|
|
2217
2420
|
const clientSecret = env["STELLARIS_GITHUB_CLIENT_SECRET"] ?? "";
|
|
2218
2421
|
const users = list(env["STELLARIS_GITHUB_USERS"]).map((login) => login.toLowerCase());
|
|
2422
|
+
const appleAppId = env["STELLARIS_APPLE_APP_ID"]?.trim() || null;
|
|
2423
|
+
if (appleAppId !== null && !AppleAppIdSchema.safeParse(appleAppId).success) {
|
|
2424
|
+
throw new Error(`STELLARIS_APPLE_APP_ID is <team id>.<bundle id>, not ${JSON.stringify(appleAppId)}`);
|
|
2425
|
+
}
|
|
2219
2426
|
if (clientId === "" && clientSecret === "" && users.length === 0) {
|
|
2427
|
+
if (appleAppId !== null) {
|
|
2428
|
+
throw new Error("STELLARIS_APPLE_APP_ID needs GitHub sign-in");
|
|
2429
|
+
}
|
|
2220
2430
|
return null;
|
|
2221
2431
|
}
|
|
2222
2432
|
if (clientId === "" || clientSecret === "" || users.length === 0) {
|
|
2223
2433
|
throw new Error("GitHub sign-in needs STELLARIS_GITHUB_CLIENT_ID, STELLARIS_GITHUB_CLIENT_SECRET, and STELLARIS_GITHUB_USERS together");
|
|
2224
2434
|
}
|
|
2225
|
-
return GithubSignInSchema.parse({ clientId, clientSecret, users });
|
|
2435
|
+
return GithubSignInSchema.parse({ clientId, clientSecret, users, appleAppId });
|
|
2226
2436
|
}
|
|
2227
2437
|
function loadServerConfig(env) {
|
|
2228
2438
|
return ServerConfigSchema.parse({
|
|
@@ -2257,7 +2467,7 @@ var RunnerConfigSchema = z13.object({
|
|
|
2257
2467
|
});
|
|
2258
2468
|
|
|
2259
2469
|
// packages/shared/dist/version.js
|
|
2260
|
-
var STELLARIS_VERSION = "0.
|
|
2470
|
+
var STELLARIS_VERSION = "0.4.0";
|
|
2261
2471
|
|
|
2262
2472
|
// packages/shared/dist/mentions.js
|
|
2263
2473
|
import { parse, postprocess, preprocess } from "micromark";
|
|
@@ -2952,6 +3162,41 @@ function filesOf(lines, status) {
|
|
|
2952
3162
|
return [...files.values()];
|
|
2953
3163
|
}
|
|
2954
3164
|
|
|
3165
|
+
// packages/board-core/dist/moves.js
|
|
3166
|
+
import { mkdir as mkdir4, rm as rm2 } from "node:fs/promises";
|
|
3167
|
+
import path5 from "node:path";
|
|
3168
|
+
import { execa as execa2 } from "execa";
|
|
3169
|
+
var MoveRepos = class {
|
|
3170
|
+
root;
|
|
3171
|
+
constructor(root) {
|
|
3172
|
+
this.root = root;
|
|
3173
|
+
}
|
|
3174
|
+
/** Makes the move's empty repository, which checks every object a push brings, and answers its path. */
|
|
3175
|
+
async open(dir) {
|
|
3176
|
+
await mkdir4(this.root, { recursive: true });
|
|
3177
|
+
await git2(["init", "--quiet", "--bare", dir], this.root);
|
|
3178
|
+
await git2(["config", "receive.fsckObjects", "true"], dir);
|
|
3179
|
+
return dir;
|
|
3180
|
+
}
|
|
3181
|
+
async drop(dir) {
|
|
3182
|
+
await rm2(path5.resolve(this.root, dir), { recursive: true, force: true });
|
|
3183
|
+
}
|
|
3184
|
+
/** Removes every move's repository, as the server starts: a move never survives a restart. */
|
|
3185
|
+
async clear() {
|
|
3186
|
+
await rm2(this.root, { recursive: true, force: true });
|
|
3187
|
+
}
|
|
3188
|
+
};
|
|
3189
|
+
async function git2(args, cwd) {
|
|
3190
|
+
const result = await execa2("git", args, {
|
|
3191
|
+
cwd,
|
|
3192
|
+
reject: false,
|
|
3193
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
3194
|
+
});
|
|
3195
|
+
if (result.exitCode !== 0) {
|
|
3196
|
+
throw new Error(`git ${args[0] ?? ""} failed: ${result.stderr.trim()}`);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
|
|
2955
3200
|
// packages/board-core/dist/mentions.js
|
|
2956
3201
|
import vm from "node:vm";
|
|
2957
3202
|
var MENTION_PARSE_MS = 500;
|
|
@@ -2986,7 +3231,8 @@ var TurnPayload = z14.object({
|
|
|
2986
3231
|
thread: z14.string().optional(),
|
|
2987
3232
|
channel: z14.string().optional(),
|
|
2988
3233
|
place: z14.string().optional(),
|
|
2989
|
-
trigger: z14.string()
|
|
3234
|
+
trigger: z14.string(),
|
|
3235
|
+
heartbeat: HeartbeatCauseSchema.optional().catch(void 0)
|
|
2990
3236
|
});
|
|
2991
3237
|
var TaskPayload = z14.object({
|
|
2992
3238
|
taskId: z14.string(),
|
|
@@ -3086,13 +3332,12 @@ function computeMetrics(input) {
|
|
|
3086
3332
|
const nothing = actions === 0 ? 1 : 0;
|
|
3087
3333
|
turns2 += 1;
|
|
3088
3334
|
idle += nothing;
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
}
|
|
3335
|
+
const { trigger, heartbeat } = turn.data;
|
|
3336
|
+
const key = heartbeat === void 0 ? trigger : `${trigger}\0${heartbeat}`;
|
|
3337
|
+
const row = turnRows.get(key) ?? { trigger, heartbeat, turns: 0, idle: 0 };
|
|
3338
|
+
turnRows.set(key, { ...row, turns: row.turns + 1, idle: row.idle + nothing });
|
|
3339
|
+
const own = agentRows.get(event.actor) ?? { turns: 0, idle: 0 };
|
|
3340
|
+
agentRows.set(event.actor, { turns: own.turns + 1, idle: own.idle + nothing });
|
|
3096
3341
|
break;
|
|
3097
3342
|
}
|
|
3098
3343
|
case "turn.started": {
|
|
@@ -3248,7 +3493,10 @@ function computeMetrics(input) {
|
|
|
3248
3493
|
turns: turns2,
|
|
3249
3494
|
idle,
|
|
3250
3495
|
unknown,
|
|
3251
|
-
byTrigger: [...turnRows].map((
|
|
3496
|
+
byTrigger: [...turnRows.values()].map(({ heartbeat, ...row }) => ({
|
|
3497
|
+
...row,
|
|
3498
|
+
...heartbeat === void 0 ? {} : { heartbeat }
|
|
3499
|
+
})).toSorted((a, b) => b.idle - a.idle || b.turns - a.turns),
|
|
3252
3500
|
byAgent: [...agentRows].map(([agent, row]) => ({ agent, role: citizens.get(agent)?.role ?? null, ...row })).toSorted((a, b) => b.idle - a.idle || b.turns - a.turns)
|
|
3253
3501
|
},
|
|
3254
3502
|
sentBack: {
|
|
@@ -3308,6 +3556,8 @@ var TurnHistoryPayloadSchema = z15.object({
|
|
|
3308
3556
|
channel: NameSchema.optional(),
|
|
3309
3557
|
place: NameSchema.optional(),
|
|
3310
3558
|
trigger: z15.string().default("manual"),
|
|
3559
|
+
// One a later board added reads as none, so the turn stays in the history.
|
|
3560
|
+
heartbeat: HeartbeatCauseSchema.optional().catch(void 0),
|
|
3311
3561
|
exitReason: z15.string().nullable().default(null),
|
|
3312
3562
|
costUsd: z15.number().default(0),
|
|
3313
3563
|
usage: UsageSchema.optional(),
|
|
@@ -3365,121 +3615,121 @@ var Mutex = class {
|
|
|
3365
3615
|
};
|
|
3366
3616
|
|
|
3367
3617
|
// packages/board-core/dist/paths.js
|
|
3368
|
-
import
|
|
3618
|
+
import path6 from "node:path";
|
|
3369
3619
|
var BoardPaths = class {
|
|
3370
3620
|
dataDir;
|
|
3371
3621
|
constructor(dataDir) {
|
|
3372
|
-
this.dataDir =
|
|
3622
|
+
this.dataDir = path6.resolve(dataDir);
|
|
3373
3623
|
}
|
|
3374
3624
|
// Board projection, also the storage of record; only the core library writes it.
|
|
3375
3625
|
get board() {
|
|
3376
|
-
return
|
|
3626
|
+
return path6.join(this.dataDir, "board");
|
|
3377
3627
|
}
|
|
3378
3628
|
get society() {
|
|
3379
|
-
return
|
|
3629
|
+
return path6.join(this.board, "society");
|
|
3380
3630
|
}
|
|
3381
3631
|
societyFile() {
|
|
3382
|
-
return
|
|
3632
|
+
return path6.join(this.society, "society.md");
|
|
3383
3633
|
}
|
|
3384
3634
|
societyChannels() {
|
|
3385
|
-
return
|
|
3635
|
+
return path6.join(this.society, "channels");
|
|
3386
3636
|
}
|
|
3387
3637
|
societyChannel(name) {
|
|
3388
|
-
return
|
|
3638
|
+
return path6.join(this.societyChannels(), name);
|
|
3389
3639
|
}
|
|
3390
3640
|
/** Threads whose channel is a society channel; project channels' threads live under the project. */
|
|
3391
3641
|
societyThreads() {
|
|
3392
|
-
return
|
|
3642
|
+
return path6.join(this.society, "threads");
|
|
3393
3643
|
}
|
|
3394
3644
|
societyKnowledge() {
|
|
3395
|
-
return
|
|
3645
|
+
return path6.join(this.society, "knowledge");
|
|
3396
3646
|
}
|
|
3397
3647
|
societyKnowledgeFile(topic) {
|
|
3398
|
-
return
|
|
3648
|
+
return path6.join(this.societyKnowledge(), `${topic}.md`);
|
|
3399
3649
|
}
|
|
3400
3650
|
/** Skills promoted to the society, one directory per skill holding SKILL.md. */
|
|
3401
3651
|
societySkills() {
|
|
3402
|
-
return
|
|
3652
|
+
return path6.join(this.society, "skills");
|
|
3403
3653
|
}
|
|
3404
3654
|
societySkill(name) {
|
|
3405
|
-
return
|
|
3655
|
+
return path6.join(this.societySkills(), name);
|
|
3406
3656
|
}
|
|
3407
3657
|
societySkillFile(name) {
|
|
3408
|
-
return
|
|
3658
|
+
return path6.join(this.societySkill(name), "SKILL.md");
|
|
3409
3659
|
}
|
|
3410
3660
|
roles() {
|
|
3411
|
-
return
|
|
3661
|
+
return path6.join(this.society, "roles");
|
|
3412
3662
|
}
|
|
3413
3663
|
role(name) {
|
|
3414
|
-
return
|
|
3664
|
+
return path6.join(this.roles(), `${name}.md`);
|
|
3415
3665
|
}
|
|
3416
3666
|
proposals() {
|
|
3417
|
-
return
|
|
3667
|
+
return path6.join(this.society, "proposals");
|
|
3418
3668
|
}
|
|
3419
3669
|
proposal(id) {
|
|
3420
|
-
return
|
|
3670
|
+
return path6.join(this.proposals(), `${id}.md`);
|
|
3421
3671
|
}
|
|
3422
3672
|
/** A skill proposal's folder, as the board took it from its proposer's home. */
|
|
3423
3673
|
proposalFiles(id) {
|
|
3424
|
-
return
|
|
3674
|
+
return path6.join(this.proposals(), id);
|
|
3425
3675
|
}
|
|
3426
3676
|
decisions() {
|
|
3427
|
-
return
|
|
3677
|
+
return path6.join(this.society, "decisions");
|
|
3428
3678
|
}
|
|
3429
3679
|
decision(id) {
|
|
3430
|
-
return
|
|
3680
|
+
return path6.join(this.decisions(), `${id}.md`);
|
|
3431
3681
|
}
|
|
3432
3682
|
runners() {
|
|
3433
|
-
return
|
|
3683
|
+
return path6.join(this.society, "runners");
|
|
3434
3684
|
}
|
|
3435
3685
|
runner(name) {
|
|
3436
|
-
return
|
|
3686
|
+
return path6.join(this.runners(), `${name}.md`);
|
|
3437
3687
|
}
|
|
3438
3688
|
members() {
|
|
3439
|
-
return
|
|
3689
|
+
return path6.join(this.society, "members");
|
|
3440
3690
|
}
|
|
3441
3691
|
/** Crons, one file each with its note as the body; ended ones stay. */
|
|
3442
3692
|
crons() {
|
|
3443
|
-
return
|
|
3693
|
+
return path6.join(this.society, "crons");
|
|
3444
3694
|
}
|
|
3445
3695
|
cron(id) {
|
|
3446
|
-
return
|
|
3696
|
+
return path6.join(this.crons(), `${id}.md`);
|
|
3447
3697
|
}
|
|
3448
3698
|
member(name) {
|
|
3449
|
-
return
|
|
3699
|
+
return path6.join(this.members(), `${name}.md`);
|
|
3450
3700
|
}
|
|
3451
3701
|
projects() {
|
|
3452
|
-
return
|
|
3702
|
+
return path6.join(this.board, "projects");
|
|
3453
3703
|
}
|
|
3454
3704
|
project(slug) {
|
|
3455
|
-
return
|
|
3705
|
+
return path6.join(this.projects(), slug);
|
|
3456
3706
|
}
|
|
3457
3707
|
projectFile(slug) {
|
|
3458
|
-
return
|
|
3708
|
+
return path6.join(this.project(slug), "project.md");
|
|
3459
3709
|
}
|
|
3460
3710
|
projectChannels(slug) {
|
|
3461
|
-
return
|
|
3711
|
+
return path6.join(this.project(slug), "channels");
|
|
3462
3712
|
}
|
|
3463
3713
|
projectChannel(slug, name) {
|
|
3464
|
-
return
|
|
3714
|
+
return path6.join(this.projectChannels(slug), name);
|
|
3465
3715
|
}
|
|
3466
3716
|
threads(slug) {
|
|
3467
|
-
return
|
|
3717
|
+
return path6.join(this.project(slug), "threads");
|
|
3468
3718
|
}
|
|
3469
3719
|
tasks(slug) {
|
|
3470
|
-
return
|
|
3720
|
+
return path6.join(this.project(slug), "tasks");
|
|
3471
3721
|
}
|
|
3472
3722
|
task(slug, id) {
|
|
3473
|
-
return
|
|
3723
|
+
return path6.join(this.tasks(slug), `${id}.md`);
|
|
3474
3724
|
}
|
|
3475
3725
|
projectKnowledge(slug) {
|
|
3476
|
-
return
|
|
3726
|
+
return path6.join(this.project(slug), "knowledge");
|
|
3477
3727
|
}
|
|
3478
3728
|
projectKnowledgeFile(slug, topic) {
|
|
3479
|
-
return
|
|
3729
|
+
return path6.join(this.projectKnowledge(slug), `${topic}.md`);
|
|
3480
3730
|
}
|
|
3481
3731
|
dashboard(slug) {
|
|
3482
|
-
return
|
|
3732
|
+
return path6.join(this.project(slug), "dashboard.md");
|
|
3483
3733
|
}
|
|
3484
3734
|
channelDir(ref) {
|
|
3485
3735
|
const parsed = parseChannelRef(ref);
|
|
@@ -3492,57 +3742,57 @@ var BoardPaths = class {
|
|
|
3492
3742
|
}
|
|
3493
3743
|
/** A thread's record, `<id>.md`, beside the directory of its messages, `<id>/`. */
|
|
3494
3744
|
threadFile(threads, id) {
|
|
3495
|
-
return
|
|
3745
|
+
return path6.join(threads, `${id}.md`);
|
|
3496
3746
|
}
|
|
3497
3747
|
threadMessages(threads, id) {
|
|
3498
|
-
return
|
|
3748
|
+
return path6.join(threads, id);
|
|
3499
3749
|
}
|
|
3500
3750
|
messageFile(dir, id, author) {
|
|
3501
|
-
return
|
|
3751
|
+
return path6.join(dir, `${id}-${author}.md`);
|
|
3502
3752
|
}
|
|
3503
3753
|
// Agent homes: authored by the agent, source of truth on the board server.
|
|
3504
3754
|
agents() {
|
|
3505
|
-
return
|
|
3755
|
+
return path6.join(this.dataDir, "agents");
|
|
3506
3756
|
}
|
|
3507
3757
|
agent(name) {
|
|
3508
|
-
return
|
|
3758
|
+
return path6.join(this.agents(), name);
|
|
3509
3759
|
}
|
|
3510
3760
|
agentFile(name) {
|
|
3511
|
-
return
|
|
3761
|
+
return path6.join(this.agent(name), "agent.json");
|
|
3512
3762
|
}
|
|
3513
3763
|
agentRole(name) {
|
|
3514
|
-
return
|
|
3764
|
+
return path6.join(this.agent(name), "role.md");
|
|
3515
3765
|
}
|
|
3516
3766
|
agentProfile(name) {
|
|
3517
|
-
return
|
|
3767
|
+
return path6.join(this.agent(name), "profile.md");
|
|
3518
3768
|
}
|
|
3519
3769
|
agentMemory(name) {
|
|
3520
|
-
return
|
|
3770
|
+
return path6.join(this.agent(name), "memory");
|
|
3521
3771
|
}
|
|
3522
3772
|
agentMemoryCore(name) {
|
|
3523
|
-
return
|
|
3773
|
+
return path6.join(this.agentMemory(name), "core.md");
|
|
3524
3774
|
}
|
|
3525
3775
|
/** Transcripts of the agent's finished turns, one JSON line per step. */
|
|
3526
3776
|
agentTurns(name) {
|
|
3527
|
-
return
|
|
3777
|
+
return path6.join(this.agent(name), "turns");
|
|
3528
3778
|
}
|
|
3529
3779
|
agentTranscript(name, turnId) {
|
|
3530
|
-
return
|
|
3780
|
+
return path6.join(this.agentTurns(name), `${turnId}.jsonl`);
|
|
3531
3781
|
}
|
|
3532
3782
|
agentSkills(name) {
|
|
3533
|
-
return
|
|
3783
|
+
return path6.join(this.agent(name), "skills");
|
|
3534
3784
|
}
|
|
3535
3785
|
agentSkill(name, skill) {
|
|
3536
|
-
return
|
|
3786
|
+
return path6.join(this.agentSkills(name), skill);
|
|
3537
3787
|
}
|
|
3538
3788
|
agentSkillFile(name, skill) {
|
|
3539
|
-
return
|
|
3789
|
+
return path6.join(this.agentSkill(name, skill), "SKILL.md");
|
|
3540
3790
|
}
|
|
3541
3791
|
agentProjects(name) {
|
|
3542
|
-
return
|
|
3792
|
+
return path6.join(this.agent(name), "projects");
|
|
3543
3793
|
}
|
|
3544
3794
|
agentProject(name, slug) {
|
|
3545
|
-
return
|
|
3795
|
+
return path6.join(this.agentProjects(name), slug);
|
|
3546
3796
|
}
|
|
3547
3797
|
/**
|
|
3548
3798
|
* A thread's, a channel's, or, in the society scope, a project's conversation's session and last
|
|
@@ -3550,46 +3800,56 @@ var BoardPaths = class {
|
|
|
3550
3800
|
*/
|
|
3551
3801
|
agentConversation(name, slug, part) {
|
|
3552
3802
|
if (part.startsWith("#")) {
|
|
3553
|
-
return
|
|
3803
|
+
return path6.join(this.agentProject(name, slug), "channels", part.slice(1));
|
|
3554
3804
|
}
|
|
3555
|
-
return part.startsWith("~") ?
|
|
3805
|
+
return part.startsWith("~") ? path6.join(this.agentProject(name, slug), "places", part.slice(1)) : path6.join(this.agentProject(name, slug), "threads", part);
|
|
3556
3806
|
}
|
|
3557
3807
|
agentCursors(name) {
|
|
3558
|
-
return
|
|
3808
|
+
return path6.join(this.agent(name), "cursors.json");
|
|
3559
3809
|
}
|
|
3560
3810
|
events() {
|
|
3561
|
-
return
|
|
3811
|
+
return path6.join(this.dataDir, "events");
|
|
3562
3812
|
}
|
|
3563
3813
|
eventLog() {
|
|
3564
|
-
return
|
|
3814
|
+
return path6.join(this.events(), "log.jsonl");
|
|
3565
3815
|
}
|
|
3566
3816
|
/** Images citizens uploaded, by id: outside the projection, which every runner mirrors. */
|
|
3567
3817
|
images() {
|
|
3568
|
-
return
|
|
3818
|
+
return path6.join(this.dataDir, "images");
|
|
3569
3819
|
}
|
|
3570
3820
|
image(id) {
|
|
3571
|
-
return
|
|
3821
|
+
return path6.join(this.images(), id);
|
|
3822
|
+
}
|
|
3823
|
+
/**
|
|
3824
|
+
* The repositories projects travel through between runners, one per move while it runs:
|
|
3825
|
+
* outside the projection, which every runner mirrors, and served only to the move's runners.
|
|
3826
|
+
*/
|
|
3827
|
+
moves() {
|
|
3828
|
+
return path6.join(this.dataDir, "moves");
|
|
3829
|
+
}
|
|
3830
|
+
move(id) {
|
|
3831
|
+
return path6.join(this.moves(), `${id}.git`);
|
|
3572
3832
|
}
|
|
3573
3833
|
state() {
|
|
3574
|
-
return
|
|
3834
|
+
return path6.join(this.dataDir, "state");
|
|
3575
3835
|
}
|
|
3576
3836
|
pausedFile() {
|
|
3577
|
-
return
|
|
3837
|
+
return path6.join(this.state(), "paused.json");
|
|
3578
3838
|
}
|
|
3579
3839
|
/** Removed knowledge topics, set aside outside the projection, by scope. */
|
|
3580
3840
|
removedKnowledge(scope) {
|
|
3581
|
-
return
|
|
3841
|
+
return path6.join(this.state(), "removed-knowledge", scope);
|
|
3582
3842
|
}
|
|
3583
3843
|
/** Skills removed from the society, set aside outside the projection, one directory per removal. */
|
|
3584
3844
|
removedSkills() {
|
|
3585
|
-
return
|
|
3845
|
+
return path6.join(this.state(), "removed-skills");
|
|
3586
3846
|
}
|
|
3587
3847
|
};
|
|
3588
3848
|
|
|
3589
3849
|
// packages/board-core/dist/skill-folder.js
|
|
3590
3850
|
import { constants } from "node:fs";
|
|
3591
3851
|
import { lstat, open as open2, readdir as readdir3, readlink, realpath } from "node:fs/promises";
|
|
3592
|
-
import
|
|
3852
|
+
import path7 from "node:path";
|
|
3593
3853
|
var SKILL_TEXT_LIMIT_CHARS = 1e5;
|
|
3594
3854
|
var SKILL_TEXT_LIMIT_BYTES = SKILL_TEXT_LIMIT_CHARS * 4;
|
|
3595
3855
|
function skillFileText(bytes) {
|
|
@@ -3600,8 +3860,8 @@ function skillFileText(bytes) {
|
|
|
3600
3860
|
};
|
|
3601
3861
|
}
|
|
3602
3862
|
function inside(root, file) {
|
|
3603
|
-
const relative =
|
|
3604
|
-
return relative !== ".." && !relative.startsWith(`..${
|
|
3863
|
+
const relative = path7.relative(root, file);
|
|
3864
|
+
return relative !== ".." && !relative.startsWith(`..${path7.sep}`) && !path7.isAbsolute(relative);
|
|
3605
3865
|
}
|
|
3606
3866
|
async function handleInside(root, handle) {
|
|
3607
3867
|
if (process.platform !== "linux") {
|
|
@@ -3611,7 +3871,7 @@ async function handleInside(root, handle) {
|
|
|
3611
3871
|
return file !== null && inside(root, file);
|
|
3612
3872
|
}
|
|
3613
3873
|
async function walk(root, prefix, found) {
|
|
3614
|
-
const entries = await readdir3(
|
|
3874
|
+
const entries = await readdir3(path7.join(root, ...prefix === "" ? [] : prefix.split("/")), {
|
|
3615
3875
|
withFileTypes: true
|
|
3616
3876
|
}).catch(() => []);
|
|
3617
3877
|
for (const entry of entries.toSorted((a, b) => a.name < b.name ? -1 : 1)) {
|
|
@@ -3632,7 +3892,7 @@ async function walk(root, prefix, found) {
|
|
|
3632
3892
|
async function readEntry(root, relative) {
|
|
3633
3893
|
let real;
|
|
3634
3894
|
try {
|
|
3635
|
-
real = await realpath(
|
|
3895
|
+
real = await realpath(path7.join(root, ...relative.split("/")));
|
|
3636
3896
|
} catch {
|
|
3637
3897
|
return null;
|
|
3638
3898
|
}
|
|
@@ -3723,7 +3983,10 @@ var SignInsSchema = z16.object({
|
|
|
3723
3983
|
expiresAt: IsoDateTimeSchema
|
|
3724
3984
|
})).default([])
|
|
3725
3985
|
});
|
|
3726
|
-
var TurnStartPayloadSchema = z16.object({
|
|
3986
|
+
var TurnStartPayloadSchema = z16.object({
|
|
3987
|
+
turnId: UlidSchema,
|
|
3988
|
+
heartbeat: TurnHistoryPayloadSchema.shape.heartbeat
|
|
3989
|
+
});
|
|
3727
3990
|
var CursorsSchema = z16.object({
|
|
3728
3991
|
digest: z16.string().nullable(),
|
|
3729
3992
|
scopes: z16.record(z16.string(), z16.string()).default({}),
|
|
@@ -3795,6 +4058,9 @@ function skillFolderOf(proposal) {
|
|
|
3795
4058
|
function proposedFrom(origin, where) {
|
|
3796
4059
|
return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel && origin.place === where.place;
|
|
3797
4060
|
}
|
|
4061
|
+
function latestLease(a, b) {
|
|
4062
|
+
return a === null ? b : b === null || a >= b ? a : b;
|
|
4063
|
+
}
|
|
3798
4064
|
function megabytes(bytes) {
|
|
3799
4065
|
return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "");
|
|
3800
4066
|
}
|
|
@@ -3934,6 +4200,7 @@ var Board = class _Board {
|
|
|
3934
4200
|
/** The turns in flight, which the server hosting them supplies; none until it does. */
|
|
3935
4201
|
turnsInFlight = () => [];
|
|
3936
4202
|
imageSource = null;
|
|
4203
|
+
imageSink = null;
|
|
3937
4204
|
newId = monotonicFactory();
|
|
3938
4205
|
events;
|
|
3939
4206
|
actionCounts = /* @__PURE__ */ new Map();
|
|
@@ -3948,9 +4215,11 @@ var Board = class _Board {
|
|
|
3948
4215
|
signIns = /* @__PURE__ */ new Map();
|
|
3949
4216
|
files = new FileTree();
|
|
3950
4217
|
homes;
|
|
4218
|
+
moves;
|
|
3951
4219
|
constructor(dataDir, options) {
|
|
3952
4220
|
this.paths = new BoardPaths(dataDir);
|
|
3953
|
-
this.homes = new HomeRepos(
|
|
4221
|
+
this.homes = new HomeRepos(path8.join(this.paths.state(), "home-hooks"));
|
|
4222
|
+
this.moves = new MoveRepos(this.paths.moves());
|
|
3954
4223
|
this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
3955
4224
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3956
4225
|
this.events = new EventLog(this.paths.eventLog(), () => this.newId(), this.now);
|
|
@@ -3976,16 +4245,20 @@ var Board = class _Board {
|
|
|
3976
4245
|
await board2.loadRunnerTokens();
|
|
3977
4246
|
await board2.loadSignIns();
|
|
3978
4247
|
await board2.homes.installHooks();
|
|
4248
|
+
await board2.moves.clear();
|
|
3979
4249
|
return board2;
|
|
3980
4250
|
}
|
|
3981
4251
|
/**
|
|
3982
4252
|
* Applies each one-time alignment an existing society has not had yet, and remembers it, so a
|
|
3983
4253
|
* member that later chooses otherwise keeps its choice. A new alignment is a name and a method in
|
|
3984
|
-
* this list; once every board has applied it, both are deleted.
|
|
4254
|
+
* this list; once every board has applied it, both are deleted.
|
|
3985
4255
|
*/
|
|
3986
4256
|
async applyAlignments() {
|
|
3987
|
-
const alignments = [
|
|
3988
|
-
|
|
4257
|
+
const alignments = [
|
|
4258
|
+
// Whoever may show an image in a post may open one a post shows.
|
|
4259
|
+
{ name: "image-reads-granted", apply: () => this.grantBeside("upload_image", "read_image") }
|
|
4260
|
+
];
|
|
4261
|
+
const file = path8.join(this.paths.state(), "alignments.json");
|
|
3989
4262
|
const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
|
|
3990
4263
|
const pending = alignments.filter(({ name }) => !applied.includes(name));
|
|
3991
4264
|
if (pending.length === 0) {
|
|
@@ -3996,6 +4269,17 @@ var Board = class _Board {
|
|
|
3996
4269
|
}
|
|
3997
4270
|
await writeJson(file, { applied: [...applied, ...pending.map(({ name }) => name)] });
|
|
3998
4271
|
}
|
|
4272
|
+
/**
|
|
4273
|
+
* Grants `verb` once to every charter granting `beside`, the work roles' charters in practice;
|
|
4274
|
+
* seed roles have it from their seed.
|
|
4275
|
+
*/
|
|
4276
|
+
async grantBeside(beside, verb) {
|
|
4277
|
+
for (const charter of await this.listRoles()) {
|
|
4278
|
+
if (charter.verbs.includes(beside) && !charter.verbs.includes(verb)) {
|
|
4279
|
+
await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
3999
4283
|
/** Writes every missing seed charter and aligns the existing ones with their seed. */
|
|
4000
4284
|
async ensureSeedRoles() {
|
|
4001
4285
|
await ensureDir(this.paths.roles());
|
|
@@ -4193,7 +4477,7 @@ ${charter.purpose}
|
|
|
4193
4477
|
});
|
|
4194
4478
|
}
|
|
4195
4479
|
signInsFile() {
|
|
4196
|
-
return
|
|
4480
|
+
return path8.join(this.paths.state(), "sign-ins.json");
|
|
4197
4481
|
}
|
|
4198
4482
|
async readSignIns() {
|
|
4199
4483
|
const file = this.signInsFile();
|
|
@@ -4290,7 +4574,7 @@ ${charter.purpose}
|
|
|
4290
4574
|
const files = await listFiles(this.paths.tasks(project));
|
|
4291
4575
|
const tasks = [];
|
|
4292
4576
|
for (const file of files) {
|
|
4293
|
-
const doc = await readMarkdown(
|
|
4577
|
+
const doc = await readMarkdown(path8.join(this.paths.tasks(project), file), TaskFrontmatterSchema);
|
|
4294
4578
|
tasks.push({ ...doc.data, body: doc.body });
|
|
4295
4579
|
}
|
|
4296
4580
|
return tasks;
|
|
@@ -4309,7 +4593,7 @@ ${charter.purpose}
|
|
|
4309
4593
|
const files = await listFiles(this.paths.proposals());
|
|
4310
4594
|
const proposals = [];
|
|
4311
4595
|
for (const file of files) {
|
|
4312
|
-
const doc = await readMarkdown(
|
|
4596
|
+
const doc = await readMarkdown(path8.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
|
|
4313
4597
|
proposals.push({ ...doc.data, body: doc.body });
|
|
4314
4598
|
}
|
|
4315
4599
|
return proposals;
|
|
@@ -4336,7 +4620,7 @@ ${charter.purpose}
|
|
|
4336
4620
|
async listMembers() {
|
|
4337
4621
|
const members = [];
|
|
4338
4622
|
for (const file of await listFiles(this.paths.members())) {
|
|
4339
|
-
const doc = await readMarkdown(
|
|
4623
|
+
const doc = await readMarkdown(path8.join(this.paths.members(), file), MemberSchema);
|
|
4340
4624
|
members.push({ ...doc.data, profile: doc.body });
|
|
4341
4625
|
}
|
|
4342
4626
|
return members;
|
|
@@ -4355,7 +4639,7 @@ ${charter.purpose}
|
|
|
4355
4639
|
const dir = project === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project);
|
|
4356
4640
|
const topics = [];
|
|
4357
4641
|
for (const file of await listFiles(dir)) {
|
|
4358
|
-
topics.push(await this.knowledgeAt(
|
|
4642
|
+
topics.push(await this.knowledgeAt(path8.join(dir, file), project, file.replace(/\.md$/, "")));
|
|
4359
4643
|
}
|
|
4360
4644
|
return topics;
|
|
4361
4645
|
}
|
|
@@ -4385,7 +4669,7 @@ ${charter.purpose}
|
|
|
4385
4669
|
const aside = this.paths.removedKnowledge(project ?? SOCIETY_SCOPE);
|
|
4386
4670
|
let highest = 0;
|
|
4387
4671
|
for (const file of (await listFiles(aside)).filter((name) => name.startsWith(`${topic}.`))) {
|
|
4388
|
-
const copy = await this.knowledgeAt(
|
|
4672
|
+
const copy = await this.knowledgeAt(path8.join(aside, file), project, topic);
|
|
4389
4673
|
highest = Math.max(highest, copy.revision);
|
|
4390
4674
|
}
|
|
4391
4675
|
return highest;
|
|
@@ -4401,7 +4685,7 @@ ${charter.purpose}
|
|
|
4401
4685
|
async listSkillsIn(dir, scope) {
|
|
4402
4686
|
const skills = [];
|
|
4403
4687
|
for (const entry of await listDirs(dir)) {
|
|
4404
|
-
const file =
|
|
4688
|
+
const file = path8.join(dir, entry, "SKILL.md");
|
|
4405
4689
|
if (!await exists(file)) {
|
|
4406
4690
|
continue;
|
|
4407
4691
|
}
|
|
@@ -4491,7 +4775,7 @@ ${charter.purpose}
|
|
|
4491
4775
|
if (!wanted.delete(file.slice(0, 26))) {
|
|
4492
4776
|
continue;
|
|
4493
4777
|
}
|
|
4494
|
-
const { data } = await readMarkdown(
|
|
4778
|
+
const { data } = await readMarkdown(path8.join(dir, file), MessageFrontmatterSchema);
|
|
4495
4779
|
found.push(MessageRefSchema.parse(data));
|
|
4496
4780
|
}
|
|
4497
4781
|
if (wanted.size === 0) {
|
|
@@ -4523,7 +4807,7 @@ ${charter.purpose}
|
|
|
4523
4807
|
if (!file.endsWith(".md")) {
|
|
4524
4808
|
continue;
|
|
4525
4809
|
}
|
|
4526
|
-
const doc = await readMarkdown(
|
|
4810
|
+
const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
|
|
4527
4811
|
if (doc.data.state !== "open") {
|
|
4528
4812
|
continue;
|
|
4529
4813
|
}
|
|
@@ -4550,7 +4834,7 @@ ${charter.purpose}
|
|
|
4550
4834
|
const all = [];
|
|
4551
4835
|
for (const threads of await this.threadScopes()) {
|
|
4552
4836
|
for (const file of await listFiles(threads)) {
|
|
4553
|
-
const doc = await readMarkdown(
|
|
4837
|
+
const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
|
|
4554
4838
|
const messages = await listFiles(this.paths.threadMessages(threads, doc.data.id));
|
|
4555
4839
|
const last = messages.at(-1);
|
|
4556
4840
|
all.push({
|
|
@@ -4763,6 +5047,46 @@ ${DASHBOARD_LINE}
|
|
|
4763
5047
|
return next;
|
|
4764
5048
|
});
|
|
4765
5049
|
}
|
|
5050
|
+
/**
|
|
5051
|
+
* Pauses or resumes a runner, the user's alone: the whole runner, or with `cli` that CLI on it
|
|
5052
|
+
* alone, each apart from the other. While either holds, no turn it covers starts there; their
|
|
5053
|
+
* wakes wait in the scheduler's queue, and a turn under way finishes. Pausing what is paused, or
|
|
5054
|
+
* resuming what is not, changes nothing and logs nothing.
|
|
5055
|
+
*/
|
|
5056
|
+
async setRunnerPaused(actor, name, change) {
|
|
5057
|
+
this.assertUser(actor, "only the user may pause or resume a runner");
|
|
5058
|
+
const cli = change.cli === void 0 ? void 0 : CliKindSchema.parse(change.cli);
|
|
5059
|
+
return this.mutex.run(async () => {
|
|
5060
|
+
const file = this.paths.runner(name);
|
|
5061
|
+
if (!await exists(file)) {
|
|
5062
|
+
throw new BoardError("NOT_FOUND", `runner ${name} not found`);
|
|
5063
|
+
}
|
|
5064
|
+
const doc = await readMarkdown(file, RunnerSchema);
|
|
5065
|
+
const { paused, pausedClis, ...rest } = doc.data;
|
|
5066
|
+
if (cli !== void 0 && change.paused && !doc.data.clis.includes(cli)) {
|
|
5067
|
+
throw new BoardError("INVALID_STATE", `runner ${name} has no ${cli} to pause`);
|
|
5068
|
+
}
|
|
5069
|
+
const current = cli === void 0 ? paused : pausedClis?.[cli];
|
|
5070
|
+
if (current !== void 0 === change.paused) {
|
|
5071
|
+
return doc.data;
|
|
5072
|
+
}
|
|
5073
|
+
const pause = change.paused ? { at: this.now().toISOString(), by: actor.name } : void 0;
|
|
5074
|
+
const clis = cli === void 0 ? { ...pausedClis } : { ...pausedClis, [cli]: pause };
|
|
5075
|
+
const kept = Object.entries(clis).filter(([, each]) => each !== void 0);
|
|
5076
|
+
const whole = cli === void 0 ? pause : paused;
|
|
5077
|
+
const next = RunnerSchema.parse({
|
|
5078
|
+
...rest,
|
|
5079
|
+
...whole === void 0 ? {} : { paused: whole },
|
|
5080
|
+
...kept.length === 0 ? {} : { pausedClis: Object.fromEntries(kept) }
|
|
5081
|
+
});
|
|
5082
|
+
await writeMarkdown(file, next, doc.body);
|
|
5083
|
+
await this.events.append(change.paused ? "runner.paused" : "runner.resumed", actor.name, {
|
|
5084
|
+
name,
|
|
5085
|
+
...cli === void 0 ? {} : { cli }
|
|
5086
|
+
});
|
|
5087
|
+
return next;
|
|
5088
|
+
});
|
|
5089
|
+
}
|
|
4766
5090
|
/**
|
|
4767
5091
|
* Logs an operations signal as an `ops.signal` event. No channel carries it: the scheduler wakes
|
|
4768
5092
|
* its readers from the event, the runner lists it in their prompts, and the interface's log shows
|
|
@@ -4797,8 +5121,9 @@ ${DASHBOARD_LINE}
|
|
|
4797
5121
|
}
|
|
4798
5122
|
if (event.type === "turn.started") {
|
|
4799
5123
|
const start = TurnStartPayloadSchema.safeParse(event.payload);
|
|
4800
|
-
if (start.success)
|
|
4801
|
-
starts.set(start.data.turnId, event.ts);
|
|
5124
|
+
if (start.success) {
|
|
5125
|
+
starts.set(start.data.turnId, { ts: event.ts, heartbeat: start.data.heartbeat });
|
|
5126
|
+
}
|
|
4802
5127
|
continue;
|
|
4803
5128
|
}
|
|
4804
5129
|
if (event.type !== "turn.completed" && event.type !== "turn.failed") {
|
|
@@ -4806,17 +5131,19 @@ ${DASHBOARD_LINE}
|
|
|
4806
5131
|
}
|
|
4807
5132
|
const parsed = TurnHistoryPayloadSchema.safeParse(event.payload);
|
|
4808
5133
|
if (parsed.success) {
|
|
4809
|
-
const {
|
|
4810
|
-
const
|
|
4811
|
-
if (turnId !== void 0) {
|
|
4812
|
-
starts.delete(turnId);
|
|
5134
|
+
const { heartbeat: ended, ...turn } = parsed.data;
|
|
5135
|
+
const start = turn.turnId === void 0 ? void 0 : starts.get(turn.turnId);
|
|
5136
|
+
if (turn.turnId !== void 0) {
|
|
5137
|
+
starts.delete(turn.turnId);
|
|
4813
5138
|
}
|
|
5139
|
+
const heartbeat = ended ?? start?.heartbeat;
|
|
4814
5140
|
entries.push({
|
|
4815
5141
|
id: event.id,
|
|
4816
5142
|
ts: event.ts,
|
|
4817
5143
|
outcome: event.type === "turn.failed" ? "failed" : "completed",
|
|
4818
|
-
...
|
|
4819
|
-
...
|
|
5144
|
+
...turn,
|
|
5145
|
+
...heartbeat === void 0 ? {} : { heartbeat },
|
|
5146
|
+
...start === void 0 ? {} : { startedAt: start.ts }
|
|
4820
5147
|
});
|
|
4821
5148
|
}
|
|
4822
5149
|
}
|
|
@@ -4846,14 +5173,31 @@ ${DASHBOARD_LINE}
|
|
|
4846
5173
|
return society;
|
|
4847
5174
|
});
|
|
4848
5175
|
}
|
|
4849
|
-
/**
|
|
5176
|
+
/**
|
|
5177
|
+
* Releases every claim whose lease has expired, but for a holder's that its own cron in the
|
|
5178
|
+
* task's thread keeps (`keptLease`). Called by the scheduler on its tick.
|
|
5179
|
+
*/
|
|
4850
5180
|
async expireLeases() {
|
|
4851
5181
|
return this.mutex.run(async () => {
|
|
4852
5182
|
const expired = [];
|
|
4853
5183
|
const now = this.now();
|
|
5184
|
+
let crons;
|
|
5185
|
+
let runners2;
|
|
4854
5186
|
for (const project of await listDirs(this.paths.projects())) {
|
|
5187
|
+
let place;
|
|
4855
5188
|
for (const task of await this.listTasks(project)) {
|
|
4856
|
-
if (task.status
|
|
5189
|
+
if (task.status !== "claimed") {
|
|
5190
|
+
continue;
|
|
5191
|
+
}
|
|
5192
|
+
crons ??= await this.listCrons();
|
|
5193
|
+
runners2 ??= await this.listRunners();
|
|
5194
|
+
place ??= await this.readProject(project);
|
|
5195
|
+
const kept = latestLease(this.keptLease(task, crons, now), await this.pausedLease(task, runners2, place.runner, now));
|
|
5196
|
+
if (kept !== null) {
|
|
5197
|
+
await this.writeTask(project, { ...task, leaseExpiresAt: kept });
|
|
5198
|
+
continue;
|
|
5199
|
+
}
|
|
5200
|
+
if (this.leaseExpired(task, now)) {
|
|
4857
5201
|
const released = await this.writeTask(project, {
|
|
4858
5202
|
...task,
|
|
4859
5203
|
status: "open",
|
|
@@ -4922,20 +5266,24 @@ ${DASHBOARD_LINE}
|
|
|
4922
5266
|
* general channel, one of its other channels, one of its threads, or, in the society scope, a
|
|
4923
5267
|
* project's channels. The heartbeat asks, so that it wakes a member where its unread
|
|
4924
5268
|
* messages are and nowhere else. Society channels count only for a member woken in the society
|
|
4925
|
-
* scope; a project member reads them at its next turn.
|
|
5269
|
+
* scope; a project member reads them at its next turn. Some posts are in the digest but not
|
|
4926
5270
|
* counted, and are read when the member wakes for something else: a task step's, which wakes
|
|
4927
|
-
* whoever it hands work to
|
|
4928
|
-
*
|
|
4929
|
-
* nothing
|
|
4930
|
-
*
|
|
5271
|
+
* whoever it hands work to; the board's own, operations signals and landing announcements, whose
|
|
5272
|
+
* signals that call for action wake their readers directly; and a knowledge topic's notice, which
|
|
5273
|
+
* asks nothing of anyone. In a task's thread a post counts only for whoever the current stage is
|
|
5274
|
+
* for, its holder or, while nobody holds it, a member who may take it, and for the task's creator
|
|
5275
|
+
* when it mentions the user, so the creator can put a wait on the user in; a post that asks the
|
|
5276
|
+
* reader counts wherever it is. A closed thread counts for nothing: its conversation has ended,
|
|
5277
|
+
* and a turn there could only read its posts and be refused a reply, while a wake queued before
|
|
5278
|
+
* it closed, as for a mention, still runs there.
|
|
4931
5279
|
*/
|
|
4932
5280
|
async unreadByConversation(actor) {
|
|
4933
5281
|
const { cursors, upto } = await this.digestSnapshot(actor.name);
|
|
4934
5282
|
const counts = /* @__PURE__ */ new Map();
|
|
4935
|
-
const
|
|
5283
|
+
const threadCounts = /* @__PURE__ */ new Map();
|
|
4936
5284
|
const unscoped = { name: actor.name, role: actor.role };
|
|
4937
|
-
for (const { scope, part, message, news } of await this.digestSince(unscoped, cursors, upto)) {
|
|
4938
|
-
if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
|
|
5285
|
+
for (const { scope, part, message, news, asked } of await this.digestSince(unscoped, cursors, upto)) {
|
|
5286
|
+
if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name || message.notice?.kind === "knowledge") {
|
|
4939
5287
|
continue;
|
|
4940
5288
|
}
|
|
4941
5289
|
const posted = parseChannelRef(message.channel).project ?? SOCIETY_SCOPE;
|
|
@@ -4944,12 +5292,13 @@ ${DASHBOARD_LINE}
|
|
|
4944
5292
|
}
|
|
4945
5293
|
const thread = threadOfPart(part);
|
|
4946
5294
|
if (thread !== void 0) {
|
|
4947
|
-
let
|
|
4948
|
-
if (
|
|
4949
|
-
|
|
4950
|
-
|
|
5295
|
+
let counted = threadCounts.get(thread);
|
|
5296
|
+
if (counted === void 0) {
|
|
5297
|
+
counted = await this.threadCount(actor, thread);
|
|
5298
|
+
threadCounts.set(thread, counted);
|
|
4951
5299
|
}
|
|
4952
|
-
|
|
5300
|
+
const forReader = counted === "every" || counted !== "none" && asked === true || counted === "asks-user" && message.mentions.includes(USER_NAME);
|
|
5301
|
+
if (!forReader) {
|
|
4953
5302
|
continue;
|
|
4954
5303
|
}
|
|
4955
5304
|
}
|
|
@@ -5102,7 +5451,7 @@ ${DASHBOARD_LINE}
|
|
|
5102
5451
|
include = participates;
|
|
5103
5452
|
}
|
|
5104
5453
|
if (include && message.author !== actor.name) {
|
|
5105
|
-
collected.push({ scope: filedIn, part, message });
|
|
5454
|
+
collected.push({ scope: filedIn, part, message, ...asked ? { asked: true } : {} });
|
|
5106
5455
|
}
|
|
5107
5456
|
}
|
|
5108
5457
|
return collected.toSorted(byId);
|
|
@@ -5145,7 +5494,7 @@ ${task.body}`;
|
|
|
5145
5494
|
}
|
|
5146
5495
|
}
|
|
5147
5496
|
for (const file of await listFiles(this.paths.projectKnowledge(project))) {
|
|
5148
|
-
const content = await readFile4(
|
|
5497
|
+
const content = await readFile4(path8.join(this.paths.projectKnowledge(project), file), "utf8");
|
|
5149
5498
|
if (content.toLowerCase().includes(needle)) {
|
|
5150
5499
|
hits.push({
|
|
5151
5500
|
kind: "knowledge",
|
|
@@ -5166,7 +5515,7 @@ ${task.body}`;
|
|
|
5166
5515
|
shared.push({
|
|
5167
5516
|
kind: "knowledge",
|
|
5168
5517
|
ref: `society/knowledge/${file}`,
|
|
5169
|
-
file:
|
|
5518
|
+
file: path8.join(this.paths.societyKnowledge(), file)
|
|
5170
5519
|
});
|
|
5171
5520
|
}
|
|
5172
5521
|
for (const skill of await this.listSocietySkills()) {
|
|
@@ -5179,7 +5528,7 @@ ${task.body}`;
|
|
|
5179
5528
|
shared.push({
|
|
5180
5529
|
kind: "memory",
|
|
5181
5530
|
ref: `memory/${file}`,
|
|
5182
|
-
file:
|
|
5531
|
+
file: path8.join(this.paths.agentMemory(actor.name), file)
|
|
5183
5532
|
});
|
|
5184
5533
|
}
|
|
5185
5534
|
for (const entry of shared) {
|
|
@@ -5268,8 +5617,8 @@ ${current.body}`);
|
|
|
5268
5617
|
removedAt: this.now().toISOString()
|
|
5269
5618
|
});
|
|
5270
5619
|
const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
|
|
5271
|
-
await
|
|
5272
|
-
await rename3(file,
|
|
5620
|
+
await mkdir5(aside, { recursive: true });
|
|
5621
|
+
await rename3(file, path8.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
|
|
5273
5622
|
await this.events.append("knowledge.removed", actor.name, {
|
|
5274
5623
|
topic: args.topic,
|
|
5275
5624
|
project: args.project
|
|
@@ -5316,8 +5665,8 @@ ${current.body}`);
|
|
|
5316
5665
|
removedAt: this.now().toISOString()
|
|
5317
5666
|
});
|
|
5318
5667
|
const aside = this.paths.removedSkills();
|
|
5319
|
-
await
|
|
5320
|
-
await rename3(this.paths.societySkill(args.name),
|
|
5668
|
+
await mkdir5(aside, { recursive: true });
|
|
5669
|
+
await rename3(this.paths.societySkill(args.name), path8.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
|
|
5321
5670
|
await this.events.append("skill.removed", actor.name, {
|
|
5322
5671
|
name: args.name,
|
|
5323
5672
|
reason: args.reason
|
|
@@ -5379,12 +5728,30 @@ ${current.body}`);
|
|
|
5379
5728
|
throw new BoardError("NOT_FOUND", "this board has no runners to read an image from");
|
|
5380
5729
|
}
|
|
5381
5730
|
const bytes = await this.imageSource(actor, args.path);
|
|
5731
|
+
return this.keepImage(actor, bytes, args.path, path8.basename(args.path));
|
|
5732
|
+
}
|
|
5733
|
+
/**
|
|
5734
|
+
* Keeps an image the user posts from a composer, as its bytes, under the same checks as an image
|
|
5735
|
+
* a turn uploads. `name`, the file's, describes it in the markdown answered; without one the
|
|
5736
|
+
* description is empty, for the user to write.
|
|
5737
|
+
*/
|
|
5738
|
+
async postImage(actor, bytes, name) {
|
|
5739
|
+
this.assertUser(actor, "only the user posts an image's bytes; a turn uploads one with upload_image");
|
|
5740
|
+
const given = name === void 0 ? "" : ImageNameSchema.parse(name);
|
|
5741
|
+
return this.keepImage(actor, bytes, given === "" ? "the image" : given, given);
|
|
5742
|
+
}
|
|
5743
|
+
/**
|
|
5744
|
+
* Keeps `bytes` under a new id when they are a PNG, JPEG, GIF, or WebP of at most
|
|
5745
|
+
* `IMAGE_LIMIT_BYTES`, and answers the markdown that shows them, described by `name`. `what`
|
|
5746
|
+
* names the image in a refusal.
|
|
5747
|
+
*/
|
|
5748
|
+
async keepImage(actor, bytes, what, name) {
|
|
5382
5749
|
const type = imageTypeOf(bytes);
|
|
5383
5750
|
if (type === null) {
|
|
5384
|
-
throw new BoardError("VALIDATION", `${
|
|
5751
|
+
throw new BoardError("VALIDATION", `${what} is not a PNG, JPEG, GIF, or WebP image by its first bytes`);
|
|
5385
5752
|
}
|
|
5386
5753
|
if (bytes.byteLength > IMAGE_LIMIT_BYTES) {
|
|
5387
|
-
throw new BoardError("VALIDATION", `${
|
|
5754
|
+
throw new BoardError("VALIDATION", `${what} is ${bytes.byteLength} bytes, over the ${IMAGE_LIMIT_BYTES} an image may be`);
|
|
5388
5755
|
}
|
|
5389
5756
|
return this.mutex.run(async () => {
|
|
5390
5757
|
const id = this.newId();
|
|
@@ -5394,15 +5761,37 @@ ${current.body}`);
|
|
|
5394
5761
|
type,
|
|
5395
5762
|
size: bytes.byteLength
|
|
5396
5763
|
});
|
|
5397
|
-
const
|
|
5764
|
+
const description = imageDescription(name);
|
|
5398
5765
|
return {
|
|
5399
5766
|
id,
|
|
5400
5767
|
type,
|
|
5401
5768
|
size: bytes.byteLength,
|
|
5402
|
-
markdown: ``
|
|
5403
5770
|
};
|
|
5404
5771
|
});
|
|
5405
5772
|
}
|
|
5773
|
+
/**
|
|
5774
|
+
* Opens an image the board keeps for the calling turn, by its id or its `image:<id>` address:
|
|
5775
|
+
* the runner the turn runs on writes the bytes to a file in a folder of the turn's own in the
|
|
5776
|
+
* system temp folder, removed when the turn ends, and the board answers its path, for the
|
|
5777
|
+
* citizen to open with its own file tools, since a post reaches a turn as markdown and an image
|
|
5778
|
+
* in it as its address. Any image the board keeps may be opened: every post, brief, topic, and
|
|
5779
|
+
* dashboard that could show one is in the projection every turn reads.
|
|
5780
|
+
*/
|
|
5781
|
+
async openImage(actor, input) {
|
|
5782
|
+
const args = VerbInputs.read_image.parse(input);
|
|
5783
|
+
await this.authorize(actor, "read_image");
|
|
5784
|
+
if (actor.scope === void 0) {
|
|
5785
|
+
throw new BoardError("INVALID_STATE", "read_image writes the image on the machine a turn runs on, so only a turn can call it");
|
|
5786
|
+
}
|
|
5787
|
+
if (this.imageSink === null) {
|
|
5788
|
+
throw new BoardError("NOT_FOUND", "this board has no runners to write an image to");
|
|
5789
|
+
}
|
|
5790
|
+
const id = imageIdOf(args.image_id);
|
|
5791
|
+
const { type, bytes } = await this.readImage(id);
|
|
5792
|
+
const file = await this.imageSink(actor, { id, type, bytes });
|
|
5793
|
+
return { id, type, size: bytes.byteLength, path: file };
|
|
5794
|
+
}
|
|
5406
5795
|
/** An uploaded image's bytes and kind, which its first bytes tell. */
|
|
5407
5796
|
async readImage(id) {
|
|
5408
5797
|
const file = this.paths.image(UlidSchema.parse(id));
|
|
@@ -5420,7 +5809,7 @@ ${current.body}`);
|
|
|
5420
5809
|
async listCrons() {
|
|
5421
5810
|
const crons = [];
|
|
5422
5811
|
for (const file of await listFiles(this.paths.crons())) {
|
|
5423
|
-
crons.push(await this.readCronFile(
|
|
5812
|
+
crons.push(await this.readCronFile(path8.join(this.paths.crons(), file)));
|
|
5424
5813
|
}
|
|
5425
5814
|
return crons;
|
|
5426
5815
|
}
|
|
@@ -5748,6 +6137,13 @@ ${current.body}`);
|
|
|
5748
6137
|
readImagesWith(source) {
|
|
5749
6138
|
this.imageSource = source;
|
|
5750
6139
|
}
|
|
6140
|
+
/**
|
|
6141
|
+
* Where `read_image` writes an image to: the runner the calling turn runs on, through the server
|
|
6142
|
+
* that hosts the runners. Without one, as in the admin CLI, nothing can be opened.
|
|
6143
|
+
*/
|
|
6144
|
+
openImagesWith(sink) {
|
|
6145
|
+
this.imageSink = sink;
|
|
6146
|
+
}
|
|
5751
6147
|
/**
|
|
5752
6148
|
* Refuses to end conversations while another citizen's turn is in one of them: `cut` picks the
|
|
5753
6149
|
* turns that would lose theirs. The caller's own turn is exempt, since ending a conversation
|
|
@@ -5767,7 +6163,7 @@ ${current.body}`);
|
|
|
5767
6163
|
async openThreadIds(dir, keep) {
|
|
5768
6164
|
const ids = /* @__PURE__ */ new Set();
|
|
5769
6165
|
for (const file of await listFiles(dir)) {
|
|
5770
|
-
const doc = await readMarkdown(
|
|
6166
|
+
const doc = await readMarkdown(path8.join(dir, file), ThreadFrontmatterSchema);
|
|
5771
6167
|
if (doc.data.state === "open" && keep({ ...doc.data, body: doc.body })) {
|
|
5772
6168
|
ids.add(doc.data.id);
|
|
5773
6169
|
}
|
|
@@ -5775,12 +6171,12 @@ ${current.body}`);
|
|
|
5775
6171
|
return ids;
|
|
5776
6172
|
}
|
|
5777
6173
|
/**
|
|
5778
|
-
* Refuses to archive a project while a turn runs in it, in a citizen's conversation of
|
|
5779
|
-
* channels in the society scope, or in
|
|
6174
|
+
* Refuses to archive or move a project while a turn runs in it, in a citizen's conversation of
|
|
6175
|
+
* its channels in the society scope, or in one of its open threads.
|
|
5780
6176
|
*/
|
|
5781
|
-
async assertProjectQuiet(actor, slug) {
|
|
6177
|
+
async assertProjectQuiet(actor, slug, what = `project ${slug} cannot be archived`) {
|
|
5782
6178
|
const threads = await this.openThreadIds(this.paths.threads(slug), () => true);
|
|
5783
|
-
this.assertNoTurnCut(actor,
|
|
6179
|
+
this.assertNoTurnCut(actor, what, (turn) => turn.scope === slug || turn.place === slug || turn.thread !== void 0 && threads.has(turn.thread));
|
|
5784
6180
|
}
|
|
5785
6181
|
async closeThread(actor, input) {
|
|
5786
6182
|
const args = VerbInputs.close_thread.parse(input);
|
|
@@ -6583,6 +6979,8 @@ ${rationale}`}`);
|
|
|
6583
6979
|
return this.removeCron(actor, VerbInputs.remove_cron.parse(input));
|
|
6584
6980
|
case "upload_image":
|
|
6585
6981
|
return this.uploadImage(actor, VerbInputs.upload_image.parse(input));
|
|
6982
|
+
case "read_image":
|
|
6983
|
+
return this.openImage(actor, VerbInputs.read_image.parse(input));
|
|
6586
6984
|
default:
|
|
6587
6985
|
throw new BoardError("VALIDATION", `unknown verb ${String(verb)}`);
|
|
6588
6986
|
}
|
|
@@ -6747,7 +7145,7 @@ ${rationale}`}`);
|
|
|
6747
7145
|
return part === void 0 ? this.paths.agentProject(agent, scope) : this.paths.agentConversation(agent, scope, part);
|
|
6748
7146
|
}
|
|
6749
7147
|
async readSessions(agent, project, part) {
|
|
6750
|
-
const file =
|
|
7148
|
+
const file = path8.join(this.sessionDir(agent, project, part), "sessions.json");
|
|
6751
7149
|
return await exists(file) ? readJson(file, SessionsFileSchema) : {};
|
|
6752
7150
|
}
|
|
6753
7151
|
/**
|
|
@@ -6758,7 +7156,7 @@ ${rationale}`}`);
|
|
|
6758
7156
|
await this.mutex.run(async () => {
|
|
6759
7157
|
const dir = this.sessionDir(agent, project, part);
|
|
6760
7158
|
await ensureDir(dir);
|
|
6761
|
-
const file =
|
|
7159
|
+
const file = path8.join(dir, "sessions.json");
|
|
6762
7160
|
const current = await exists(file) ? await readJson(file, SessionsFileSchema) : {};
|
|
6763
7161
|
const kept = current.runner === void 0 || current.runner === runner ? current : {};
|
|
6764
7162
|
await writeJson(file, { ...kept, [cli]: sessionId, runner });
|
|
@@ -6770,7 +7168,7 @@ ${rationale}`}`);
|
|
|
6770
7168
|
*/
|
|
6771
7169
|
async dropSession(agent, project, cli, sessionId, part) {
|
|
6772
7170
|
return this.mutex.run(async () => {
|
|
6773
|
-
const file =
|
|
7171
|
+
const file = path8.join(this.sessionDir(agent, project, part), "sessions.json");
|
|
6774
7172
|
if (!await exists(file)) {
|
|
6775
7173
|
return false;
|
|
6776
7174
|
}
|
|
@@ -6785,7 +7183,7 @@ ${rationale}`}`);
|
|
|
6785
7183
|
}
|
|
6786
7184
|
/** The last turn of one conversation: the scope's home, a channel's, a project's, or a thread's. */
|
|
6787
7185
|
async readLastTurn(agent, project, part) {
|
|
6788
|
-
const file =
|
|
7186
|
+
const file = path8.join(this.sessionDir(agent, project, part), "last-turn.json");
|
|
6789
7187
|
return await exists(file) ? readJson(file, TurnRecordSchema) : null;
|
|
6790
7188
|
}
|
|
6791
7189
|
/** The latest turn a citizen took in a scope, in any of its conversations there. */
|
|
@@ -6793,9 +7191,9 @@ ${rationale}`}`);
|
|
|
6793
7191
|
let latest = await this.readLastTurn(agent, project);
|
|
6794
7192
|
const place = this.paths.agentProject(agent, project);
|
|
6795
7193
|
const parts = [
|
|
6796
|
-
...await listDirs(
|
|
6797
|
-
...(await listDirs(
|
|
6798
|
-
...(await listDirs(
|
|
7194
|
+
...await listDirs(path8.join(place, "threads")),
|
|
7195
|
+
...(await listDirs(path8.join(place, "channels"))).map((name) => `#${name}`),
|
|
7196
|
+
...(await listDirs(path8.join(place, "places"))).map((name) => `~${name}`)
|
|
6799
7197
|
];
|
|
6800
7198
|
for (const part of parts) {
|
|
6801
7199
|
const turn = await this.readLastTurn(agent, project, part);
|
|
@@ -6818,6 +7216,7 @@ ${rationale}`}`);
|
|
|
6818
7216
|
...parsed.channel === void 0 ? {} : { channel: parsed.channel },
|
|
6819
7217
|
...parsed.place === void 0 ? {} : { place: parsed.place },
|
|
6820
7218
|
trigger: parsed.trigger.kind,
|
|
7219
|
+
...parsed.trigger.heartbeat === void 0 ? {} : { heartbeat: parsed.trigger.heartbeat },
|
|
6821
7220
|
session: parsed.session,
|
|
6822
7221
|
runner: parsed.runner
|
|
6823
7222
|
});
|
|
@@ -6840,6 +7239,11 @@ ${rationale}`}`);
|
|
|
6840
7239
|
for (const id of parsed.crons ?? []) {
|
|
6841
7240
|
await this.recordCronRun(id, parsed, failed);
|
|
6842
7241
|
}
|
|
7242
|
+
if (parsed.exitReason === "completed" || parsed.exitReason === "blocked") {
|
|
7243
|
+
for (const shown of parsed.cronFailures ?? []) {
|
|
7244
|
+
await this.catchUpCron(shown, parsed);
|
|
7245
|
+
}
|
|
7246
|
+
}
|
|
6843
7247
|
if (parsed.exitReason === "stopped") {
|
|
6844
7248
|
await this.events.append("turn.stopped", parsed.stoppedBy ?? USER_NAME, {
|
|
6845
7249
|
...parsed.id === void 0 ? {} : { turnId: parsed.id },
|
|
@@ -6857,10 +7261,12 @@ ${rationale}`}`);
|
|
|
6857
7261
|
...parsed.channel === void 0 ? {} : { channel: parsed.channel },
|
|
6858
7262
|
...parsed.place === void 0 ? {} : { place: parsed.place },
|
|
6859
7263
|
trigger: parsed.trigger.kind,
|
|
7264
|
+
...parsed.trigger.heartbeat === void 0 ? {} : { heartbeat: parsed.trigger.heartbeat },
|
|
6860
7265
|
session: parsed.session,
|
|
6861
7266
|
exitReason: parsed.exitReason,
|
|
6862
7267
|
costUsd: parsed.costUsd,
|
|
6863
7268
|
...parsed.usage === null ? {} : { usage: parsed.usage },
|
|
7269
|
+
...parsed.contextTokens === void 0 ? {} : { contextTokens: parsed.contextTokens },
|
|
6864
7270
|
toolCalls: parsed.toolCalls,
|
|
6865
7271
|
model: parsed.model,
|
|
6866
7272
|
summary: parsed.status?.summary ?? null,
|
|
@@ -6979,6 +7385,27 @@ ${rationale}`}`);
|
|
|
6979
7385
|
failures: outcome === "failed" ? cron.failures + 1 : outcome === "completed" ? 0 : cron.failures
|
|
6980
7386
|
});
|
|
6981
7387
|
}
|
|
7388
|
+
/**
|
|
7389
|
+
* Marks a cron's failed fire caught up by a completed turn whose prompt named it, while that
|
|
7390
|
+
* failure is still its latest run. Its schedule and its run of failures stay as they are.
|
|
7391
|
+
*/
|
|
7392
|
+
async catchUpCron(shown, record) {
|
|
7393
|
+
if (record.id === void 0 || !await exists(this.paths.cron(shown.cron))) {
|
|
7394
|
+
return;
|
|
7395
|
+
}
|
|
7396
|
+
const cron = await this.readCron(shown.cron);
|
|
7397
|
+
const failure = cronFailure(cron);
|
|
7398
|
+
if (failure === null || failure.turnId !== shown.turnId) {
|
|
7399
|
+
return;
|
|
7400
|
+
}
|
|
7401
|
+
await this.writeCron({
|
|
7402
|
+
...cron,
|
|
7403
|
+
lastRun: {
|
|
7404
|
+
...failure,
|
|
7405
|
+
caughtUp: { turnId: record.id, at: record.endedAt ?? this.now().toISOString() }
|
|
7406
|
+
}
|
|
7407
|
+
});
|
|
7408
|
+
}
|
|
6982
7409
|
/**
|
|
6983
7410
|
* Whether a conversation with a workspace of its own has ended: a thread closed, a task ended
|
|
6984
7411
|
* (for one from before tasks had threads), a channel archived, or a project archived, which ends
|
|
@@ -7061,11 +7488,11 @@ ${rationale}`}`);
|
|
|
7061
7488
|
}
|
|
7062
7489
|
/** Small named state files under data/state, for cursors the scheduler must keep across restarts. */
|
|
7063
7490
|
async readState(name, schema, fallback) {
|
|
7064
|
-
const file =
|
|
7491
|
+
const file = path8.join(this.paths.state(), `${name}.json`);
|
|
7065
7492
|
return await exists(file) ? readJson(file, schema) : fallback;
|
|
7066
7493
|
}
|
|
7067
7494
|
async writeState(name, value) {
|
|
7068
|
-
await this.mutex.run(() => writeJson(
|
|
7495
|
+
await this.mutex.run(() => writeJson(path8.join(this.paths.state(), `${name}.json`), value));
|
|
7069
7496
|
}
|
|
7070
7497
|
/** The project's dashboard, empty at revision 0 when it has none. */
|
|
7071
7498
|
async readDashboard(slug) {
|
|
@@ -7096,7 +7523,7 @@ ${rationale}`}`);
|
|
|
7096
7523
|
async listRunners() {
|
|
7097
7524
|
const runners2 = [];
|
|
7098
7525
|
for (const file of await listFiles(this.paths.runners())) {
|
|
7099
|
-
runners2.push((await readMarkdown(
|
|
7526
|
+
runners2.push((await readMarkdown(path8.join(this.paths.runners(), file), RunnerSchema)).data);
|
|
7100
7527
|
}
|
|
7101
7528
|
return runners2;
|
|
7102
7529
|
}
|
|
@@ -7148,7 +7575,7 @@ ${rationale}`}`);
|
|
|
7148
7575
|
return this.runnerTokens.get(hashToken(token)) ?? null;
|
|
7149
7576
|
}
|
|
7150
7577
|
runnerTokensFile() {
|
|
7151
|
-
return
|
|
7578
|
+
return path8.join(this.paths.state(), "runners.json");
|
|
7152
7579
|
}
|
|
7153
7580
|
async readRunnerTokens() {
|
|
7154
7581
|
const file = this.runnerTokensFile();
|
|
@@ -7162,7 +7589,8 @@ ${rationale}`}`);
|
|
|
7162
7589
|
}
|
|
7163
7590
|
/**
|
|
7164
7591
|
* Places a project on the runner that will hold its repository, unless it already lives on one,
|
|
7165
|
-
* and returns where it lives. Placement is once: every later turn of the project goes there
|
|
7592
|
+
* and returns where it lives. Placement is once: every later turn of the project goes there,
|
|
7593
|
+
* until the user moves the project (`recordProjectMove`).
|
|
7166
7594
|
*/
|
|
7167
7595
|
async placeProject(slug, runner) {
|
|
7168
7596
|
return this.mutex.run(async () => {
|
|
@@ -7204,12 +7632,12 @@ ${rationale}`}`);
|
|
|
7204
7632
|
}
|
|
7205
7633
|
const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
7206
7634
|
if (entry.isDirectory()) {
|
|
7207
|
-
await walk2(
|
|
7635
|
+
await walk2(path8.join(dir, entry.name), relative);
|
|
7208
7636
|
continue;
|
|
7209
7637
|
}
|
|
7210
7638
|
const file = entry.isFile() ? conflictCopyOf(relative) : null;
|
|
7211
7639
|
if (file !== null) {
|
|
7212
|
-
const since = await this.homes.changedAt(home, relative) ?? (await stat3(
|
|
7640
|
+
const since = await this.homes.changedAt(home, relative) ?? (await stat3(path8.join(home, relative)).catch(() => null))?.mtime.toISOString();
|
|
7213
7641
|
if (since !== void 0) {
|
|
7214
7642
|
found.push({ path: relative, file, since });
|
|
7215
7643
|
}
|
|
@@ -7283,6 +7711,74 @@ ${rationale}`}`);
|
|
|
7283
7711
|
return next;
|
|
7284
7712
|
});
|
|
7285
7713
|
}
|
|
7714
|
+
/**
|
|
7715
|
+
* The board's half of moving a project to another runner, which only the user does, checked
|
|
7716
|
+
* before anything travels and while the hub holds the project's turns: the project is active and
|
|
7717
|
+
* lives on a runner other than `to`, which the board knows; no task there is landing, since its
|
|
7718
|
+
* landing runs on the old runner; and no turn runs in the project. Tasks in play move with their
|
|
7719
|
+
* branches, so the answer lists them for the capabilities the new runner must offer.
|
|
7720
|
+
*/
|
|
7721
|
+
async prepareProjectMove(actor, slug, to) {
|
|
7722
|
+
this.assertUser(actor, "only the user may move a project to another runner");
|
|
7723
|
+
const runner = NameSchema.parse(to);
|
|
7724
|
+
return this.mutex.run(async () => {
|
|
7725
|
+
const project = await this.readActiveProject(slug);
|
|
7726
|
+
const from = project.runner;
|
|
7727
|
+
if (from === void 0) {
|
|
7728
|
+
throw new BoardError("INVALID_STATE", `project ${slug} lives on no runner yet; its first turn places it`);
|
|
7729
|
+
}
|
|
7730
|
+
if (from === runner) {
|
|
7731
|
+
throw new BoardError("INVALID_STATE", `project ${slug} already lives on ${runner}`);
|
|
7732
|
+
}
|
|
7733
|
+
await this.readRunner(runner);
|
|
7734
|
+
const inPlay2 = (await this.listTasks(slug)).filter((task) => task.status === "open" || task.status === "claimed");
|
|
7735
|
+
const landing = inPlay2.filter((task) => task.completing);
|
|
7736
|
+
if (landing.length > 0) {
|
|
7737
|
+
throw new BoardError("INVALID_STATE", `project ${slug} cannot be moved while ${landing.length} task(s) land on ${from} (${landing.map((task) => task.id).join(", ")}); move it once they have landed`);
|
|
7738
|
+
}
|
|
7739
|
+
await this.assertProjectQuiet(actor, slug, `project ${slug} cannot be moved`);
|
|
7740
|
+
return { project: { ...project, runner: from }, inPlay: inPlay2 };
|
|
7741
|
+
});
|
|
7742
|
+
}
|
|
7743
|
+
/**
|
|
7744
|
+
* Records that a project moved, once its new runner holds every branch its old one had, or with
|
|
7745
|
+
* nothing carried when the user moved it while the old runner was away: every later turn of
|
|
7746
|
+
* the project goes to `to`. Refused when the project was archived or moved meanwhile.
|
|
7747
|
+
*/
|
|
7748
|
+
async recordProjectMove(actor, move) {
|
|
7749
|
+
this.assertUser(actor, "only the user may move a project to another runner");
|
|
7750
|
+
return this.mutex.run(async () => {
|
|
7751
|
+
const current = await this.readActiveProject(move.slug);
|
|
7752
|
+
if (current.runner !== move.from) {
|
|
7753
|
+
throw new BoardError("INVALID_STATE", `project ${move.slug} no longer lives on ${move.from}, so this move no longer applies`);
|
|
7754
|
+
}
|
|
7755
|
+
await this.readRunner(move.to);
|
|
7756
|
+
const project = await this.updateProject(move.slug, (each) => ({ ...each, runner: move.to }));
|
|
7757
|
+
await this.events.append("project.moved", actor.name, {
|
|
7758
|
+
slug: move.slug,
|
|
7759
|
+
from: move.from,
|
|
7760
|
+
to: move.to,
|
|
7761
|
+
carried: move.carried
|
|
7762
|
+
});
|
|
7763
|
+
return project;
|
|
7764
|
+
});
|
|
7765
|
+
}
|
|
7766
|
+
/**
|
|
7767
|
+
* Makes the repository a move carries a project through, for the server's git route to serve,
|
|
7768
|
+
* and answers the move's id, which names it; `dropMoveRepo` removes it when the move ends.
|
|
7769
|
+
*/
|
|
7770
|
+
async openMoveRepo() {
|
|
7771
|
+
const id = this.newId();
|
|
7772
|
+
await this.moves.open(this.paths.move(id));
|
|
7773
|
+
return id;
|
|
7774
|
+
}
|
|
7775
|
+
async dropMoveRepo(id) {
|
|
7776
|
+
await this.moves.drop(this.paths.move(UlidSchema.parse(id)));
|
|
7777
|
+
}
|
|
7778
|
+
/** Where the moves' repositories live, which the server's git route serves from. */
|
|
7779
|
+
get moveRepos() {
|
|
7780
|
+
return this.paths.moves();
|
|
7781
|
+
}
|
|
7286
7782
|
/** The projection a runner mirrors for its agents to read, by relative path, with hashes. */
|
|
7287
7783
|
async boardManifest() {
|
|
7288
7784
|
return this.files.manifest(this.paths.board, () => true);
|
|
@@ -7293,7 +7789,7 @@ ${rationale}`}`);
|
|
|
7293
7789
|
async writeTurnRecord(record) {
|
|
7294
7790
|
const dir = this.sessionDir(record.agent, record.project, conversationPart(record));
|
|
7295
7791
|
await ensureDir(dir);
|
|
7296
|
-
await writeJson(
|
|
7792
|
+
await writeJson(path8.join(dir, "last-turn.json"), record);
|
|
7297
7793
|
}
|
|
7298
7794
|
// ---------------------------------------------------------------------------------------------
|
|
7299
7795
|
// Internals
|
|
@@ -7412,8 +7908,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7412
7908
|
async ensureAgentProject(name, slug) {
|
|
7413
7909
|
const dir = this.paths.agentProject(name, slug);
|
|
7414
7910
|
await ensureDir(dir);
|
|
7415
|
-
if (!await exists(
|
|
7416
|
-
await writeJson(
|
|
7911
|
+
if (!await exists(path8.join(dir, "sessions.json"))) {
|
|
7912
|
+
await writeJson(path8.join(dir, "sessions.json"), {});
|
|
7417
7913
|
}
|
|
7418
7914
|
}
|
|
7419
7915
|
/**
|
|
@@ -7655,11 +8151,11 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7655
8151
|
const file = this.paths.societySkillFile(skill.name);
|
|
7656
8152
|
const replaced = await exists(file);
|
|
7657
8153
|
const taken = proposal.skillFiles;
|
|
7658
|
-
const staged =
|
|
8154
|
+
const staged = path8.join(this.paths.proposalFiles(proposal.id), "SKILL.md");
|
|
7659
8155
|
if (taken === void 0 || !await exists(staged)) {
|
|
7660
8156
|
throw new BoardError("INVALID_STATE", `proposal ${proposal.id} holds no skill files`);
|
|
7661
8157
|
}
|
|
7662
|
-
await
|
|
8158
|
+
await rm3(dir, { recursive: true, force: true });
|
|
7663
8159
|
await cp(this.paths.proposalFiles(proposal.id), dir, { recursive: true });
|
|
7664
8160
|
const { body } = await readLooseMarkdown(file);
|
|
7665
8161
|
await writeMarkdown(file, {
|
|
@@ -7712,10 +8208,10 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7712
8208
|
return `the frontmatter of ${folder}/SKILL.md in ${proposal.proposedBy}'s home does not parse (${manual.error})`;
|
|
7713
8209
|
}
|
|
7714
8210
|
const dir = this.paths.proposalFiles(proposal.id);
|
|
7715
|
-
await
|
|
8211
|
+
await rm3(dir, { recursive: true, force: true });
|
|
7716
8212
|
for (const entry of taken.files) {
|
|
7717
|
-
const target =
|
|
7718
|
-
await ensureDir(
|
|
8213
|
+
const target = path8.join(dir, ...entry.path.split("/"));
|
|
8214
|
+
await ensureDir(path8.dirname(target));
|
|
7719
8215
|
await writeFile4(target, contents.get(entry.path) ?? Buffer.alloc(0));
|
|
7720
8216
|
await chmod2(target, entry.executable ? 493 : 420);
|
|
7721
8217
|
}
|
|
@@ -7806,7 +8302,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7806
8302
|
if (!isSafeRelativePath(entry.path)) {
|
|
7807
8303
|
continue;
|
|
7808
8304
|
}
|
|
7809
|
-
const bytes = await readFile4(
|
|
8305
|
+
const bytes = await readFile4(path8.join(dir, ...entry.path.split("/"))).catch(() => null);
|
|
7810
8306
|
files.push({
|
|
7811
8307
|
...entry,
|
|
7812
8308
|
...bytes === null ? { text: null, truncated: false } : skillFileText(bytes)
|
|
@@ -7955,7 +8451,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7955
8451
|
this.assertNoTurnCut(actor, `${ref} cannot be archived`, (turn) => turn.channel === channel && turn.scope === scope || turn.thread !== void 0 && closing.has(turn.thread));
|
|
7956
8452
|
const threadsClosed = [];
|
|
7957
8453
|
for (const file of await listFiles(threads)) {
|
|
7958
|
-
const doc = await readMarkdown(
|
|
8454
|
+
const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
|
|
7959
8455
|
if (doc.data.channel !== ref || doc.data.state !== "open") {
|
|
7960
8456
|
continue;
|
|
7961
8457
|
}
|
|
@@ -8087,7 +8583,7 @@ ${charter.purpose}
|
|
|
8087
8583
|
const threads = this.paths.threads(slug);
|
|
8088
8584
|
const threadsClosed = [];
|
|
8089
8585
|
for (const file of await listFiles(threads)) {
|
|
8090
|
-
const doc = await readMarkdown(
|
|
8586
|
+
const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
|
|
8091
8587
|
if (doc.data.state !== "open") {
|
|
8092
8588
|
continue;
|
|
8093
8589
|
}
|
|
@@ -8162,10 +8658,10 @@ ${charter.purpose}
|
|
|
8162
8658
|
...project,
|
|
8163
8659
|
members: project.members.filter((member) => member !== name)
|
|
8164
8660
|
}));
|
|
8165
|
-
const sessions =
|
|
8661
|
+
const sessions = path8.join(this.paths.agentProject(name, slug), "sessions.json");
|
|
8166
8662
|
if (await exists(sessions)) {
|
|
8167
8663
|
const archived = `sessions.archived.${ts.replace(/[:.]/g, "-")}.json`;
|
|
8168
|
-
await rename3(sessions,
|
|
8664
|
+
await rename3(sessions, path8.join(this.paths.agentProject(name, slug), archived));
|
|
8169
8665
|
}
|
|
8170
8666
|
}
|
|
8171
8667
|
this.tokenIndex.delete(current.tokenHash);
|
|
@@ -8279,7 +8775,7 @@ ${charter.purpose}
|
|
|
8279
8775
|
}
|
|
8280
8776
|
assertMayGate(actor, what) {
|
|
8281
8777
|
if (!PLANNING_ROLES.includes(actor.role)) {
|
|
8282
|
-
throw new BoardError("FORBIDDEN", `only the user, the steward, and the concierge may ${what}; ask one of them in the task's thread`);
|
|
8778
|
+
throw new BoardError("FORBIDDEN", `only the user, the steward, and the concierge may ${what}; ask one of them in the task's thread, with a mention`);
|
|
8283
8779
|
}
|
|
8284
8780
|
}
|
|
8285
8781
|
/** The user holds anything; everyone else must be a project member the current stage admits. */
|
|
@@ -8321,6 +8817,51 @@ ${charter.purpose}
|
|
|
8321
8817
|
leaseEnd(now) {
|
|
8322
8818
|
return new Date(now.getTime() + this.leaseMs).toISOString();
|
|
8323
8819
|
}
|
|
8820
|
+
/**
|
|
8821
|
+
* The lease a holder's own crons in the task's thread keep its claim to, when later than the one
|
|
8822
|
+
* it has, else null. While one of them is pending, the lease is renewed for a full lease once less
|
|
8823
|
+
* than half is left, so it never lapses between fires that may be hours apart; once every one is
|
|
8824
|
+
* paused, ended, or failed at its latest fire with nothing catching it up (`cronStoppedAt`), it
|
|
8825
|
+
* runs a full lease from when the last of them stopped.
|
|
8826
|
+
*/
|
|
8827
|
+
keptLease(task, crons, now) {
|
|
8828
|
+
if (task.leaseExpiresAt === void 0) {
|
|
8829
|
+
return null;
|
|
8830
|
+
}
|
|
8831
|
+
const lease = Date.parse(task.leaseExpiresAt);
|
|
8832
|
+
let pending = false;
|
|
8833
|
+
let stopped = null;
|
|
8834
|
+
for (const cron of crons) {
|
|
8835
|
+
if (cron.agent !== task.claimedBy || !firesInTask(cron, task)) {
|
|
8836
|
+
continue;
|
|
8837
|
+
}
|
|
8838
|
+
const at = cronStoppedAt(cron);
|
|
8839
|
+
if (at === null) {
|
|
8840
|
+
pending = true;
|
|
8841
|
+
} else {
|
|
8842
|
+
stopped = Math.max(stopped ?? 0, Date.parse(at));
|
|
8843
|
+
}
|
|
8844
|
+
}
|
|
8845
|
+
const kept = pending ? lease - now.getTime() < this.leaseMs / 2 ? now.getTime() + this.leaseMs : lease : stopped === null ? lease : stopped + this.leaseMs;
|
|
8846
|
+
return kept > lease ? new Date(kept).toISOString() : null;
|
|
8847
|
+
}
|
|
8848
|
+
/**
|
|
8849
|
+
* The lease a holder keeps while the user's pause holds its turns in the task's project
|
|
8850
|
+
* (`heldOn`), when later than the one it has, else null. It is renewed for a full lease once less
|
|
8851
|
+
* than half is left, as for a pending cron, so it never lapses while the holder cannot take a
|
|
8852
|
+
* turn there, and after the resume the holder has at least half a lease to come back to it.
|
|
8853
|
+
*/
|
|
8854
|
+
async pausedLease(task, runners2, runner, now) {
|
|
8855
|
+
if (task.claimedBy === void 0 || task.leaseExpiresAt === void 0) {
|
|
8856
|
+
return null;
|
|
8857
|
+
}
|
|
8858
|
+
const holder = await this.readAgent(task.claimedBy).catch(() => null);
|
|
8859
|
+
if (holder === null || !heldOn(runners2, runner, holder.cli)) {
|
|
8860
|
+
return null;
|
|
8861
|
+
}
|
|
8862
|
+
const lease = Date.parse(task.leaseExpiresAt);
|
|
8863
|
+
return lease - now.getTime() < this.leaseMs / 2 ? this.leaseEnd(now) : null;
|
|
8864
|
+
}
|
|
8324
8865
|
leaseExpired(task, now) {
|
|
8325
8866
|
return task.leaseExpiresAt !== void 0 && new Date(task.leaseExpiresAt).getTime() <= now.getTime();
|
|
8326
8867
|
}
|
|
@@ -8433,11 +8974,7 @@ ${charter.purpose}
|
|
|
8433
8974
|
if (task.claimedBy === actor.name || task.createdBy === actor.name || task.stages.some((stage) => stage.agent === actor.name || stage.holders.includes(actor.name))) {
|
|
8434
8975
|
return true;
|
|
8435
8976
|
}
|
|
8436
|
-
|
|
8437
|
-
return false;
|
|
8438
|
-
}
|
|
8439
|
-
const agent = await this.readAgent(actor.name);
|
|
8440
|
-
return agent.memberships.includes(task.project) && mayHoldStage(actor, task);
|
|
8977
|
+
return await this.mayTakeStage(actor, task);
|
|
8441
8978
|
}
|
|
8442
8979
|
if (subject?.kind === "proposal") {
|
|
8443
8980
|
const proposal = await this.readProposal(subject.id);
|
|
@@ -8451,6 +8988,34 @@ ${charter.purpose}
|
|
|
8451
8988
|
}
|
|
8452
8989
|
return false;
|
|
8453
8990
|
}
|
|
8991
|
+
/** Whether a task's current stage waits for a holder and the member may take it. */
|
|
8992
|
+
async mayTakeStage(actor, task) {
|
|
8993
|
+
if (task.status !== "open" || task.completing) {
|
|
8994
|
+
return false;
|
|
8995
|
+
}
|
|
8996
|
+
const agent = await this.readAgent(actor.name);
|
|
8997
|
+
return agent.memberships.includes(task.project) && mayHoldStage(actor, task);
|
|
8998
|
+
}
|
|
8999
|
+
/**
|
|
9000
|
+
* Which of a thread's posts count toward a reader's heartbeat (see `unreadByConversation`). A
|
|
9001
|
+
* thread, or a task, the board cannot find counts as an open topic's would.
|
|
9002
|
+
*/
|
|
9003
|
+
async threadCount(actor, id) {
|
|
9004
|
+
const found = await this.tryFindThread(id);
|
|
9005
|
+
if (found?.thread.state === "closed") {
|
|
9006
|
+
return "none";
|
|
9007
|
+
}
|
|
9008
|
+
const subject = found?.thread.subject;
|
|
9009
|
+
const task = subject?.kind === "task" ? await this.findTask(subject.id).then((location) => location.task, () => null) : null;
|
|
9010
|
+
if (task === null) {
|
|
9011
|
+
return "every";
|
|
9012
|
+
}
|
|
9013
|
+
const stageIsTheirs = task.status === "claimed" ? task.claimedBy === actor.name : await this.mayTakeStage(actor, task);
|
|
9014
|
+
if (stageIsTheirs) {
|
|
9015
|
+
return "every";
|
|
9016
|
+
}
|
|
9017
|
+
return task.createdBy === actor.name ? "asks-user" : "asked";
|
|
9018
|
+
}
|
|
8454
9019
|
async threadScopes() {
|
|
8455
9020
|
const projects = await listDirs(this.paths.projects());
|
|
8456
9021
|
return [this.paths.societyThreads(), ...projects.map((slug) => this.paths.threads(slug))];
|
|
@@ -8506,7 +9071,7 @@ ${charter.purpose}
|
|
|
8506
9071
|
if (since !== null && id <= since || upto !== void 0 && id > upto) {
|
|
8507
9072
|
continue;
|
|
8508
9073
|
}
|
|
8509
|
-
const doc = await readMarkdown(
|
|
9074
|
+
const doc = await readMarkdown(path8.join(dir, file), MessageFrontmatterSchema);
|
|
8510
9075
|
messages.push({ ...doc.data, body: doc.body });
|
|
8511
9076
|
}
|
|
8512
9077
|
return messages;
|
|
@@ -8555,6 +9120,15 @@ ${charter.purpose}
|
|
|
8555
9120
|
};
|
|
8556
9121
|
|
|
8557
9122
|
// packages/scheduler/dist/wake.js
|
|
9123
|
+
function heartbeatCause(input) {
|
|
9124
|
+
if (input.digestSize > 0) {
|
|
9125
|
+
return "unread";
|
|
9126
|
+
}
|
|
9127
|
+
if (input.claimsHeld > 0) {
|
|
9128
|
+
return "held";
|
|
9129
|
+
}
|
|
9130
|
+
return input.waitingStages > 0 ? "waiting" : null;
|
|
9131
|
+
}
|
|
8558
9132
|
function decideWake(input) {
|
|
8559
9133
|
if (input.paused) {
|
|
8560
9134
|
return { wake: false, reason: "society paused", priority: 0 };
|
|
@@ -8586,7 +9160,7 @@ function decideWake(input) {
|
|
|
8586
9160
|
case "heartbeat":
|
|
8587
9161
|
break;
|
|
8588
9162
|
}
|
|
8589
|
-
if (input
|
|
9163
|
+
if (heartbeatCause(input) === null) {
|
|
8590
9164
|
return { wake: false, reason: "nothing to read, hold, or take", priority: 0 };
|
|
8591
9165
|
}
|
|
8592
9166
|
return {
|
|
@@ -8634,6 +9208,7 @@ var DEFAULT_TIMINGS = Object.freeze({
|
|
|
8634
9208
|
homeConflictMs: 24 * 36e5,
|
|
8635
9209
|
scaleCooldownMs: 36e5,
|
|
8636
9210
|
reflectionMs: 24 * 36e5,
|
|
9211
|
+
sessionCapTokens: 3e5,
|
|
8637
9212
|
transientRetryMs: [6e4, 3 * 6e4, 10 * 6e4]
|
|
8638
9213
|
});
|
|
8639
9214
|
var SchedulerTimingsSchema = z17.object({
|
|
@@ -8652,6 +9227,7 @@ var SchedulerTimingsSchema = z17.object({
|
|
|
8652
9227
|
homeConflictMs: z17.number().positive(),
|
|
8653
9228
|
scaleCooldownMs: z17.number().positive(),
|
|
8654
9229
|
reflectionMs: z17.number().positive(),
|
|
9230
|
+
sessionCapTokens: z17.number().int().positive(),
|
|
8655
9231
|
transientRetryMs: z17.array(z17.number().positive())
|
|
8656
9232
|
}).partial();
|
|
8657
9233
|
function parseTimings(input) {
|
|
@@ -8679,6 +9255,18 @@ var StateSchema = z17.object({
|
|
|
8679
9255
|
lastHeartbeat: z17.record(z17.string(), z17.string()),
|
|
8680
9256
|
/** Waiting stages already signalled, as `task:stage`, so each is logged once while it waits. */
|
|
8681
9257
|
waitingSeen: z17.record(z17.string(), z17.string()).default({}),
|
|
9258
|
+
/**
|
|
9259
|
+
* The last pass at which each waiting stage was held by the user's pause of its runner, as
|
|
9260
|
+
* `task:stage`; its wait counts from there, so a stage that waited through a pause is not
|
|
9261
|
+
* signalled the moment the pause ends.
|
|
9262
|
+
*/
|
|
9263
|
+
waitingHeld: z17.record(z17.string(), z17.string()).default({}),
|
|
9264
|
+
/**
|
|
9265
|
+
* The last operations pass at which each citizen was held by the user's pause of a runner it
|
|
9266
|
+
* works on; its idle time counts from there, so a citizen held through a pause is not signalled
|
|
9267
|
+
* idle the moment the pause ends.
|
|
9268
|
+
*/
|
|
9269
|
+
idleHeld: z17.record(z17.string(), z17.string()).default({}),
|
|
8682
9270
|
/** Queued dispatches survive a restart; the cursor has already moved past the events that made them. */
|
|
8683
9271
|
pending: z17.record(z17.string(), PendingSchema).default({}),
|
|
8684
9272
|
opsReported: z17.record(z17.string(), z17.string()).default({}),
|
|
@@ -8694,7 +9282,12 @@ var StateSchema = z17.object({
|
|
|
8694
9282
|
costSinceReport: z17.number().nonnegative().default(0),
|
|
8695
9283
|
turnsSinceReport: z17.number().int().nonnegative().default(0),
|
|
8696
9284
|
/** When each member last reflected, or was first seen; the cadence counts from here. */
|
|
8697
|
-
lastReflection: z17.record(z17.string(), z17.string()).default({})
|
|
9285
|
+
lastReflection: z17.record(z17.string(), z17.string()).default({}),
|
|
9286
|
+
/**
|
|
9287
|
+
* The home session each society member last asked for an early reflection for, once per
|
|
9288
|
+
* session, and whether that reflection still waits to be queued.
|
|
9289
|
+
*/
|
|
9290
|
+
earlyReflection: z17.record(z17.string(), z17.object({ session: z17.string(), due: z17.boolean() })).default({})
|
|
8698
9291
|
});
|
|
8699
9292
|
function messageOf(trigger) {
|
|
8700
9293
|
return trigger.kind === "mention" || trigger.kind === "user_post" ? trigger.messageId : void 0;
|
|
@@ -9038,6 +9631,7 @@ var Scheduler = class {
|
|
|
9038
9631
|
const cost = payload["costUsd"];
|
|
9039
9632
|
this.state.costSinceReport += typeof cost === "number" ? cost : 0;
|
|
9040
9633
|
this.state.turnsSinceReport += 1;
|
|
9634
|
+
await this.checkSessionSize(event);
|
|
9041
9635
|
return;
|
|
9042
9636
|
}
|
|
9043
9637
|
case "wake.requested": {
|
|
@@ -9611,10 +10205,14 @@ var Scheduler = class {
|
|
|
9611
10205
|
* A heartbeat per member and scope on the cadence, which then looks at every conversation of the
|
|
9612
10206
|
* scope: the unread messages of its home, of each other channel, and of each thread, a stage the
|
|
9613
10207
|
* member holds, and a stage of an unblocked task waiting that it may take, each in its task's
|
|
9614
|
-
* thread.
|
|
9615
|
-
*
|
|
10208
|
+
* thread. A stage counts for neither while the member has a cron pending in its task's thread
|
|
10209
|
+
* (`cronStoppedAt`: neither paused nor ended, and its latest fire's turn did not fail or was
|
|
10210
|
+
* caught up since), whose fires are its checks there. Each conversation with something wakes on
|
|
10211
|
+
* its own, so a heartbeat never brings one task's business into another's turn, and its trigger
|
|
10212
|
+
* says what it woke for (`heartbeatCause`).
|
|
9616
10213
|
*/
|
|
9617
10214
|
async checkHeartbeats(now) {
|
|
10215
|
+
let pendingCrons;
|
|
9618
10216
|
for (const agent of await this.board.listAgents()) {
|
|
9619
10217
|
if (agent.status !== "active" || agent.cli === null) {
|
|
9620
10218
|
continue;
|
|
@@ -9640,6 +10238,9 @@ var Scheduler = class {
|
|
|
9640
10238
|
this.state.lastHeartbeat[cadence] = iso(now);
|
|
9641
10239
|
unread ??= await this.board.unreadByConversation(actor);
|
|
9642
10240
|
held ??= await this.board.heldClaims(agent.name);
|
|
10241
|
+
pendingCrons ??= (await this.board.listCrons()).filter((cron) => cronStoppedAt(cron) === null);
|
|
10242
|
+
const crons = pendingCrons;
|
|
10243
|
+
const leftToCron = (task) => crons.some((cron) => cron.agent === agent.name && firesInTask(cron, task));
|
|
9643
10244
|
const reasons = /* @__PURE__ */ new Map();
|
|
9644
10245
|
const reasonsIn = (beside) => {
|
|
9645
10246
|
const key = conversationKey2(agent.name, project, beside);
|
|
@@ -9658,13 +10259,13 @@ var Scheduler = class {
|
|
|
9658
10259
|
}
|
|
9659
10260
|
}
|
|
9660
10261
|
for (const task of held) {
|
|
9661
|
-
if (task.project === project) {
|
|
10262
|
+
if (task.project === project && !leftToCron(task)) {
|
|
9662
10263
|
reasonsIn({ thread: { id: task.id, task: true } }).claimsHeld += 1;
|
|
9663
10264
|
}
|
|
9664
10265
|
}
|
|
9665
10266
|
if (project !== SOCIETY_SCOPE) {
|
|
9666
10267
|
for (const task of await this.board.openTasks(project)) {
|
|
9667
|
-
if (mayHoldStage(agent, task) && (await this.blockersInPlay(task.blockedBy)).length === 0) {
|
|
10268
|
+
if (mayHoldStage(agent, task) && !leftToCron(task) && (await this.blockersInPlay(task.blockedBy)).length === 0) {
|
|
9668
10269
|
reasonsIn({ thread: { id: task.id, task: true } }).waiting += 1;
|
|
9669
10270
|
}
|
|
9670
10271
|
}
|
|
@@ -9673,7 +10274,16 @@ var Scheduler = class {
|
|
|
9673
10274
|
if (this.pending.has(key) || this.running.has(key)) {
|
|
9674
10275
|
continue;
|
|
9675
10276
|
}
|
|
9676
|
-
const
|
|
10277
|
+
const cause = heartbeatCause({
|
|
10278
|
+
digestSize: reason.digestSize,
|
|
10279
|
+
claimsHeld: reason.claimsHeld,
|
|
10280
|
+
waitingStages: reason.waiting
|
|
10281
|
+
});
|
|
10282
|
+
const trigger = TriggerSchema.parse({
|
|
10283
|
+
kind: "heartbeat",
|
|
10284
|
+
reason: "heartbeat",
|
|
10285
|
+
...cause === null ? {} : { heartbeat: cause }
|
|
10286
|
+
});
|
|
9677
10287
|
const decision = decideWake({
|
|
9678
10288
|
trigger,
|
|
9679
10289
|
digestSize: reason.digestSize,
|
|
@@ -9766,9 +10376,44 @@ var Scheduler = class {
|
|
|
9766
10376
|
item.dispatch = rest.length === 0 ? dispatch : { ...dispatch, crons: rest };
|
|
9767
10377
|
}
|
|
9768
10378
|
}
|
|
10379
|
+
/**
|
|
10380
|
+
* A society role's home session that a turn left at `sessionCapTokens` or more, by its latest
|
|
10381
|
+
* model call, asks for a reflection ahead of the cadence, which `checkReflections` queues once
|
|
10382
|
+
* none of the member's turns is queued or running; the reflection's end starts the home afresh
|
|
10383
|
+
* (`TurnHost`), so its history goes only once a reflection saved what to keep. A session asks
|
|
10384
|
+
* once, so a reflection that fails leaves the next to the cadence, and none asks while a
|
|
10385
|
+
* reflection is queued or running.
|
|
10386
|
+
*/
|
|
10387
|
+
async checkSessionSize(event) {
|
|
10388
|
+
const { payload } = event;
|
|
10389
|
+
const tokens = payload["contextTokens"];
|
|
10390
|
+
const session = stringOf(payload["session"]);
|
|
10391
|
+
if (typeof tokens !== "number" || tokens < this.timings.sessionCapTokens || session === null || payload["project"] !== SOCIETY_SCOPE || payload["thread"] !== void 0 || payload["channel"] !== void 0 || payload["place"] !== void 0 || payload["trigger"] === "reflection" || payload["sessionDropped"] === true) {
|
|
10392
|
+
return;
|
|
10393
|
+
}
|
|
10394
|
+
const agent = await this.tryReadAgent(event.actor);
|
|
10395
|
+
if (agent === null || agent.status !== "active") {
|
|
10396
|
+
return;
|
|
10397
|
+
}
|
|
10398
|
+
const charter = await this.board.readRole(agent.role).catch(() => null);
|
|
10399
|
+
if (charter === null || !charter.societyScope || !charter.reflects) {
|
|
10400
|
+
return;
|
|
10401
|
+
}
|
|
10402
|
+
if (this.state.earlyReflection[agent.name]?.session === session) {
|
|
10403
|
+
return;
|
|
10404
|
+
}
|
|
10405
|
+
const ofAgent = (key) => parseSessionKey(key)?.agent === agent.name;
|
|
10406
|
+
const reflecting = [...this.pending].some(([key, item]) => ofAgent(key) && item.dispatch.trigger.kind === "reflection") || [...this.exclusive].some(([key, kind]) => ofAgent(key) && kind === "reflection");
|
|
10407
|
+
if (reflecting) {
|
|
10408
|
+
return;
|
|
10409
|
+
}
|
|
10410
|
+
this.state.earlyReflection[agent.name] = { session, due: true };
|
|
10411
|
+
this.log.info({ agent: agent.name, contextTokens: tokens, cap: this.timings.sessionCapTokens }, "home session past the cap; a reflection is due");
|
|
10412
|
+
}
|
|
9769
10413
|
/**
|
|
9770
10414
|
* Every `reflectionMs`, a member whose charter reflects takes a reflection turn in the scope of
|
|
9771
|
-
* its latest working turn, unless it has not worked since its last reflection
|
|
10415
|
+
* its latest working turn, unless it has not worked since its last reflection; a society member
|
|
10416
|
+
* whose home session passed the cap takes one in its home ahead of the cadence, which restarts it.
|
|
9772
10417
|
*/
|
|
9773
10418
|
async checkReflections(now) {
|
|
9774
10419
|
for (const agent of await this.board.listAgents()) {
|
|
@@ -9779,6 +10424,15 @@ var Scheduler = class {
|
|
|
9779
10424
|
if (!charter.reflects) {
|
|
9780
10425
|
continue;
|
|
9781
10426
|
}
|
|
10427
|
+
const early = this.state.earlyReflection[agent.name];
|
|
10428
|
+
if (early?.due === true) {
|
|
10429
|
+
if (!this.busy(agent.name)) {
|
|
10430
|
+
this.state.lastReflection[agent.name] = iso(now);
|
|
10431
|
+
this.state.earlyReflection[agent.name] = { ...early, due: false };
|
|
10432
|
+
this.enqueue(agent.name, SOCIETY_SCOPE, { kind: "reflection", reason: "home session past the cap" }, now);
|
|
10433
|
+
}
|
|
10434
|
+
continue;
|
|
10435
|
+
}
|
|
9782
10436
|
const last = this.state.lastReflection[agent.name];
|
|
9783
10437
|
if (last === void 0) {
|
|
9784
10438
|
this.state.lastReflection[agent.name] = iso(now);
|
|
@@ -9792,8 +10446,7 @@ var Scheduler = class {
|
|
|
9792
10446
|
this.state.lastReflection[agent.name] = iso(now);
|
|
9793
10447
|
continue;
|
|
9794
10448
|
}
|
|
9795
|
-
|
|
9796
|
-
if (busy) {
|
|
10449
|
+
if (this.busy(agent.name)) {
|
|
9797
10450
|
continue;
|
|
9798
10451
|
}
|
|
9799
10452
|
this.state.lastReflection[agent.name] = iso(now);
|
|
@@ -9803,6 +10456,13 @@ var Scheduler = class {
|
|
|
9803
10456
|
this.enqueue(agent.name, latest.scope, { kind: "reflection", reason: "scheduled reflection" }, now);
|
|
9804
10457
|
}
|
|
9805
10458
|
}
|
|
10459
|
+
/**
|
|
10460
|
+
* Whether a member has a turn queued or running anywhere, so a reflection waits for the next tick
|
|
10461
|
+
* rather than merge into a working turn or run beside turns writing the memory it consolidates.
|
|
10462
|
+
*/
|
|
10463
|
+
busy(agent) {
|
|
10464
|
+
return [...this.pending.keys(), ...this.running.keys()].some((key) => parseSessionKey(key)?.agent === agent);
|
|
10465
|
+
}
|
|
9806
10466
|
/** The member's most recent turn that was not itself a reflection, with the scope it ran in. */
|
|
9807
10467
|
async latestWorkingTurn(agent) {
|
|
9808
10468
|
let latest = null;
|
|
@@ -9826,7 +10486,9 @@ var Scheduler = class {
|
|
|
9826
10486
|
*/
|
|
9827
10487
|
async checkWaitingStages(now) {
|
|
9828
10488
|
const stillWaiting = /* @__PURE__ */ new Set();
|
|
10489
|
+
const runners2 = await this.board.listRunners();
|
|
9829
10490
|
for (const project of await this.board.listProjects()) {
|
|
10491
|
+
let members;
|
|
9830
10492
|
for (const task of await this.board.openTasks(project.slug)) {
|
|
9831
10493
|
const stage = currentStage(task);
|
|
9832
10494
|
if (stage === void 0) {
|
|
@@ -9837,7 +10499,14 @@ var Scheduler = class {
|
|
|
9837
10499
|
if (this.state.waitingSeen[key] !== void 0) {
|
|
9838
10500
|
continue;
|
|
9839
10501
|
}
|
|
9840
|
-
|
|
10502
|
+
members ??= await this.board.projectMembers(project.slug);
|
|
10503
|
+
const takers = members.filter((member) => member.cli !== null && mayHoldStage(member, task));
|
|
10504
|
+
if (takers.length > 0 && takers.every((taker) => heldOn(runners2, project.runner, taker.cli))) {
|
|
10505
|
+
this.state.waitingHeld[key] = iso(now);
|
|
10506
|
+
continue;
|
|
10507
|
+
}
|
|
10508
|
+
const held = this.state.waitingHeld[key];
|
|
10509
|
+
const age = now - Math.max(Date.parse(task.stageSince), held === void 0 ? 0 : Date.parse(held));
|
|
9841
10510
|
if (age < this.timings.waitingStageMs) {
|
|
9842
10511
|
continue;
|
|
9843
10512
|
}
|
|
@@ -9858,6 +10527,11 @@ var Scheduler = class {
|
|
|
9858
10527
|
});
|
|
9859
10528
|
}
|
|
9860
10529
|
}
|
|
10530
|
+
for (const key of Object.keys(this.state.waitingHeld)) {
|
|
10531
|
+
if (!stillWaiting.has(key)) {
|
|
10532
|
+
delete this.state.waitingHeld[key];
|
|
10533
|
+
}
|
|
10534
|
+
}
|
|
9861
10535
|
for (const key of Object.keys(this.state.waitingSeen)) {
|
|
9862
10536
|
if (!stillWaiting.has(key)) {
|
|
9863
10537
|
delete this.state.waitingSeen[key];
|
|
@@ -9923,7 +10597,8 @@ var Scheduler = class {
|
|
|
9923
10597
|
const roles = (await this.board.listRoles()).filter((role) => role.name !== USER_ROLE);
|
|
9924
10598
|
const runners2 = await this.board.listRunners();
|
|
9925
10599
|
const anywhere = new Set(runners2.filter((runner) => runner.status === "connected").flatMap((runner) => runner.capabilities));
|
|
9926
|
-
|
|
10600
|
+
const projects = await this.board.listProjects();
|
|
10601
|
+
for (const project of projects) {
|
|
9927
10602
|
const slug = project.slug;
|
|
9928
10603
|
const tasks = await this.board.listTasks(slug);
|
|
9929
10604
|
const home = runners2.find((runner) => runner.name === project.runner);
|
|
@@ -10015,7 +10690,13 @@ var Scheduler = class {
|
|
|
10015
10690
|
}
|
|
10016
10691
|
}
|
|
10017
10692
|
const crons = (await this.board.listCrons()).filter((cron) => cron.ended === void 0);
|
|
10018
|
-
|
|
10693
|
+
const agents = await this.board.listAgents();
|
|
10694
|
+
for (const name of Object.keys(this.state.idleHeld)) {
|
|
10695
|
+
if (!agents.some((agent) => agent.name === name && agent.status === "active")) {
|
|
10696
|
+
delete this.state.idleHeld[name];
|
|
10697
|
+
}
|
|
10698
|
+
}
|
|
10699
|
+
for (const agent of agents) {
|
|
10019
10700
|
if (agent.status !== "active" || agent.cli === null) {
|
|
10020
10701
|
continue;
|
|
10021
10702
|
}
|
|
@@ -10027,7 +10708,12 @@ var Scheduler = class {
|
|
|
10027
10708
|
last = Math.max(last, Date.parse(ended));
|
|
10028
10709
|
}
|
|
10029
10710
|
}
|
|
10030
|
-
const
|
|
10711
|
+
const held = heldOn(runners2, agent.homeRunner, agent.cli) || projects.some((project) => agent.memberships.includes(project.slug) && heldOn(runners2, project.runner, agent.cli));
|
|
10712
|
+
if (held) {
|
|
10713
|
+
this.state.idleHeld[agent.name] = iso(now);
|
|
10714
|
+
}
|
|
10715
|
+
const heldAt = this.state.idleHeld[agent.name];
|
|
10716
|
+
const idle = now - Math.max(last, heldAt === void 0 ? 0 : Date.parse(heldAt));
|
|
10031
10717
|
if (idle >= this.timings.idleMemberMs) {
|
|
10032
10718
|
signals.push({
|
|
10033
10719
|
kind: "idle_member",
|
|
@@ -10249,6 +10935,10 @@ var Scheduler = class {
|
|
|
10249
10935
|
this.exclusive.set(key, "onboarding");
|
|
10250
10936
|
} else if (dispatch.trigger.kind === "reflection") {
|
|
10251
10937
|
this.exclusive.set(key, "reflection");
|
|
10938
|
+
const early = this.state.earlyReflection[dispatch.agent];
|
|
10939
|
+
if (early?.due === true) {
|
|
10940
|
+
this.state.earlyReflection[dispatch.agent] = { ...early, due: false };
|
|
10941
|
+
}
|
|
10252
10942
|
}
|
|
10253
10943
|
}
|
|
10254
10944
|
}
|
|
@@ -10376,20 +11066,21 @@ var Scheduler = class {
|
|
|
10376
11066
|
import { randomUUID } from "node:crypto";
|
|
10377
11067
|
|
|
10378
11068
|
// packages/turn-host/dist/render.js
|
|
11069
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
10379
11070
|
var TURN_CONTRACT = [
|
|
10380
11071
|
"- Every turn belongs to one conversation, named at the top of your prompt: your home conversation in a project or the society, for its general channel and for work tied to no other channel and no thread; the conversation of one other channel of the place, a workstream such as a release; outside projects, the conversation of one project's channels, general and its workstreams together, for posts there that ask something of you while you are not a member; or the conversation of one thread you take part in, a task's, a proposal's, or a topic's. Each has its own session and its own turns, which may run while this one does. The prompt carries this conversation only: in a thread, its new messages, or the whole thread when the conversation is new, and in a task's thread the task and its stage; in a channel's, a project's, or at home, that channel's, that project's channels', or general's messages that concern you. Read anything else you need with get_task, search, and the board's files, and leave other channels', other threads', and other projects' work to their own turns.",
|
|
10381
|
-
"- Act through the board tools (the `board` MCP server). Holding a stage is a lease; every turn that touches the task renews it.",
|
|
11072
|
+
"- Act through the board tools (the `board` MCP server). Holding a stage is a lease; every turn that touches the task renews it, and so does the board while a cron of yours in the task's thread is pending.",
|
|
10382
11073
|
"- Work only inside your worktree. In a task's conversation it is a worktree of its own on the task's branch `task/<id>`: commit your work there. The branch is checked out in one worktree at a time and kept for the turns of whoever holds the task's stage, so a turn of anyone else there, or one that starts while another turn has the branch, works detached at its tip, which Your workspace then says; its commits go on the branch by a fast-forward when a turn in the task ends with the branch free, and the task's thread is told of commits that no longer build on its tip. After each turn the runner commits what you left on the branch and lets it go for the next holder. Your home worktree is on your own branch, `agent/<name>`, for work tied to no task; nothing lands from it by itself. In a proposal's, a topic's, or a channel's conversation it is a worktree of its own, detached at the project's default branch, which goes when the thread closes or the channel is archived, so work that should last goes on a task's branch. Before each turn the runner fetches the project's remote and brings what holds no work of yours up to the default branch: your own branch when it has no commits of its own, a task's branch nobody has worked on, and a detached worktree with nothing uncommitted; what it leaves where it is, and why, is under Your workspace in the prompt. It never merges or commits for you.",
|
|
10383
11074
|
"- Posts that would wake this conversation may arrive while you work, delivered into the turn under a heading that says so and formatted like the digest. They are part of this turn: act on them now, after the step in hand, or not at all, as with the digest. The user may also stop a turn; the next turn is told.",
|
|
10384
11075
|
"- Silence is allowed. If the digest needs no reply, post nothing.",
|
|
10385
|
-
"- To come back to something later, set a cron with create_cron: a note on what to do then, and a five-field cron expression or one time with at. It wakes you in this conversation unless you name another, at most every fifteen minutes, and every fire is a paid turn, so end a cron with remove_cron once it has done its job. Never wait by
|
|
11076
|
+
"- To come back to something later, set a cron with create_cron: a note on what to do then, and a five-field cron expression or one time with at. It wakes you in this conversation unless you name another, at most every fifteen minutes, and every fire is a paid turn, so end a cron with remove_cron once it has done its job. Never wait by sleeping or leaving a process running: a turn's board token ends with the turn, so nothing it leaves behind can act on the board. While you check your own stage's work on a schedule, such as a running job, CI, or a deploy, keep holding the stage: the cron sets when you check, and while it is pending in the task's thread, neither ended nor paused, with its latest fire's turn not failed or a turn of yours there completed since, the board keeps your lease and the heartbeat leaves the stage to it, waking you there only for a post you have not read. Release a stage only to hand it on, or once its task's blocked_by names the tasks it waits on: the stage then wakes nobody until the last of them ends. Any other wait on someone else, such as the user or an outside party, is a stage of its own, not a held stage. The society's crons are under society/crons/ in the board projection.",
|
|
10386
11077
|
"- An @mention wakes the citizen named, and every wake costs a turn. Address someone with @ only when you need them to act; when you merely refer to citizens, write their names plainly. A name in a code span or a code block is no mention, so code keeps its @ as written. @user wakes nobody, since the user takes no turns; it is what puts a question before the user, so use it only when you need the user to answer or decide.",
|
|
10387
|
-
"- A thread is a conversation off a channel that reaches only its participants and whoever is mentioned in it. Every task has one, opened with it under the task's id on the channel of its project it was filed in, general unless it was filed in another: talk about a task there, with post_message and its thread_id, and the notes you give advance_task and update_task are posted there as you. Its participants are the task's creator, its stage holders and assignees, whoever may take the stage that waits, and anyone who posted; it closes when the task ends. Every proposal has one too, in governance, opened with its pitch and closed by its decision. Open other threads with open_thread on a channel with a title, for any other topic; close_thread posts your summary to its channel.",
|
|
10388
|
-
"- To show an image in a post, such as a screenshot or a chart, save it anywhere you can read it, such as a folder from `mktemp -d`, call upload_image with its path, and put the markdown it returns in the post, ``, with what the image shows as its alt text: the board keeps the image, so it shows in any conversation after the file is gone. It takes a PNG, JPEG, GIF, or WebP of at most 8 MB from your worktree, your home, or the system temp folder. An image committed on a task's branch, linked by its path in the task's worktree, still shows in place, and so does one at an https address. Never put an image or any other file into a message as base64 or a data: URL: the board does not show it, and every reader of the conversation gets it in full in their prompt.",
|
|
11078
|
+
"- A thread is a conversation off a channel that reaches only its participants and whoever is mentioned in it. Every task has one, opened with it under the task's id on the channel of its project it was filed in, general unless it was filed in another: talk about a task there, with post_message and its thread_id, and the notes you give advance_task and update_task are posted there as you. Its participants are the task's creator, its stage holders and assignees, whoever may take the stage that waits, and anyone who posted; it closes when the task ends. In a task's thread, only the current stage's holder, or while nobody holds it whoever may take it, wakes for your post, and a post that asks the user also wakes the task's creator; mention anyone else you need, including the concierge for a plan change. Every proposal has one too, in governance, opened with its pitch and closed by its decision. Open other threads with open_thread on a channel with a title, for any other topic; close_thread posts your summary to its channel.",
|
|
11079
|
+
"- To show an image in a post, such as a screenshot or a chart, save it anywhere you can read it, such as a folder from `mktemp -d`, call upload_image with its path, and put the markdown it returns in the post, ``, with what the image shows as its alt text: the board keeps the image, so it shows in any conversation after the file is gone. It takes a PNG, JPEG, GIF, or WebP of at most 8 MB from your worktree, your home, or the system temp folder. An image committed on a task's branch, linked by its path in the task's worktree, still shows in place, and so does one at an https address. Never put an image or any other file into a message as base64 or a data: URL: the board does not show it, and every reader of the conversation gets it in full in their prompt. To see an image a post, a brief, or a topic shows as `image:<id>`, call read_image with the id: it returns the path of a file of this turn's own that holds the image, removed when the turn ends, which you open with the tool that shows you an image, Read in Claude Code or view_image in Codex.",
|
|
10389
11080
|
"- Route every lesson: about you, your craft, or the user, write it to memory/core.md in your home directory, and keep that file short by moving detail to memory/<topic>.md, your archive; how a codebase behaves (architecture, conventions, gotchas, how to build, test, and verify it) goes in its AGENTS.md or README, committed on a task's branch like any change to the code, or proposed in the task's thread or the project channel when it is outside the task at hand; a durable fact about a project as a place (runners, access, flow, where results live) goes through `write_knowledge` on that project; something the whole project should know now, post it to the project channel, and anything about one task, in the task's thread.",
|
|
10390
11081
|
'- A procedure you have followed twice is a candidate for a skill, which it is when it makes a citizen better at a kind of work on any project: write it to skills/<name>/SKILL.md in your home, frontmatter with `name` and a one-line `description` and then the steps, and your skills index lists it from the next turn. Files the steps use, such as scripts/ and references/, go in the same folder, named in SKILL.md by paths relative to it. Propose it with kind "skill" when more than one citizen would use it and nothing in it is tied to one place; a skill only you use stays yours, however many projects you use it on.',
|
|
10391
11082
|
"- To take a published skill for yourself, install it in a temporary folder and copy it into your home: in a folder from `mktemp -d`, `npx -y skills add <source> --skill <name> -a universal --copy -y` writes `.agents/skills/<name>/` and a lock file there, and you copy that skill folder to skills/<name>/ in your home, named in lowercase letters, digits, and hyphens. Never install one inside a worktree, where it would be committed into the project, or with -g, which writes to the runner's own home, shared by every citizen on it and listed in no skills index.",
|
|
10392
|
-
"- Report memoryUpdated: true in the status object whenever you changed memory/core.md or a skill,
|
|
11083
|
+
"- Report memoryUpdated: true in the status object whenever you changed memory/core.md or a skill. A change to your instructions reaches each of your conversations at its next turn, whose prompt then carries them whole, replacing what that conversation had; a turn already running keeps what it started with.",
|
|
10393
11084
|
"- Other turns of yours may be running at the same time and share your home. Change a file there with your edit tool, which refuses a file changed since you read it, and never rewrite one whole from an earlier read, with a shell redirect or a script, or you erase what another of your turns wrote.",
|
|
10394
11085
|
"- To ask the user something, mention @user where the question belongs: the task's thread for a task, the proposal's thread for a proposal, and for anything else a thread you open on your scope's general channel, titled with the question. The user answers in the same thread, and the answer comes back to that thread's conversation. A proposal waiting on the user is already in front of them and needs no mention.",
|
|
10395
11086
|
"- End every turn with the status object: summary, claims held, what is blocked, and whether the user must decide. If you report that the user must decide, mentioned @user nowhere this turn, and proposed nothing for the user to decide, the runner asks in this turn's thread when it has one, and otherwise opens a thread on your scope's general channel with your summary as the question."
|
|
@@ -10398,9 +11089,9 @@ var PLANNING = [
|
|
|
10398
11089
|
"- A task is a plan of stages between open and done. Hold the current stage with claim_task and finish it with advance_task; the next stage becomes current, and past the last one the task is done.",
|
|
10399
11090
|
"- Name stages by what gets done. Plan the next few steps rather than everything, and reshape with plan_task as the work teaches you.",
|
|
10400
11091
|
"- Assign a stage to a role when anyone in it could do it, to a citizen only when it must be them, and to nobody when anyone in the project could. A citizen named on a stage must be a member of the task's project; add them first. A stage wakes everyone it is open to when it comes up, so the narrower the assignee, the fewer turns it costs.",
|
|
10401
|
-
"- Reshape rather than force: another round is an inserted stage, and a wait on something outside the society is a stage named for what it waits on. When a check finds work unfinished, the holder of the current stage sends the task back to an earlier stage with update_task, and so may the user, the steward, and the concierge; anyone else asks the concierge in the task's thread.",
|
|
10402
|
-
"- A task that cannot start until other tasks finish names them with update_task blocked_by. While any of them is in play,
|
|
10403
|
-
"- A gated stage is an independent check: nobody who held an earlier stage of the task may hold it, and only the user, the steward, and the concierge may add, remove, move, reassign, or ungate it. Ask them in the task's thread when a gate should change.",
|
|
11092
|
+
"- Reshape rather than force: another round is an inserted stage, and a wait on something outside the society is a stage named for what it waits on. When a check finds work unfinished, the holder of the current stage sends the task back to an earlier stage with update_task, and so may the user, the steward, and the concierge; anyone else asks the concierge in the task's thread, with a mention.",
|
|
11093
|
+
"- A task that cannot start until other tasks finish names them with update_task blocked_by, and whoever holds its stage then releases it, since the heartbeat still wakes a holder. While any of them is in play, a stage nobody holds wakes nobody, neither when it comes up nor on a heartbeat; once the last has ended, done or abandoned, the stage wakes as if it had just come up, or wakes its holder. Nobody needs to clear the block or mention anyone when they end; change blocked_by only when the plan changes.",
|
|
11094
|
+
"- A gated stage is an independent check: nobody who held an earlier stage of the task may hold it, and only the user, the steward, and the concierge may add, remove, move, reassign, or ungate it. Ask them in the task's thread, with a mention, when a gate should change.",
|
|
10404
11095
|
"- Commit your work on the task's branch before you advance, and say what you did in the advance note; it is the handover the next holder reads in the task's thread."
|
|
10405
11096
|
].join("\n");
|
|
10406
11097
|
var GOVERNANCE = [
|
|
@@ -10459,6 +11150,10 @@ function renderInstructions(input) {
|
|
|
10459
11150
|
return `${sections.join("\n")}
|
|
10460
11151
|
`;
|
|
10461
11152
|
}
|
|
11153
|
+
function instructionsHash(input) {
|
|
11154
|
+
const { onboarding: _first, ...kept } = input;
|
|
11155
|
+
return createHash3("sha256").update(renderInstructions(kept)).digest("hex");
|
|
11156
|
+
}
|
|
10462
11157
|
|
|
10463
11158
|
// packages/turn-host/dist/prompt.js
|
|
10464
11159
|
var REFLECTION_INTRO = "This turn is for your memory; take no new work, and answer unread messages only where a reply is needed. Read memory/core.md and your recent turn records, then:";
|
|
@@ -10467,9 +11162,11 @@ var REFLECTION_STEPS = [
|
|
|
10467
11162
|
"- Consolidate memory/core.md: keep it to lessons that still hold, about sixty lines at most, and move detail worth keeping to memory/<topic>.md in your home, which the board's search covers for you alone.",
|
|
10468
11163
|
'- Extract a skill: a procedure you have followed twice is a candidate, and one that makes a citizen better at a kind of work on any project goes to skills/<name>/SKILL.md, frontmatter with `name` and a one-line `description` and then the steps, with any script or reference it uses in the same folder. Propose it with kind "skill" when more than one citizen would use it and nothing in it is tied to one place; one only you use stays yours.',
|
|
10469
11164
|
"- Refresh profile.md: one paragraph on what you do well and what to send your way; the roster the front desk routes with is built from it.",
|
|
10470
|
-
"- Share: how a codebase behaves goes in its AGENTS.md or README through a task like any change to the code, so propose it in that task's thread or the project channel; a durable fact about this scope as a place (runners, access, flow, where results live) goes through write_knowledge; a norm the whole society should follow goes to the steward as a plain post in general."
|
|
10471
|
-
"- Report memoryUpdated: true in the status object when core.md or a skill changed."
|
|
11165
|
+
"- Share: how a codebase behaves goes in its AGENTS.md or README through a task like any change to the code, so propose it in that task's thread or the project channel; a durable fact about this scope as a place (runners, access, flow, where results live) goes through write_knowledge; a norm the whole society should follow goes to the steward as a plain post in general."
|
|
10472
11166
|
];
|
|
11167
|
+
var SAVE_WHAT_IS_OPEN = "- This conversation's session ends with this turn, and the next turn here starts a new one that knows only your home's files and the board: write anything still open into memory/core.md, or the archive it points to, before you end.";
|
|
11168
|
+
var REFLECTION_REPORT = "- Report memoryUpdated: true in the status object when core.md or a skill changed.";
|
|
11169
|
+
var INSTRUCTIONS_CHANGED = "Your instructions changed since this conversation last received them. Between the tags below are the whole of them now: they replace the instructions this conversation began with and any earlier text like this, so where the two differ, follow these, and a rule missing from them no longer applies.";
|
|
10473
11170
|
var MAX_BODY_CHARS = 1500;
|
|
10474
11171
|
var MAX_PROFILE_CHARS = 200;
|
|
10475
11172
|
function clip(text) {
|
|
@@ -10524,12 +11221,19 @@ function quotaPhrase(cli, quota, now) {
|
|
|
10524
11221
|
}
|
|
10525
11222
|
function runnerLine(runner, projects, now) {
|
|
10526
11223
|
const living = projects.filter((project) => project.runner === runner.name).map((project) => project.slug);
|
|
10527
|
-
const state =
|
|
11224
|
+
const state = [
|
|
11225
|
+
runner.status === "connected" ? "connected" : `away${runner.lastSeen === void 0 ? "" : ` since ${runner.lastSeen}`}`,
|
|
11226
|
+
...runner.paused === void 0 ? [] : [`paused by the user since ${runner.paused.at}`]
|
|
11227
|
+
].join(", ");
|
|
11228
|
+
const pauses = runner.clis.flatMap((cli) => {
|
|
11229
|
+
const pause = runner.pausedClis?.[cli];
|
|
11230
|
+
return pause === void 0 ? [] : [`${cli} paused by the user since ${pause.at}`];
|
|
11231
|
+
});
|
|
10528
11232
|
const quotas = runner.clis.flatMap((cli) => {
|
|
10529
11233
|
const quota = runner.quotas?.[cli];
|
|
10530
11234
|
return quota === void 0 ? [] : [quotaPhrase(cli, quota, now)];
|
|
10531
11235
|
});
|
|
10532
|
-
return `- ${runner.name}: ${state}; ${runner.os}; CLIs ${runner.clis.length === 0 ? "none" : runner.clis.join(", ")}; offers ${runner.capabilities.length === 0 ? "nothing beyond its CLIs" : runner.capabilities.join(", ")}; ${living.length === 0 ? "no projects" : `projects ${living.join(", ")}`}${quotas.length === 0 ? "" : `; quota ${quotas.join("; ")}`}`;
|
|
11236
|
+
return `- ${runner.name}: ${state}; ${runner.os}; CLIs ${runner.clis.length === 0 ? "none" : runner.clis.join(", ")}; offers ${runner.capabilities.length === 0 ? "nothing beyond its CLIs" : runner.capabilities.join(", ")}; ${living.length === 0 ? "no projects" : `projects ${living.join(", ")}`}${pauses.map((pause) => `; ${pause}`).join("")}${quotas.length === 0 ? "" : `; quota ${quotas.join("; ")}`}`;
|
|
10533
11237
|
}
|
|
10534
11238
|
function profileLine(profile) {
|
|
10535
11239
|
const line = profile.split("\n").map((entry) => entry.trim()).find((entry) => entry.length > 0 && !entry.startsWith("#"));
|
|
@@ -10566,7 +11270,7 @@ function messageLines(messages, threads) {
|
|
|
10566
11270
|
function cronLines(cron, timeZone) {
|
|
10567
11271
|
const previous = cron.lastRun === void 0 ? "" : ` The turn of its previous fire ${cron.lastRun.outcome === "stopped" ? "was stopped by the user" : cron.lastRun.outcome} at ${cron.lastRun.at}.`;
|
|
10568
11272
|
const next = nextFire(cron, timeZone);
|
|
10569
|
-
const state = isOneTime(cron.schedule) ? " It fired once, as set, and has ended." : cron.ended !== void 0 ? ` ${cron.ended.by} has ended it since it fired (${cron.ended.reason}), so do what it asks only if it still matters.` : next === null ? "" : ` Next it fires at ${next.toISOString()}.`;
|
|
11273
|
+
const state = isOneTime(cron.schedule) ? " It fired once, as set, and has ended." : cron.ended !== void 0 ? ` ${cron.ended.by} has ended it since it fired (${cron.ended.reason}), so do what it asks only if it still matters.` : cron.paused !== void 0 ? ` ${cron.paused.by} has paused it since it fired, so it fires no more until resumed.` : next === null ? "" : ` Next it fires at ${next.toISOString()}.`;
|
|
10570
11274
|
return [
|
|
10571
11275
|
`- "${cron.title}" (cron ${cron.id}), set by ${cron.createdBy} at ${cron.createdAt}, fires ${describeSchedule(cron.schedule, timeZone)}.${previous}${state}`,
|
|
10572
11276
|
"",
|
|
@@ -10620,6 +11324,10 @@ function buildTurnPrompt(input) {
|
|
|
10620
11324
|
if (dispatch.trigger.kind === "user_post") {
|
|
10621
11325
|
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.");
|
|
10622
11326
|
}
|
|
11327
|
+
const changed = input.changedInstructions ?? null;
|
|
11328
|
+
if (changed !== null) {
|
|
11329
|
+
lines.push("", "## Your instructions changed", "", INSTRUCTIONS_CHANGED, "", "<instructions>", changed.trimEnd(), "</instructions>");
|
|
11330
|
+
}
|
|
10623
11331
|
const conversation = input.conversation ?? null;
|
|
10624
11332
|
lines.push("", "## This conversation", "");
|
|
10625
11333
|
if (conversation === null && dispatch.channel !== void 0) {
|
|
@@ -10654,7 +11362,7 @@ function buildTurnPrompt(input) {
|
|
|
10654
11362
|
lines.push("", WORKSPACE_TOKEN);
|
|
10655
11363
|
const conflicts = input.conflicts ?? [];
|
|
10656
11364
|
if (dispatch.trigger.kind === "reflection") {
|
|
10657
|
-
lines.push("", "## Reflection", "", REFLECTION_INTRO, ...conflicts.length > 0 ? [RECONCILE] : [], ...REFLECTION_STEPS);
|
|
11365
|
+
lines.push("", "## Reflection", "", REFLECTION_INTRO, ...conflicts.length > 0 ? [RECONCILE] : [], ...REFLECTION_STEPS, ...input.afreshAfterReflection === true ? [SAVE_WHAT_IS_OPEN] : [], REFLECTION_REPORT);
|
|
10658
11366
|
}
|
|
10659
11367
|
if (input.onboarding !== null) {
|
|
10660
11368
|
lines.push("", "## First turn", "", renderOnboardingPreamble(input.onboarding));
|
|
@@ -10668,6 +11376,14 @@ function buildTurnPrompt(input) {
|
|
|
10668
11376
|
"Its CLI could not resume this conversation's session, so the session was dropped and this turn starts a new one: nothing of your earlier turns here carries over but what is on the board and in your home."
|
|
10669
11377
|
] : [], "The worktree may hold uncommitted changes. Run git status before doing anything else.");
|
|
10670
11378
|
}
|
|
11379
|
+
const failed = input.failedCrons ?? [];
|
|
11380
|
+
if (failed.length > 0) {
|
|
11381
|
+
const timeZone = input.clock?.timeZone ?? DEFAULT_TIME_ZONE;
|
|
11382
|
+
lines.push("", "## Cron fires that did not finish", "", failed.length === 1 ? "The turn of this cron's latest fire here failed, so what its note asks may not be done:" : "The turns of these crons' latest fires here failed, so what their notes ask may not be done:", "", ...failed.flatMap((cron) => cronLines(cron, timeZone)));
|
|
11383
|
+
}
|
|
11384
|
+
if (last?.trigger.kind === "reflection" && last.exitReason === "completed" && last.sessionDropped === true) {
|
|
11385
|
+
lines.push("", "## Your session started afresh", "", "Your last turn here was a reflection, and this conversation's session ended with it, so this turn starts a new one: what you kept is in memory/core.md and the archive it points to, and what your earlier turns did is on the board, in your posts and the threads and tasks you worked in, which search and the board's files reach.");
|
|
11386
|
+
}
|
|
10671
11387
|
const society = input.societyView;
|
|
10672
11388
|
if (society !== void 0 && society !== null) {
|
|
10673
11389
|
lines.push("", "## The society", "", `Each ask is a conversation of its own, and others may be in a turn at the same time. Before proposing a citizen, a role, or a project, look at the proposals still waiting under ${PATH_TOKENS.board}/society/proposals/: another ask may have proposed it already.`, "", "### Projects", "");
|
|
@@ -10684,7 +11400,7 @@ function buildTurnPrompt(input) {
|
|
|
10684
11400
|
}
|
|
10685
11401
|
const machines = input.runners;
|
|
10686
11402
|
if (machines !== void 0 && machines !== null) {
|
|
10687
|
-
lines.push("", "## Runners", "", "The machines turns run on. A project lives on the runner that takes its first turn, one that offers every capability the project and that turn's task require, and
|
|
11403
|
+
lines.push("", "## Runners", "", "The machines turns run on. A project lives on the runner that takes its first turn, one that offers every capability the project and that turn's task require, and moves only when the user moves it. A task's required_capabilities must name capabilities exactly as a runner below offers them; a task needing one its project's runner lacks waits. A runner's quota is what each CLI's login there last reported of its plan, in percent of each window; a CLI limited until a time fails its turns there until then. A runner, or a CLI on it, paused by the user starts none of the turns it would run until the user resumes it: they wait, and the stages their citizens hold keep their leases.", "");
|
|
10688
11404
|
if (machines.runners.length === 0) {
|
|
10689
11405
|
lines.push("None registered yet.");
|
|
10690
11406
|
}
|
|
@@ -10907,7 +11623,10 @@ var TurnHost = class {
|
|
|
10907
11623
|
const newSession = recorded === void 0;
|
|
10908
11624
|
const lastTurn = await this.board.readLastTurn(agent.name, dispatch.project, part);
|
|
10909
11625
|
const costSoFarUsd = !newSession && lastTurn?.session === recorded ? lastTurn.sessionCostUsd ?? lastTurn.costUsd : 0;
|
|
11626
|
+
const usageSoFar = !newSession && lastTurn?.session === recorded ? lastTurn.sessionUsage : void 0;
|
|
10910
11627
|
const resumeRetries = !newSession && lastTurn?.session === recorded && lastTurn.resumeFailed === "retry" ? lastTurn.resumeRetries ?? 1 : 0;
|
|
11628
|
+
const afreshAfterReflection = charter.societyScope && agent.role !== USER_ROLE && societyScope && part === void 0;
|
|
11629
|
+
const afterReflection = lastTurn?.trigger.kind === "reflection" && lastTurn.sessionDropped === true;
|
|
10911
11630
|
const digest = await this.board.readDigest({
|
|
10912
11631
|
...actor,
|
|
10913
11632
|
scope: dispatch.project,
|
|
@@ -10919,14 +11638,14 @@ var TurnHost = class {
|
|
|
10919
11638
|
const messages = threadSoFar ?? digest.messages;
|
|
10920
11639
|
const cursorAfter = threadSoFar?.at(-1)?.id ?? digest.cursor;
|
|
10921
11640
|
const held = task === null ? [] : (await this.board.heldClaims(agent.name)).filter((each) => each.id === task.id);
|
|
10922
|
-
const onboarding = dispatch.onboarding || newSession && part === void 0 ? {
|
|
11641
|
+
const onboarding = dispatch.onboarding || newSession && part === void 0 && !afterReflection ? {
|
|
10923
11642
|
agentName: agent.name,
|
|
10924
11643
|
roleSummary: charter.purpose,
|
|
10925
11644
|
project: dispatch.project,
|
|
10926
11645
|
worktree: PATH_TOKENS.worktree,
|
|
10927
11646
|
otherChannels: (project?.channels ?? []).map((name) => channelRef(dispatch.project, name)).filter((ref) => !agent.subscriptions.includes(ref))
|
|
10928
11647
|
} : null;
|
|
10929
|
-
const
|
|
11648
|
+
const rendered = {
|
|
10930
11649
|
agentName: agent.name,
|
|
10931
11650
|
roleCharter: await this.board.readAgentRoleBody(agent.name),
|
|
10932
11651
|
memoryCore: await this.board.readMemoryCore(agent.name),
|
|
@@ -10944,7 +11663,11 @@ var TurnHost = class {
|
|
|
10944
11663
|
}))
|
|
10945
11664
|
],
|
|
10946
11665
|
...onboarding === null ? {} : { onboarding }
|
|
10947
|
-
}
|
|
11666
|
+
};
|
|
11667
|
+
const instructions = renderInstructions(rendered);
|
|
11668
|
+
const instructionsNow = instructionsHash(rendered);
|
|
11669
|
+
const instructionsHeld = !newSession && lastTurn?.session === recorded ? lastTurn.sessionInstructionsHash : void 0;
|
|
11670
|
+
const changedInstructions = !newSession && instructionsHeld !== instructionsNow;
|
|
10948
11671
|
const societyView = charter.wakeTriggers.includes(USER_POST_TRIGGER2) ? {
|
|
10949
11672
|
projects: (await this.board.listProjects()).filter((each) => each.archived === void 0),
|
|
10950
11673
|
members: await this.board.listMembers()
|
|
@@ -10985,6 +11708,11 @@ var TurnHost = class {
|
|
|
10985
11708
|
crons.push(cron);
|
|
10986
11709
|
}
|
|
10987
11710
|
}
|
|
11711
|
+
const failedCrons = (await this.board.listCrons()).filter((cron) => cron.agent === agent.name && cron.scope === dispatch.project && conversationPart(cron) === part && cronFailure(cron) !== null && !(dispatch.crons ?? []).includes(cron.id));
|
|
11712
|
+
const cronFailures = failedCrons.flatMap((cron) => {
|
|
11713
|
+
const failure = cronFailure(cron);
|
|
11714
|
+
return failure === null ? [] : [{ cron: cron.id, turnId: failure.turnId }];
|
|
11715
|
+
});
|
|
10988
11716
|
const prompt = buildTurnPrompt({
|
|
10989
11717
|
dispatch,
|
|
10990
11718
|
clock: { now: this.now(), timeZone: society.timezone },
|
|
@@ -10997,7 +11725,10 @@ var TurnHost = class {
|
|
|
10997
11725
|
waitingStages: waiting,
|
|
10998
11726
|
project,
|
|
10999
11727
|
lastTurn,
|
|
11728
|
+
failedCrons,
|
|
11729
|
+
...changedInstructions ? { changedInstructions: instructions } : {},
|
|
11000
11730
|
onboarding,
|
|
11731
|
+
afreshAfterReflection,
|
|
11001
11732
|
societyView,
|
|
11002
11733
|
runners: runners2,
|
|
11003
11734
|
knowledge,
|
|
@@ -11010,7 +11741,10 @@ var TurnHost = class {
|
|
|
11010
11741
|
const record = await this.board.beginTurn({
|
|
11011
11742
|
...base,
|
|
11012
11743
|
session: recorded ?? null,
|
|
11013
|
-
sessionCostUsd: costSoFarUsd
|
|
11744
|
+
sessionCostUsd: costSoFarUsd,
|
|
11745
|
+
...usageSoFar === void 0 ? {} : { sessionUsage: usageSoFar },
|
|
11746
|
+
...instructionsHeld === void 0 ? {} : { sessionInstructionsHash: instructionsHeld },
|
|
11747
|
+
...cronFailures.length === 0 ? {} : { cronFailures }
|
|
11014
11748
|
});
|
|
11015
11749
|
if (record.id === void 0) {
|
|
11016
11750
|
throw new Error("the board gave the turn no id");
|
|
@@ -11028,7 +11762,9 @@ var TurnHost = class {
|
|
|
11028
11762
|
...place === void 0 ? {} : { place },
|
|
11029
11763
|
session: recorded ?? null,
|
|
11030
11764
|
costSoFarUsd,
|
|
11765
|
+
...usageSoFar === void 0 ? {} : { usageSoFar },
|
|
11031
11766
|
instructions,
|
|
11767
|
+
instructionsHash: instructionsNow,
|
|
11032
11768
|
prompt,
|
|
11033
11769
|
mcp: { url: this.mcpUrl, token },
|
|
11034
11770
|
limits: { timeoutMs: this.turnTimeoutMs, maxTurns: this.maxTurns },
|
|
@@ -11052,11 +11788,13 @@ var TurnHost = class {
|
|
|
11052
11788
|
stopRequestedBy: null,
|
|
11053
11789
|
held: held.map((each) => each.id),
|
|
11054
11790
|
costSoFarUsd,
|
|
11791
|
+
instructionsNow,
|
|
11055
11792
|
resumeRetries,
|
|
11056
11793
|
configuredModel: agent.model ?? null,
|
|
11057
11794
|
transcript: [],
|
|
11058
11795
|
coldToken: residentKey === null ? token : null,
|
|
11059
11796
|
residentKey: residentKey === null ? null : `${runner.name}|${residentKey}`,
|
|
11797
|
+
afreshAfterReflection,
|
|
11060
11798
|
keepLeases,
|
|
11061
11799
|
done,
|
|
11062
11800
|
fail
|
|
@@ -11220,8 +11958,13 @@ var TurnHost = class {
|
|
|
11220
11958
|
const part = conversationPart(job);
|
|
11221
11959
|
const resumeRetries = outcome.resumeFailed === "retry" ? turn.resumeRetries + 1 : void 0;
|
|
11222
11960
|
const dropSession = job.session !== null && (outcome.resumeFailed === "lost" || (resumeRetries ?? 0) >= RESUME_RETRIES);
|
|
11961
|
+
const reflected = turn.afreshAfterReflection && turn.record.trigger.kind === "reflection" && outcome.exitReason === "completed";
|
|
11962
|
+
const { sessionUsage: _started, sessionInstructionsHash: heldBefore, ...begun } = turn.record;
|
|
11963
|
+
const promptDelivered = outcome.exitReason === "completed" || outcome.exitReason === "blocked" || outcome.exitReason === "stopped";
|
|
11964
|
+
const instructionsTaken = outcome.exitReason === "stopped" ? outcome.contextTokens !== void 0 : promptDelivered;
|
|
11965
|
+
const instructionsHeld = job.session === null ? outcome.session === null ? void 0 : turn.instructionsNow : instructionsTaken ? turn.instructionsNow : heldBefore;
|
|
11223
11966
|
const finished = {
|
|
11224
|
-
...
|
|
11967
|
+
...begun,
|
|
11225
11968
|
session: outcome.session,
|
|
11226
11969
|
endedAt: this.now().toISOString(),
|
|
11227
11970
|
exitReason: outcome.exitReason,
|
|
@@ -11230,6 +11973,9 @@ var TurnHost = class {
|
|
|
11230
11973
|
usage: outcome.usage,
|
|
11231
11974
|
costUsd: outcome.costUsd,
|
|
11232
11975
|
sessionCostUsd: outcome.sessionCostUsd ?? turn.costSoFarUsd,
|
|
11976
|
+
...outcome.sessionUsage === void 0 ? {} : { sessionUsage: outcome.sessionUsage },
|
|
11977
|
+
...instructionsHeld === void 0 ? {} : { sessionInstructionsHash: instructionsHeld },
|
|
11978
|
+
...outcome.contextTokens === void 0 ? {} : { contextTokens: outcome.contextTokens },
|
|
11233
11979
|
toolCalls: turn.transcript.filter((entry) => entry.event.type === "tool_call").length,
|
|
11234
11980
|
model: outcome.model ?? turn.configuredModel,
|
|
11235
11981
|
...outcome.work === null ? {} : { work: outcome.work },
|
|
@@ -11237,7 +11983,7 @@ var TurnHost = class {
|
|
|
11237
11983
|
...outcome.resumeFailed === void 0 ? {} : { resumeFailed: outcome.resumeFailed },
|
|
11238
11984
|
...outcome.transient === true ? { transient: true } : {},
|
|
11239
11985
|
...resumeRetries === void 0 ? {} : { resumeRetries },
|
|
11240
|
-
...dropSession ? { sessionDropped: true } : {}
|
|
11986
|
+
...dropSession || reflected ? { sessionDropped: true } : {}
|
|
11241
11987
|
};
|
|
11242
11988
|
const ended = await this.conversationEnded(job);
|
|
11243
11989
|
const closing = turn.record.trigger.kind === "closing";
|
|
@@ -11255,10 +12001,15 @@ var TurnHost = class {
|
|
|
11255
12001
|
resumeFailed: outcome.resumeFailed,
|
|
11256
12002
|
resumeRetries
|
|
11257
12003
|
}, "dropped a session its CLI could not resume");
|
|
12004
|
+
} else if (reflected) {
|
|
12005
|
+
if (job.session !== null) {
|
|
12006
|
+
await this.board.dropSession(job.agent, job.scope, job.cli, job.session, part);
|
|
12007
|
+
}
|
|
12008
|
+
this.log.info({ agent: job.agent, project: job.scope, session: outcome.session }, "dropped a home session after its reflection");
|
|
11258
12009
|
} else if (outcome.session !== null && outcome.session !== job.session) {
|
|
11259
12010
|
await this.board.writeSession(job.agent, job.scope, job.cli, outcome.session, turn.runner, part);
|
|
11260
12011
|
}
|
|
11261
|
-
if (
|
|
12012
|
+
if (promptDelivered) {
|
|
11262
12013
|
await this.board.setDigestCursor(job.agent, job.scope, turn.delivered, part);
|
|
11263
12014
|
await this.renewLeases(turn.actor, turn.held);
|
|
11264
12015
|
if (outcome.status?.needsUserDecision === true) {
|
|
@@ -11550,6 +12301,8 @@ var RunnerProtocolError = class extends Error {
|
|
|
11550
12301
|
};
|
|
11551
12302
|
var RunnerAwayError = class extends Error {
|
|
11552
12303
|
};
|
|
12304
|
+
var MoveSourceAwayError = class extends Error {
|
|
12305
|
+
};
|
|
11553
12306
|
var SILENT2 = { info() {
|
|
11554
12307
|
}, warn() {
|
|
11555
12308
|
}, error() {
|
|
@@ -11582,6 +12335,12 @@ var RunnerHub = class {
|
|
|
11582
12335
|
/** Disconnects being recorded, which `close` waits for. */
|
|
11583
12336
|
detaching = /* @__PURE__ */ new Set();
|
|
11584
12337
|
runnerListeners = /* @__PURE__ */ new Set();
|
|
12338
|
+
/** The project being moved, by slug: none of its turns is placed and nothing lands there until it ends. */
|
|
12339
|
+
moving = /* @__PURE__ */ new Map();
|
|
12340
|
+
/** Turns of each project placed on a runner and not yet opened, which the board cannot see yet. */
|
|
12341
|
+
starting = /* @__PURE__ */ new Map();
|
|
12342
|
+
/** Moves started since the hub did, so a placement that a move overtook gives its turn back. */
|
|
12343
|
+
moveStarts = 0;
|
|
11585
12344
|
constructor(options) {
|
|
11586
12345
|
this.board = options.board;
|
|
11587
12346
|
this.host = options.host;
|
|
@@ -11853,6 +12612,7 @@ var RunnerHub = class {
|
|
|
11853
12612
|
return accepted ? "sent" : "refused";
|
|
11854
12613
|
}
|
|
11855
12614
|
async assign(dispatch) {
|
|
12615
|
+
const starts = this.moveStarts;
|
|
11856
12616
|
const agent = await this.board.readAgent(dispatch.agent).catch(() => null);
|
|
11857
12617
|
if (agent === null) {
|
|
11858
12618
|
return { refused: `no agent ${dispatch.agent}` };
|
|
@@ -11864,11 +12624,15 @@ var RunnerHub = class {
|
|
|
11864
12624
|
return { refused: `${agent.name} has no CLI binding` };
|
|
11865
12625
|
}
|
|
11866
12626
|
const cli = agent.cli;
|
|
11867
|
-
const
|
|
12627
|
+
const runners2 = await this.board.listRunners();
|
|
12628
|
+
const free = (seat) => seat.send !== null && !seat.unconfirmed && seat.hello.clis.includes(cli) && (seat.hello.slots === null || seat.inUse < seat.hello.slots) && !heldOn(runners2, seat.name, cli);
|
|
11868
12629
|
let chosen;
|
|
11869
12630
|
if (dispatch.project === SOCIETY_SCOPE) {
|
|
11870
12631
|
chosen = await this.pinnedSeat(agent.name, agent.homeRunner, cli, free);
|
|
11871
12632
|
} else {
|
|
12633
|
+
if (this.moving.has(dispatch.project)) {
|
|
12634
|
+
return null;
|
|
12635
|
+
}
|
|
11872
12636
|
const project = await this.board.readProject(dispatch.project);
|
|
11873
12637
|
let home = project.runner;
|
|
11874
12638
|
if (home === void 0) {
|
|
@@ -11895,7 +12659,13 @@ var RunnerHub = class {
|
|
|
11895
12659
|
if (chosen === void 0) {
|
|
11896
12660
|
return null;
|
|
11897
12661
|
}
|
|
12662
|
+
if (dispatch.project !== SOCIETY_SCOPE && (this.moving.has(dispatch.project) || this.moveStarts !== starts)) {
|
|
12663
|
+
return null;
|
|
12664
|
+
}
|
|
11898
12665
|
chosen.inUse += 1;
|
|
12666
|
+
if (dispatch.project !== SOCIETY_SCOPE) {
|
|
12667
|
+
this.starting.set(dispatch.project, (this.starting.get(dispatch.project) ?? 0) + 1);
|
|
12668
|
+
}
|
|
11899
12669
|
return { runner: chosen.name };
|
|
11900
12670
|
}
|
|
11901
12671
|
async runTurn(dispatch, assignment) {
|
|
@@ -11904,11 +12674,23 @@ var RunnerHub = class {
|
|
|
11904
12674
|
}
|
|
11905
12675
|
const seat = this.seats.get(assignment.runner);
|
|
11906
12676
|
let turnId = null;
|
|
12677
|
+
let placed = dispatch.project !== SOCIETY_SCOPE;
|
|
12678
|
+
const opening = () => {
|
|
12679
|
+
if (placed) {
|
|
12680
|
+
placed = false;
|
|
12681
|
+
const left = (this.starting.get(dispatch.project) ?? 1) - 1;
|
|
12682
|
+
if (left > 0) {
|
|
12683
|
+
this.starting.set(dispatch.project, left);
|
|
12684
|
+
} else {
|
|
12685
|
+
this.starting.delete(dispatch.project);
|
|
12686
|
+
}
|
|
12687
|
+
}
|
|
12688
|
+
};
|
|
11907
12689
|
try {
|
|
11908
12690
|
if (seat === void 0) {
|
|
11909
12691
|
return await this.host.refuseTurn(dispatch, assignment.runner, "the runner is gone");
|
|
11910
12692
|
}
|
|
11911
|
-
const opened = await this.host.openTurn(dispatch, this.seatView(seat));
|
|
12693
|
+
const opened = await this.host.openTurn(dispatch, this.seatView(seat)).finally(opening);
|
|
11912
12694
|
if ("failed" in opened) {
|
|
11913
12695
|
return opened.failed;
|
|
11914
12696
|
}
|
|
@@ -11920,6 +12702,7 @@ var RunnerHub = class {
|
|
|
11920
12702
|
turnId = opened.job.turnId;
|
|
11921
12703
|
return await opened.ended;
|
|
11922
12704
|
} finally {
|
|
12705
|
+
opening();
|
|
11923
12706
|
if (seat !== void 0) {
|
|
11924
12707
|
seat.inUse = Math.max(0, seat.inUse - 1);
|
|
11925
12708
|
if (turnId !== null) {
|
|
@@ -11952,6 +12735,9 @@ var RunnerHub = class {
|
|
|
11952
12735
|
});
|
|
11953
12736
|
return "done";
|
|
11954
12737
|
}
|
|
12738
|
+
if (this.moving.has(project)) {
|
|
12739
|
+
return "deferred";
|
|
12740
|
+
}
|
|
11955
12741
|
let outcome = { ok: true, detail: "nothing to land, since the task left no branch" };
|
|
11956
12742
|
if (record.runner !== void 0) {
|
|
11957
12743
|
const seat = this.seats.get(record.runner);
|
|
@@ -12004,6 +12790,9 @@ var RunnerHub = class {
|
|
|
12004
12790
|
if (task.status !== "abandoned" || task.onDone !== "ghpr" || pull === void 0 || pull.closed !== void 0) {
|
|
12005
12791
|
return "done";
|
|
12006
12792
|
}
|
|
12793
|
+
if (this.moving.has(project)) {
|
|
12794
|
+
return "deferred";
|
|
12795
|
+
}
|
|
12007
12796
|
const record = await this.board.readProject(project);
|
|
12008
12797
|
let outcome = {
|
|
12009
12798
|
ok: false,
|
|
@@ -12036,16 +12825,172 @@ var RunnerHub = class {
|
|
|
12036
12825
|
});
|
|
12037
12826
|
return "done";
|
|
12038
12827
|
}
|
|
12828
|
+
/**
|
|
12829
|
+
* Moves a project to another runner, which only the user does, carrying every branch and tag of
|
|
12830
|
+
* its repository: the project's turns, landings, and pull-request closes wait while it runs.
|
|
12831
|
+
* The old runner pushes to a repository on the server once no worktree of the project holds work
|
|
12832
|
+
* no branch does, the new runner clones it, and the board records the new runner only once that
|
|
12833
|
+
* runner holds every branch and tag at the commit the old one had; until then the project works
|
|
12834
|
+
* where it was. The server's repository goes as the move ends, and the old runner then removes
|
|
12835
|
+
* its copy. With the old runner away nothing can travel, so the move goes ahead only when the
|
|
12836
|
+
* user says `withoutBranches`, and the branches stay there.
|
|
12837
|
+
*/
|
|
12838
|
+
async moveProject(actor, slug, to, options = {}) {
|
|
12839
|
+
const under = [...this.moving.keys()];
|
|
12840
|
+
if (under.length > 0) {
|
|
12841
|
+
throw new BoardError("INVALID_STATE", `a move of ${under.join(", ")} is under way; try again once it ends`);
|
|
12842
|
+
}
|
|
12843
|
+
const move = { id: null, from: null, to, phase: "checking" };
|
|
12844
|
+
this.moving.set(slug, move);
|
|
12845
|
+
this.moveStarts += 1;
|
|
12846
|
+
let source;
|
|
12847
|
+
let moved;
|
|
12848
|
+
try {
|
|
12849
|
+
if ((this.starting.get(slug) ?? 0) > 0) {
|
|
12850
|
+
throw new BoardError("INVALID_STATE", `a turn of ${slug} is starting; try again once it has started and ended`);
|
|
12851
|
+
}
|
|
12852
|
+
const { project, inPlay: inPlay2 } = await this.board.prepareProjectMove(actor, slug, to);
|
|
12853
|
+
const from = project.runner;
|
|
12854
|
+
move.from = from;
|
|
12855
|
+
const target = this.seats.get(to);
|
|
12856
|
+
if (target === void 0 || target.send === null) {
|
|
12857
|
+
throw new BoardError("INVALID_STATE", `runner ${to} is not connected`);
|
|
12858
|
+
}
|
|
12859
|
+
await this.assertCanHost(target, project, inPlay2);
|
|
12860
|
+
source = this.seats.get(from);
|
|
12861
|
+
if (source === void 0 || source.send === null) {
|
|
12862
|
+
if (options.withoutBranches !== true) {
|
|
12863
|
+
throw new MoveSourceAwayError(sourceAwayMessage(project, from, to, inPlay2));
|
|
12864
|
+
}
|
|
12865
|
+
move.phase = "recording";
|
|
12866
|
+
const recorded2 = await this.board.recordProjectMove(actor, {
|
|
12867
|
+
slug,
|
|
12868
|
+
from,
|
|
12869
|
+
to,
|
|
12870
|
+
carried: null
|
|
12871
|
+
});
|
|
12872
|
+
this.log.info({ project: slug, from, to }, "project moved without its branches");
|
|
12873
|
+
return { project: recorded2, carried: null, released: null };
|
|
12874
|
+
}
|
|
12875
|
+
if (options.withoutBranches === true) {
|
|
12876
|
+
throw new BoardError("INVALID_STATE", `runner ${from}, where ${slug} lives, is connected, so the move carries its branches`);
|
|
12877
|
+
}
|
|
12878
|
+
if (!source.hello.projectMoves) {
|
|
12879
|
+
throw new BoardError("INVALID_STATE", `runner ${from}, where ${slug} lives, cannot carry a project yet; upgrade it`);
|
|
12880
|
+
}
|
|
12881
|
+
const repo = projectRepo(project);
|
|
12882
|
+
move.id = await this.board.openMoveRepo();
|
|
12883
|
+
const id = move.id;
|
|
12884
|
+
move.phase = "export";
|
|
12885
|
+
const exported = ProjectExportedSchema.parse(await this.request(source, (request) => ({ type: "export", request, export: { move: id, repo } }), moveTransferMs(MOVE_PACK_LIMIT_BYTES) + this.requestTimeoutMs).catch((error) => {
|
|
12886
|
+
throw new BoardError("INVALID_STATE", `${from} could not send ${slug}: ${describe(error)}`);
|
|
12887
|
+
}));
|
|
12888
|
+
if (!exported.ok) {
|
|
12889
|
+
throw new BoardError("INVALID_STATE", exported.held.length === 0 ? `${slug}'s repository on ${from} is ${mebibytes(exported.bytes)}, more than a move carries (${mebibytes(MOVE_PACK_LIMIT_BYTES)})` : `${slug} cannot be moved while work no branch holds sits in its worktrees on ${from}: ${exported.held.map(heldLine).join("; ")}. Have each citizen commit it or clear it, then move again`);
|
|
12890
|
+
}
|
|
12891
|
+
move.phase = "import";
|
|
12892
|
+
const imported = ProjectImportedSchema.parse(await this.request(target, (request) => ({
|
|
12893
|
+
type: "import",
|
|
12894
|
+
request,
|
|
12895
|
+
import: {
|
|
12896
|
+
move: id,
|
|
12897
|
+
repo,
|
|
12898
|
+
pullRequests: project.onDone === "ghpr",
|
|
12899
|
+
bytes: exported.bytes
|
|
12900
|
+
}
|
|
12901
|
+
}), moveTransferMs(exported.bytes) + this.requestTimeoutMs).catch((error) => {
|
|
12902
|
+
throw new BoardError("INVALID_STATE", `${to} could not take ${slug}: ${describe(error)}`);
|
|
12903
|
+
}));
|
|
12904
|
+
move.phase = "recording";
|
|
12905
|
+
const differ = differentRefs(exported.refs, imported.refs);
|
|
12906
|
+
if (differ.length > 0) {
|
|
12907
|
+
await this.releaseOn(target, slug);
|
|
12908
|
+
throw new BoardError("INVALID_STATE", `${slug} arrived on ${to} unlike it left ${from} (${differ.slice(0, 5).join(", ")}${differ.length > 5 ? ", \u2026" : ""}); it stays on ${from}`);
|
|
12909
|
+
}
|
|
12910
|
+
const carried = Object.keys(exported.refs).length;
|
|
12911
|
+
const recorded = await this.board.recordProjectMove(actor, { slug, from, to, carried }).catch(async (error) => {
|
|
12912
|
+
await this.releaseOn(target, slug);
|
|
12913
|
+
throw error;
|
|
12914
|
+
});
|
|
12915
|
+
this.log.info({ project: slug, from, to, carried }, "project moved");
|
|
12916
|
+
moved = { project: recorded, carried };
|
|
12917
|
+
} finally {
|
|
12918
|
+
this.moving.delete(slug);
|
|
12919
|
+
if (move.id !== null) {
|
|
12920
|
+
const id = move.id;
|
|
12921
|
+
await this.board.dropMoveRepo(id).catch((error) => {
|
|
12922
|
+
this.log.warn({ move: id, error: String(error) }, "could not remove a move's repository");
|
|
12923
|
+
});
|
|
12924
|
+
}
|
|
12925
|
+
}
|
|
12926
|
+
return { ...moved, released: await this.releaseOn(source, slug) };
|
|
12927
|
+
}
|
|
12928
|
+
/**
|
|
12929
|
+
* Whether a runner may reach a move's repository on the server: the old runner to push while it
|
|
12930
|
+
* exports, the new one to fetch while it imports. `unknown` for a move that is not under way.
|
|
12931
|
+
*/
|
|
12932
|
+
moveAccess(runner, id, push) {
|
|
12933
|
+
const move = [...this.moving.values()].find((each) => each.id !== null && each.id === id);
|
|
12934
|
+
if (move === void 0) {
|
|
12935
|
+
return "unknown";
|
|
12936
|
+
}
|
|
12937
|
+
const allowed = push ? move.phase === "export" && runner === move.from : move.phase === "import" && runner === move.to;
|
|
12938
|
+
return allowed ? "allowed" : "refused";
|
|
12939
|
+
}
|
|
12940
|
+
/** Refuses a runner that cannot take a project's turns: its members' CLIs, the capabilities, a git identity. */
|
|
12941
|
+
async assertCanHost(seat, project, inPlay2) {
|
|
12942
|
+
if (!seat.hello.projectMoves) {
|
|
12943
|
+
throw new BoardError("INVALID_STATE", `runner ${seat.name} cannot take a project yet; upgrade it`);
|
|
12944
|
+
}
|
|
12945
|
+
const lacking = [];
|
|
12946
|
+
for (const name of project.members) {
|
|
12947
|
+
const agent = await this.board.readAgent(name).catch(() => null);
|
|
12948
|
+
if (agent !== null && agent.status === "active" && agent.cli !== null && !seat.hello.clis.includes(agent.cli)) {
|
|
12949
|
+
lacking.push(`${agent.cli} for ${agent.name}`);
|
|
12950
|
+
}
|
|
12951
|
+
}
|
|
12952
|
+
const needs = /* @__PURE__ */ new Set([
|
|
12953
|
+
...project.requiredCapabilities,
|
|
12954
|
+
...inPlay2.flatMap((task) => task.requiredCapabilities)
|
|
12955
|
+
]);
|
|
12956
|
+
const missing = [...needs].filter((each) => !seat.hello.capabilities.includes(each));
|
|
12957
|
+
if (missing.length > 0) {
|
|
12958
|
+
lacking.push(`the ${missing.length === 1 ? "capability" : "capabilities"} ${missing.join(", ")}`);
|
|
12959
|
+
}
|
|
12960
|
+
const identity = seat.hello.gitIdentityMissing ?? [];
|
|
12961
|
+
if (identity.length > 0) {
|
|
12962
|
+
lacking.push(`a git ${identity.join(" and ")} for citizens' commits`);
|
|
12963
|
+
}
|
|
12964
|
+
if (lacking.length > 0) {
|
|
12965
|
+
throw new BoardError("INVALID_STATE", `runner ${seat.name} cannot take ${project.slug}: it lacks ${lacking.join("; ")}`);
|
|
12966
|
+
}
|
|
12967
|
+
}
|
|
12968
|
+
/** Asks a runner to remove its copy of a project that lives elsewhere now; how that went, never a throw. */
|
|
12969
|
+
async releaseOn(seat, slug) {
|
|
12970
|
+
if (seat === void 0) {
|
|
12971
|
+
return null;
|
|
12972
|
+
}
|
|
12973
|
+
try {
|
|
12974
|
+
const released = ProjectReleasedSchema.parse(await this.request(seat, (request) => ({ type: "release", request, release: { slug } }), SWEEP_TIMEOUT_MS));
|
|
12975
|
+
return {
|
|
12976
|
+
ok: true,
|
|
12977
|
+
detail: `${seat.name} removed ${released.worktrees} worktree(s)${released.clone ? " and its clone" : ""}`
|
|
12978
|
+
};
|
|
12979
|
+
} catch (error) {
|
|
12980
|
+
this.log.warn({ runner: seat.name, project: slug, error: String(error) }, "could not release a project");
|
|
12981
|
+
return { ok: false, detail: `${seat.name} kept its copy of ${slug}: ${describe(error)}` };
|
|
12982
|
+
}
|
|
12983
|
+
}
|
|
12039
12984
|
/**
|
|
12040
12985
|
* One path on a branch of a project's repository, read on the runner the project lives on.
|
|
12041
12986
|
* NOT_FOUND when the project has no runner yet or the branch or the path does not exist, and
|
|
12042
12987
|
* `RunnerAwayError` while its runner is away.
|
|
12043
12988
|
*/
|
|
12044
|
-
async readBranch(project, branch,
|
|
12989
|
+
async readBranch(project, branch, path12) {
|
|
12045
12990
|
const { seat, repo } = await this.projectSeat(project, "branchReads", "read files");
|
|
12046
|
-
const file = BranchFileSchema.nullable().parse(await this.request(seat, (request) => ({ type: "file", request, read: { repo, branch, path:
|
|
12991
|
+
const file = BranchFileSchema.nullable().parse(await this.request(seat, (request) => ({ type: "file", request, read: { repo, branch, path: path12 } }), FILE_TIMEOUT_MS));
|
|
12047
12992
|
if (file === null) {
|
|
12048
|
-
throw new BoardError("NOT_FOUND", `${branch} has no ${
|
|
12993
|
+
throw new BoardError("NOT_FOUND", `${branch} has no ${path12 === "" ? "commit" : path12}`);
|
|
12049
12994
|
}
|
|
12050
12995
|
return file;
|
|
12051
12996
|
}
|
|
@@ -12065,11 +13010,11 @@ var RunnerHub = class {
|
|
|
12065
13010
|
* What a branch of a project's repository did to one file since it left the default branch, read
|
|
12066
13011
|
* on the runner the project lives on, with the same refusals as `branchChanges`.
|
|
12067
13012
|
*/
|
|
12068
|
-
async branchDiff(project, branch,
|
|
13013
|
+
async branchDiff(project, branch, path12) {
|
|
12069
13014
|
const { seat, repo } = await this.projectSeat(project, "branchChanges", "list changes");
|
|
12070
|
-
const diff = BranchFileDiffSchema.nullable().parse(await this.request(seat, (request) => ({ type: "diff", request, read: { repo, branch, path:
|
|
13015
|
+
const diff = BranchFileDiffSchema.nullable().parse(await this.request(seat, (request) => ({ type: "diff", request, read: { repo, branch, path: path12 } }), FILE_TIMEOUT_MS));
|
|
12071
13016
|
if (diff === null) {
|
|
12072
|
-
throw new BoardError("NOT_FOUND", `${branch} did not change ${
|
|
13017
|
+
throw new BoardError("NOT_FOUND", `${branch} did not change ${path12}`);
|
|
12073
13018
|
}
|
|
12074
13019
|
return diff;
|
|
12075
13020
|
}
|
|
@@ -12080,18 +13025,8 @@ var RunnerHub = class {
|
|
|
12080
13025
|
* back as a `BoardError` in the runner's words.
|
|
12081
13026
|
*/
|
|
12082
13027
|
async readTurnImage(actor, file) {
|
|
12083
|
-
const
|
|
12084
|
-
|
|
12085
|
-
throw new BoardError("INVALID_STATE", `${actor.name} has no turn running in this conversation to read ${file} from`);
|
|
12086
|
-
}
|
|
12087
|
-
const seat = this.seats.get(turn.runner);
|
|
12088
|
-
if (seat === void 0 || seat.send === null) {
|
|
12089
|
-
throw new RunnerAwayError(`runner ${turn.runner}, where the turn runs, is not connected`);
|
|
12090
|
-
}
|
|
12091
|
-
if (!seat.hello.imageReads) {
|
|
12092
|
-
throw new BoardError("INVALID_STATE", `runner ${seat.name}, where the turn runs, cannot read images yet; upgrade it`);
|
|
12093
|
-
}
|
|
12094
|
-
const image = TurnImageSchema.nullable().parse(await this.request(seat, (request) => ({ type: "image", request, read: { turnId: turn.turnId, path: file } }), FILE_TIMEOUT_MS));
|
|
13028
|
+
const { turnId, seat } = this.turnSeat(actor, "imageReads", `read ${file} from`, "read images");
|
|
13029
|
+
const image = TurnImageSchema.nullable().parse(await this.request(seat, (request) => ({ type: "image", request, read: { turnId, path: file } }), FILE_TIMEOUT_MS));
|
|
12095
13030
|
if (image === null) {
|
|
12096
13031
|
throw new BoardError("INVALID_STATE", `the turn is no longer running on runner ${seat.name}`);
|
|
12097
13032
|
}
|
|
@@ -12100,6 +13035,28 @@ var RunnerHub = class {
|
|
|
12100
13035
|
}
|
|
12101
13036
|
return Buffer.from(image.content, "base64");
|
|
12102
13037
|
}
|
|
13038
|
+
/**
|
|
13039
|
+
* Hands an image the board keeps to the runner a citizen's turn runs on, for `read_image`, which
|
|
13040
|
+
* writes it to a folder of that turn's own in the system temp folder, removed when the turn
|
|
13041
|
+
* ends, and answers the file's path there, with the same refusals as `readTurnImage`.
|
|
13042
|
+
*/
|
|
13043
|
+
async writeTurnImage(actor, image) {
|
|
13044
|
+
const { turnId, seat } = this.turnSeat(actor, "imageWrites", "open an image in", "open images");
|
|
13045
|
+
const file = TurnImageFileSchema.nullable().parse(await this.request(seat, (request) => ({
|
|
13046
|
+
type: "image_write",
|
|
13047
|
+
request,
|
|
13048
|
+
write: {
|
|
13049
|
+
turnId,
|
|
13050
|
+
id: image.id,
|
|
13051
|
+
type: image.type,
|
|
13052
|
+
content: Buffer.from(image.bytes).toString("base64")
|
|
13053
|
+
}
|
|
13054
|
+
}), FILE_TIMEOUT_MS));
|
|
13055
|
+
if (file === null) {
|
|
13056
|
+
throw new BoardError("INVALID_STATE", `the turn is no longer running on runner ${seat.name}`);
|
|
13057
|
+
}
|
|
13058
|
+
return file.path;
|
|
13059
|
+
}
|
|
12103
13060
|
/** The models a CLI offers, asked of a connected runner that has it; none when no runner has it. */
|
|
12104
13061
|
/**
|
|
12105
13062
|
* The connected runner a model list for `cli` comes from: the first of `prefer` that has the CLI,
|
|
@@ -12277,6 +13234,25 @@ var RunnerHub = class {
|
|
|
12277
13234
|
repo: projectRepo(record)
|
|
12278
13235
|
};
|
|
12279
13236
|
}
|
|
13237
|
+
/**
|
|
13238
|
+
* The caller's turn in flight, found by its conversation, and the connected runner it runs on,
|
|
13239
|
+
* which must say `able` in its hello: an image's way to and from the machine a turn runs on.
|
|
13240
|
+
* `where` and `what` say what the caller asked for in a refusal.
|
|
13241
|
+
*/
|
|
13242
|
+
turnSeat(actor, able, where, what) {
|
|
13243
|
+
const turn = this.host.runningTurns().find((each) => each.agent === actor.name && each.scope === actor.scope && each.thread === actor.thread && each.channel === actor.channel && each.place === actor.place);
|
|
13244
|
+
if (turn === void 0) {
|
|
13245
|
+
throw new BoardError("INVALID_STATE", `${actor.name} has no turn running in this conversation to ${where}`);
|
|
13246
|
+
}
|
|
13247
|
+
const seat = this.seats.get(turn.runner);
|
|
13248
|
+
if (seat === void 0 || seat.send === null) {
|
|
13249
|
+
throw new RunnerAwayError(`runner ${turn.runner}, where the turn runs, is not connected`);
|
|
13250
|
+
}
|
|
13251
|
+
if (!seat.hello[able]) {
|
|
13252
|
+
throw new BoardError("INVALID_STATE", `runner ${seat.name}, where the turn runs, cannot ${what} yet; upgrade it`);
|
|
13253
|
+
}
|
|
13254
|
+
return { turnId: turn.turnId, seat };
|
|
13255
|
+
}
|
|
12280
13256
|
assertOwns(runner, turnId) {
|
|
12281
13257
|
const owner = this.host.runnerOf(turnId);
|
|
12282
13258
|
if (owner !== void 0 && owner !== runner) {
|
|
@@ -12306,6 +13282,32 @@ var RunnerHub = class {
|
|
|
12306
13282
|
});
|
|
12307
13283
|
}
|
|
12308
13284
|
};
|
|
13285
|
+
function describe(error) {
|
|
13286
|
+
return error instanceof Error ? error.message : String(error);
|
|
13287
|
+
}
|
|
13288
|
+
function mebibytes(bytes) {
|
|
13289
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
13290
|
+
}
|
|
13291
|
+
function heldLine(held) {
|
|
13292
|
+
const where = held.conversation === null ? "its own worktree" : held.conversation.kind === "thread" ? `thread ${held.conversation.id}` : held.conversation.kind === "channel" ? `channel ${held.conversation.name}` : `the channels of ${held.conversation.project}`;
|
|
13293
|
+
const parts = [
|
|
13294
|
+
held.uncommitted > 0 ? `${held.uncommitted} uncommitted file(s)` : "",
|
|
13295
|
+
held.onNoBranch > 0 ? `${held.onNoBranch} commit(s) on no branch` : ""
|
|
13296
|
+
].filter((part) => part !== "");
|
|
13297
|
+
return `${held.agent}, ${where}: ${parts.join(" and ")}`;
|
|
13298
|
+
}
|
|
13299
|
+
function differentRefs(left, right) {
|
|
13300
|
+
const names = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
13301
|
+
return [...names].filter((name) => left[name] !== right[name]).toSorted();
|
|
13302
|
+
}
|
|
13303
|
+
function sourceAwayMessage(project, from, to, inPlay2) {
|
|
13304
|
+
const stays = [
|
|
13305
|
+
"its local branches and worktrees",
|
|
13306
|
+
project.repo === null ? `its whole history, since ${project.slug} has no remote (${to} would start an empty repository)` : "",
|
|
13307
|
+
inPlay2.length === 0 ? "" : `the work of ${inPlay2.length} task(s) in play (${inPlay2.map((task) => task.id).join(", ")})`
|
|
13308
|
+
].filter((part) => part !== "");
|
|
13309
|
+
return `runner ${from}, where ${project.slug} lives, is not connected, so nothing can travel. Staying on ${from}: ${stays.join("; ")}. Move it without them to go ahead`;
|
|
13310
|
+
}
|
|
12309
13311
|
|
|
12310
13312
|
// apps/server/dist/index.js
|
|
12311
13313
|
import pino from "pino";
|
|
@@ -12381,7 +13383,9 @@ import { z as z19, ZodError } from "zod";
|
|
|
12381
13383
|
// apps/server/dist/body-limit.js
|
|
12382
13384
|
import { bodyLimit } from "hono/body-limit";
|
|
12383
13385
|
var ENROLL_BODY_LIMIT_BYTES = 16 * 1024;
|
|
13386
|
+
var APP_TOKEN_BODY_LIMIT_BYTES = 16 * 1024;
|
|
12384
13387
|
var API_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
13388
|
+
var IMAGE_BODY_LIMIT_BYTES = IMAGE_LIMIT_BYTES;
|
|
12385
13389
|
var RUNNER_BODY_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
12386
13390
|
var RUNNER_ANSWER_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
12387
13391
|
function limitBody(maxSize) {
|
|
@@ -12398,13 +13402,19 @@ function limitBody(maxSize) {
|
|
|
12398
13402
|
}
|
|
12399
13403
|
|
|
12400
13404
|
// apps/server/dist/github-auth.js
|
|
12401
|
-
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
13405
|
+
import { createHash as createHash4, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
|
|
12402
13406
|
import { Hono } from "hono";
|
|
12403
13407
|
import { z as z18 } from "zod";
|
|
12404
13408
|
var SIGN_IN_TTL_MS = 30 * 24 * 36e5;
|
|
12405
13409
|
var STATE_TTL_MS = 10 * 6e4;
|
|
12406
13410
|
var MAX_STATES = 1e3;
|
|
13411
|
+
var CODE_TTL_MS = 2 * 6e4;
|
|
12407
13412
|
var TokenAnswerSchema = z18.object({ access_token: z18.string().min(1) });
|
|
13413
|
+
var CodeChallengeSchema = z18.string().regex(/^[A-Za-z0-9_-]{43}$/);
|
|
13414
|
+
var CodeExchangeSchema = z18.object({
|
|
13415
|
+
code: z18.string().min(1).max(64),
|
|
13416
|
+
code_verifier: z18.string().regex(/^[A-Za-z0-9._~-]{43,128}$/)
|
|
13417
|
+
});
|
|
12408
13418
|
var GithubUserSchema = z18.object({
|
|
12409
13419
|
login: z18.string().min(1),
|
|
12410
13420
|
id: z18.number().int(),
|
|
@@ -12429,32 +13439,56 @@ function safeNext(next) {
|
|
|
12429
13439
|
} catch {
|
|
12430
13440
|
return "/";
|
|
12431
13441
|
}
|
|
12432
|
-
const
|
|
12433
|
-
return url.origin === BOARD_ORIGIN && /^\/(?![/\\])/.test(
|
|
13442
|
+
const path12 = `${url.pathname}${url.search}`;
|
|
13443
|
+
return url.origin === BOARD_ORIGIN && /^\/(?![/\\])/.test(path12) ? path12 : "/";
|
|
12434
13444
|
}
|
|
12435
|
-
var APP_LANDING = "
|
|
13445
|
+
var APP_LANDING = "/auth/app/signin";
|
|
12436
13446
|
function land(c, next, fragment) {
|
|
13447
|
+
c.header("cache-control", "no-store");
|
|
12437
13448
|
return c.redirect(`${next}#${fragment}`, 302);
|
|
12438
13449
|
}
|
|
13450
|
+
function prune(map, now) {
|
|
13451
|
+
for (const [key, entry] of map) {
|
|
13452
|
+
if (entry.expiresAt <= now || map.size >= MAX_STATES) {
|
|
13453
|
+
map.delete(key);
|
|
13454
|
+
}
|
|
13455
|
+
}
|
|
13456
|
+
}
|
|
13457
|
+
function provesChallenge(verifier, challenge) {
|
|
13458
|
+
const answer = Buffer.from(createHash4("sha256").update(verifier).digest("base64url"));
|
|
13459
|
+
const expected = Buffer.from(challenge);
|
|
13460
|
+
return answer.length === expected.length && timingSafeEqual(answer, expected);
|
|
13461
|
+
}
|
|
12439
13462
|
function githubAuthRoutes(options) {
|
|
12440
13463
|
const { board: board2, github } = options;
|
|
12441
13464
|
const fetchImpl = options.fetch ?? fetch;
|
|
12442
13465
|
const now = options.now ?? Date.now;
|
|
12443
13466
|
const states = /* @__PURE__ */ new Map();
|
|
13467
|
+
const codes = /* @__PURE__ */ new Map();
|
|
12444
13468
|
const routes = new Hono();
|
|
12445
|
-
routes.get("/config", (c) => c.json({ github: github !== null }));
|
|
13469
|
+
routes.get("/config", (c) => c.json({ github: github !== null, appLanding: (github?.appleAppId ?? null) !== null }));
|
|
13470
|
+
routes.get("/app/signin", (c) => c.text("not found", 404, { "cache-control": "no-store" }));
|
|
12446
13471
|
if (github === null) {
|
|
12447
13472
|
return routes;
|
|
12448
13473
|
}
|
|
12449
|
-
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
|
|
13474
|
+
const limit2 = options.limit ?? ((_c, next) => next());
|
|
13475
|
+
routes.get("/github", limit2, (c) => {
|
|
13476
|
+
const app2 = c.req.query("landing") === "app";
|
|
13477
|
+
let challenge = null;
|
|
13478
|
+
if (app2) {
|
|
13479
|
+
if (github.appleAppId === null) {
|
|
13480
|
+
return c.text("Signing in to the app is off on this board", 403);
|
|
13481
|
+
}
|
|
13482
|
+
const parsed = CodeChallengeSchema.safeParse(c.req.query("code_challenge"));
|
|
13483
|
+
if (!parsed.success || c.req.query("code_challenge_method") !== "S256") {
|
|
13484
|
+
return c.text("Signing in to the app needs a code_challenge with method S256", 400);
|
|
12453
13485
|
}
|
|
13486
|
+
challenge = parsed.data;
|
|
12454
13487
|
}
|
|
13488
|
+
prune(states, now());
|
|
12455
13489
|
const state = randomBytes2(24).toString("base64url");
|
|
12456
|
-
const next =
|
|
12457
|
-
states.set(state, { next, expiresAt: now() + STATE_TTL_MS });
|
|
13490
|
+
const next = app2 ? APP_LANDING : safeNext(c.req.query("next"));
|
|
13491
|
+
states.set(state, { next, challenge, expiresAt: now() + STATE_TTL_MS });
|
|
12458
13492
|
const authorize = new URL("https://github.com/login/oauth/authorize");
|
|
12459
13493
|
authorize.searchParams.set("client_id", github.clientId);
|
|
12460
13494
|
authorize.searchParams.set("state", state);
|
|
@@ -12507,9 +13541,43 @@ function githubAuthRoutes(options) {
|
|
|
12507
13541
|
options.log?.warn({ login }, "GitHub sign-in refused a login that is not on the list");
|
|
12508
13542
|
return land(c, pending.next, "signin-error=not-allowed");
|
|
12509
13543
|
}
|
|
13544
|
+
if (pending.challenge !== null) {
|
|
13545
|
+
prune(codes, now());
|
|
13546
|
+
const appCode = randomBytes2(24).toString("base64url");
|
|
13547
|
+
codes.set(appCode, {
|
|
13548
|
+
challenge: pending.challenge,
|
|
13549
|
+
login,
|
|
13550
|
+
avatar,
|
|
13551
|
+
expiresAt: now() + CODE_TTL_MS
|
|
13552
|
+
});
|
|
13553
|
+
return land(c, pending.next, `code=${appCode}`);
|
|
13554
|
+
}
|
|
12510
13555
|
const token = await board2.signIn(login, SIGN_IN_TTL_MS, avatar);
|
|
12511
13556
|
return land(c, pending.next, `session=${encodeURIComponent(token)}`);
|
|
12512
13557
|
});
|
|
13558
|
+
if (github.appleAppId !== null) {
|
|
13559
|
+
routes.post("/app/token", limit2, limitBody(APP_TOKEN_BODY_LIMIT_BYTES), async (c) => {
|
|
13560
|
+
const body = CodeExchangeSchema.safeParse(await c.req.json().catch(() => null));
|
|
13561
|
+
if (!body.success) {
|
|
13562
|
+
return c.json({ error: "invalid_request" }, 400);
|
|
13563
|
+
}
|
|
13564
|
+
const pending = codes.get(body.data.code);
|
|
13565
|
+
codes.delete(body.data.code);
|
|
13566
|
+
if (pending === void 0 || pending.expiresAt <= now() || !provesChallenge(body.data.code_verifier, pending.challenge)) {
|
|
13567
|
+
return c.json({ error: "invalid_grant" }, 400);
|
|
13568
|
+
}
|
|
13569
|
+
const token = await board2.signIn(pending.login, SIGN_IN_TTL_MS, pending.avatar);
|
|
13570
|
+
return c.json({ session: token }, 200, { "cache-control": "no-store" });
|
|
13571
|
+
});
|
|
13572
|
+
}
|
|
13573
|
+
return routes;
|
|
13574
|
+
}
|
|
13575
|
+
function appleAppSiteAssociation(github) {
|
|
13576
|
+
const routes = new Hono();
|
|
13577
|
+
routes.get("/apple-app-site-association", (c) => {
|
|
13578
|
+
const appId = github?.appleAppId ?? null;
|
|
13579
|
+
return appId === null ? c.text("not found", 404) : c.json({ webcredentials: { apps: [appId] } });
|
|
13580
|
+
});
|
|
12513
13581
|
return routes;
|
|
12514
13582
|
}
|
|
12515
13583
|
|
|
@@ -12617,14 +13685,114 @@ function rateLimit(options) {
|
|
|
12617
13685
|
}
|
|
12618
13686
|
|
|
12619
13687
|
// apps/server/dist/runner-routes.js
|
|
12620
|
-
import
|
|
13688
|
+
import path9 from "node:path";
|
|
12621
13689
|
import { Hono as Hono2 } from "hono";
|
|
12622
13690
|
import { streamSSE } from "hono/streaming";
|
|
12623
13691
|
|
|
12624
13692
|
// apps/server/dist/home-git.js
|
|
12625
|
-
import {
|
|
13693
|
+
import { once } from "node:events";
|
|
13694
|
+
import { execa as execa3 } from "execa";
|
|
13695
|
+
|
|
13696
|
+
// apps/server/dist/serve.js
|
|
13697
|
+
import { serve } from "@hono/node-server";
|
|
13698
|
+
var REQUEST_TIMEOUT_MS = 3e5;
|
|
13699
|
+
var HEADERS_TIMEOUT_MS = 6e4;
|
|
13700
|
+
var deadlines = /* @__PURE__ */ new WeakMap();
|
|
13701
|
+
function serveBoard(options, listening) {
|
|
13702
|
+
const timeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
13703
|
+
return serve({
|
|
13704
|
+
fetch: (request, env) => {
|
|
13705
|
+
const { incoming, outgoing } = env;
|
|
13706
|
+
if (!incoming.complete) {
|
|
13707
|
+
const timer = setTimeout(() => {
|
|
13708
|
+
if (!incoming.complete) {
|
|
13709
|
+
incoming.socket.destroy();
|
|
13710
|
+
}
|
|
13711
|
+
}, timeoutMs);
|
|
13712
|
+
timer.unref();
|
|
13713
|
+
const lift = () => clearTimeout(timer);
|
|
13714
|
+
deadlines.set(request, lift);
|
|
13715
|
+
outgoing.once("close", lift);
|
|
13716
|
+
}
|
|
13717
|
+
return options.fetch(request, env);
|
|
13718
|
+
},
|
|
13719
|
+
port: options.port,
|
|
13720
|
+
hostname: options.hostname,
|
|
13721
|
+
serverOptions: {
|
|
13722
|
+
requestTimeout: 0,
|
|
13723
|
+
headersTimeout: options.headersTimeoutMs ?? HEADERS_TIMEOUT_MS,
|
|
13724
|
+
...options.connectionsCheckingIntervalMs === void 0 ? {} : { connectionsCheckingInterval: options.connectionsCheckingIntervalMs }
|
|
13725
|
+
}
|
|
13726
|
+
}, listening);
|
|
13727
|
+
}
|
|
13728
|
+
function liftDeadline(request) {
|
|
13729
|
+
deadlines.get(request)?.();
|
|
13730
|
+
deadlines.delete(request);
|
|
13731
|
+
}
|
|
13732
|
+
|
|
13733
|
+
// apps/server/dist/home-git.js
|
|
12626
13734
|
var SMART_PATHS = /* @__PURE__ */ new Set(["/info/refs", "/git-upload-pack", "/git-receive-pack"]);
|
|
12627
|
-
var MAX_BYTES =
|
|
13735
|
+
var MAX_BYTES = MOVE_PACK_LIMIT_BYTES;
|
|
13736
|
+
var IDLE_MS = 5 * 6e4;
|
|
13737
|
+
async function feed(body, stdin, limit2, idleMs) {
|
|
13738
|
+
if (body === null) {
|
|
13739
|
+
stdin.end();
|
|
13740
|
+
return "done";
|
|
13741
|
+
}
|
|
13742
|
+
const reader = body.getReader();
|
|
13743
|
+
const waits = new AbortController();
|
|
13744
|
+
let ended = null;
|
|
13745
|
+
const endedAs = () => ended;
|
|
13746
|
+
const end = (why) => {
|
|
13747
|
+
if (ended === null) {
|
|
13748
|
+
ended = why;
|
|
13749
|
+
waits.abort();
|
|
13750
|
+
if (why === "idle" || why === "cut") {
|
|
13751
|
+
reader.cancel().catch(() => void 0);
|
|
13752
|
+
} else {
|
|
13753
|
+
reader.releaseLock();
|
|
13754
|
+
}
|
|
13755
|
+
}
|
|
13756
|
+
return ended;
|
|
13757
|
+
};
|
|
13758
|
+
const timer = setTimeout(() => end("idle"), idleMs);
|
|
13759
|
+
const stopped = () => {
|
|
13760
|
+
end("stopped");
|
|
13761
|
+
};
|
|
13762
|
+
stdin.on("error", stopped);
|
|
13763
|
+
stdin.once("close", stopped);
|
|
13764
|
+
let total = 0;
|
|
13765
|
+
try {
|
|
13766
|
+
for (; ; ) {
|
|
13767
|
+
const next = await reader.read().catch(() => null);
|
|
13768
|
+
const early = endedAs();
|
|
13769
|
+
if (early !== null) {
|
|
13770
|
+
return early;
|
|
13771
|
+
}
|
|
13772
|
+
if (next === null) {
|
|
13773
|
+
return end("cut");
|
|
13774
|
+
}
|
|
13775
|
+
if (next.done) {
|
|
13776
|
+
stdin.end();
|
|
13777
|
+
return "done";
|
|
13778
|
+
}
|
|
13779
|
+
timer.refresh();
|
|
13780
|
+
total += next.value.byteLength;
|
|
13781
|
+
if (total > limit2) {
|
|
13782
|
+
return end("too large");
|
|
13783
|
+
}
|
|
13784
|
+
if (!stdin.write(next.value)) {
|
|
13785
|
+
const drained = await once(stdin, "drain", { signal: waits.signal }).then(() => true, () => false);
|
|
13786
|
+
if (!drained) {
|
|
13787
|
+
return end("stopped");
|
|
13788
|
+
}
|
|
13789
|
+
}
|
|
13790
|
+
}
|
|
13791
|
+
} finally {
|
|
13792
|
+
clearTimeout(timer);
|
|
13793
|
+
stdin.off("close", stopped);
|
|
13794
|
+
}
|
|
13795
|
+
}
|
|
12628
13796
|
function parseCgi(output) {
|
|
12629
13797
|
const crlf = output.indexOf("\r\n\r\n");
|
|
12630
13798
|
const lf = output.indexOf("\n\n");
|
|
@@ -12652,12 +13820,13 @@ function parseCgi(output) {
|
|
|
12652
13820
|
var HomeGit = class {
|
|
12653
13821
|
pushes = /* @__PURE__ */ new Map();
|
|
12654
13822
|
serve(input) {
|
|
13823
|
+
liftDeadline(input.request);
|
|
12655
13824
|
if (input.rest !== "/git-receive-pack") {
|
|
12656
13825
|
return serveHomeGit(input);
|
|
12657
13826
|
}
|
|
12658
|
-
const previous = this.pushes.get(input.
|
|
13827
|
+
const previous = this.pushes.get(input.repository) ?? Promise.resolve();
|
|
12659
13828
|
const run = previous.then(() => serveHomeGit(input), () => serveHomeGit(input));
|
|
12660
|
-
this.pushes.set(input.
|
|
13829
|
+
this.pushes.set(input.repository, run.catch(() => void 0));
|
|
12661
13830
|
return run;
|
|
12662
13831
|
}
|
|
12663
13832
|
};
|
|
@@ -12666,10 +13835,13 @@ async function serveHomeGit(input) {
|
|
|
12666
13835
|
return new Response("not a smart HTTP path", { status: 404 });
|
|
12667
13836
|
}
|
|
12668
13837
|
const { request } = input;
|
|
12669
|
-
const
|
|
12670
|
-
|
|
12671
|
-
|
|
13838
|
+
const limit2 = input.limitBytes ?? MAX_BYTES;
|
|
13839
|
+
const length = request.headers.get("content-length");
|
|
13840
|
+
const declared = length !== null && /^\d+$/.test(length) ? length : null;
|
|
13841
|
+
if (declared !== null && Number(declared) > limit2) {
|
|
13842
|
+
return new Response("pack too large for the board to take", { status: 413 });
|
|
12672
13843
|
}
|
|
13844
|
+
liftDeadline(request);
|
|
12673
13845
|
const url = new URL(request.url);
|
|
12674
13846
|
const env = {
|
|
12675
13847
|
PATH: process.env["PATH"] ?? "/usr/bin:/bin",
|
|
@@ -12677,13 +13849,15 @@ async function serveHomeGit(input) {
|
|
|
12677
13849
|
GIT_PROJECT_ROOT: input.root,
|
|
12678
13850
|
GIT_HTTP_EXPORT_ALL: "1",
|
|
12679
13851
|
REQUEST_METHOD: request.method,
|
|
12680
|
-
PATH_INFO: `/${input.
|
|
13852
|
+
PATH_INFO: `/${input.repository}${input.rest}`,
|
|
12681
13853
|
QUERY_STRING: url.search.replace(/^\?/, ""),
|
|
12682
13854
|
CONTENT_TYPE: request.headers.get("content-type") ?? "",
|
|
12683
|
-
CONTENT_LENGTH: String(body.length),
|
|
12684
13855
|
REMOTE_USER: input.runner,
|
|
12685
13856
|
REMOTE_ADDR: "127.0.0.1"
|
|
12686
13857
|
};
|
|
13858
|
+
if (declared !== null) {
|
|
13859
|
+
env["CONTENT_LENGTH"] = declared;
|
|
13860
|
+
}
|
|
12687
13861
|
const encoding = request.headers.get("content-encoding");
|
|
12688
13862
|
if (encoding !== null) {
|
|
12689
13863
|
env["HTTP_CONTENT_ENCODING"] = encoding;
|
|
@@ -12692,14 +13866,25 @@ async function serveHomeGit(input) {
|
|
|
12692
13866
|
if (protocol !== null) {
|
|
12693
13867
|
env["HTTP_GIT_PROTOCOL"] = protocol;
|
|
12694
13868
|
}
|
|
12695
|
-
const
|
|
13869
|
+
const backend = execa3("git", ["http-backend"], {
|
|
12696
13870
|
env,
|
|
12697
13871
|
extendEnv: false,
|
|
12698
|
-
input: body,
|
|
12699
13872
|
encoding: "buffer",
|
|
13873
|
+
// The blank line that ends the headers ends the whole answer when the body is empty, as for
|
|
13874
|
+
// the 4-byte probe git sends before a pack too large for one post; stripped, it reads as none.
|
|
13875
|
+
stripFinalNewline: false,
|
|
12700
13876
|
reject: false,
|
|
12701
13877
|
maxBuffer: MAX_BYTES
|
|
12702
13878
|
});
|
|
13879
|
+
const fed = await feed(request.body, backend.stdin, limit2, input.idleMs ?? IDLE_MS);
|
|
13880
|
+
if (fed !== "done" && fed !== "stopped") {
|
|
13881
|
+
backend.kill();
|
|
13882
|
+
await backend;
|
|
13883
|
+
return fed === "too large" ? new Response("pack too large for the board to take", { status: 413 }) : new Response(fed === "idle" ? "the request stopped arriving" : "the request was cut", {
|
|
13884
|
+
status: fed === "idle" ? 408 : 400
|
|
13885
|
+
});
|
|
13886
|
+
}
|
|
13887
|
+
const result = await backend;
|
|
12703
13888
|
if (result.exitCode !== 0 && result.stdout.length === 0) {
|
|
12704
13889
|
return new Response(`git http-backend failed: ${Buffer.from(result.stderr).toString("utf8")}`, {
|
|
12705
13890
|
status: 500
|
|
@@ -12812,20 +13997,47 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
12812
13997
|
const marker = `/homes/${agent}/git`;
|
|
12813
13998
|
const at = c.req.path.indexOf(marker);
|
|
12814
13999
|
return homeGit.serve({
|
|
12815
|
-
root:
|
|
12816
|
-
agent,
|
|
14000
|
+
root: path9.dirname(repository),
|
|
14001
|
+
repository: agent,
|
|
12817
14002
|
rest: at === -1 ? "" : c.req.path.slice(at + marker.length),
|
|
12818
14003
|
request: c.req.raw,
|
|
12819
14004
|
runner: c.get("runner")
|
|
12820
14005
|
});
|
|
12821
14006
|
});
|
|
12822
14007
|
routes.route("/homes/:agent", home);
|
|
14008
|
+
routes.all("/moves/:id/git/*", async (c) => {
|
|
14009
|
+
const id = UlidSchema.safeParse(c.req.param("id"));
|
|
14010
|
+
const marker = `/moves/${c.req.param("id")}/git`;
|
|
14011
|
+
const at = c.req.path.indexOf(marker);
|
|
14012
|
+
const rest = at === -1 ? "" : c.req.path.slice(at + marker.length);
|
|
14013
|
+
const service = rest === "/info/refs" ? c.req.query("service") ?? "" : rest.slice(1);
|
|
14014
|
+
if (service !== "git-upload-pack" && service !== "git-receive-pack") {
|
|
14015
|
+
return c.json({ error: "NOT_FOUND", message: "not a smart HTTP request" }, 404);
|
|
14016
|
+
}
|
|
14017
|
+
const access3 = id.success ? hub.moveAccess(c.get("runner"), id.data, service === "git-receive-pack") : "unknown";
|
|
14018
|
+
if (access3 === "unknown") {
|
|
14019
|
+
return c.json({ error: "NOT_FOUND", message: "no such move under way" }, 404);
|
|
14020
|
+
}
|
|
14021
|
+
if (access3 === "refused" || !id.success) {
|
|
14022
|
+
return c.json({
|
|
14023
|
+
error: "FORBIDDEN",
|
|
14024
|
+
message: `this runner may not ${service === "git-receive-pack" ? "push to" : "fetch from"} this move now`
|
|
14025
|
+
}, 403);
|
|
14026
|
+
}
|
|
14027
|
+
return homeGit.serve({
|
|
14028
|
+
root: board2.moveRepos,
|
|
14029
|
+
repository: `${id.data}.git`,
|
|
14030
|
+
rest,
|
|
14031
|
+
request: c.req.raw,
|
|
14032
|
+
runner: c.get("runner")
|
|
14033
|
+
});
|
|
14034
|
+
});
|
|
12823
14035
|
return routes;
|
|
12824
14036
|
}
|
|
12825
14037
|
|
|
12826
14038
|
// apps/server/dist/static.js
|
|
12827
14039
|
import { readFile as readFile5, stat as stat4 } from "node:fs/promises";
|
|
12828
|
-
import
|
|
14040
|
+
import path10 from "node:path";
|
|
12829
14041
|
var CONTENT_TYPES = {
|
|
12830
14042
|
".html": "text/html; charset=utf-8",
|
|
12831
14043
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -12853,11 +14065,11 @@ function within2(root, pathname) {
|
|
|
12853
14065
|
} catch {
|
|
12854
14066
|
return null;
|
|
12855
14067
|
}
|
|
12856
|
-
const candidate =
|
|
12857
|
-
return candidate === root || candidate.startsWith(root +
|
|
14068
|
+
const candidate = path10.resolve(root, `.${decoded}`);
|
|
14069
|
+
return candidate === root || candidate.startsWith(root + path10.sep) ? candidate : null;
|
|
12858
14070
|
}
|
|
12859
14071
|
function spaHandler(staticDir) {
|
|
12860
|
-
const root =
|
|
14072
|
+
const root = path10.resolve(staticDir);
|
|
12861
14073
|
return async (c) => {
|
|
12862
14074
|
const { pathname } = new URL(c.req.url);
|
|
12863
14075
|
if (pathname.startsWith("/api/")) {
|
|
@@ -12868,14 +14080,14 @@ function spaHandler(staticDir) {
|
|
|
12868
14080
|
if (file !== null && await isFile(file)) {
|
|
12869
14081
|
return c.body(await readFile5(file), 200, {
|
|
12870
14082
|
...headers,
|
|
12871
|
-
"content-type": CONTENT_TYPES[
|
|
14083
|
+
"content-type": CONTENT_TYPES[path10.extname(file)] ?? "application/octet-stream",
|
|
12872
14084
|
"cache-control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache"
|
|
12873
14085
|
});
|
|
12874
14086
|
}
|
|
12875
14087
|
if (pathname.startsWith("/assets/")) {
|
|
12876
14088
|
return c.text("not found", 404, headers);
|
|
12877
14089
|
}
|
|
12878
|
-
const index =
|
|
14090
|
+
const index = path10.join(root, "index.html");
|
|
12879
14091
|
if (!await isFile(index)) {
|
|
12880
14092
|
return c.text("The interface is not built: run pnpm build:web.", 404);
|
|
12881
14093
|
}
|
|
@@ -12902,6 +14114,10 @@ var RetireBodySchema = z19.object({ reason: z19.string().min(1) });
|
|
|
12902
14114
|
var RunnerBodySchema = z19.object({ name: NameSchema });
|
|
12903
14115
|
var EnrollmentApprovalSchema = z19.object({ name: NameSchema });
|
|
12904
14116
|
var HomeRunnerBodySchema = z19.object({ runner: NameSchema.nullable() });
|
|
14117
|
+
var ProjectRunnerBodySchema = z19.object({
|
|
14118
|
+
runner: NameSchema,
|
|
14119
|
+
withoutBranches: z19.boolean().optional()
|
|
14120
|
+
});
|
|
12905
14121
|
var ModelBodySchema = z19.object({ model: ModelNameSchema.nullable() });
|
|
12906
14122
|
var EffortBodySchema = z19.object({ effort: EffortSchema.nullable() });
|
|
12907
14123
|
var HistoryLimitSchema = z19.coerce.number().int().min(1).max(500);
|
|
@@ -12935,6 +14151,9 @@ function createApp(deps) {
|
|
|
12935
14151
|
if (error instanceof RunnerAwayError) {
|
|
12936
14152
|
return c.json({ error: "RUNNER_AWAY", message: error.message }, 503);
|
|
12937
14153
|
}
|
|
14154
|
+
if (error instanceof MoveSourceAwayError) {
|
|
14155
|
+
return c.json({ error: "SOURCE_AWAY", message: error.message }, 409);
|
|
14156
|
+
}
|
|
12938
14157
|
return c.json({ error: "INTERNAL", message: error.message }, 500);
|
|
12939
14158
|
});
|
|
12940
14159
|
const authenticate = async (c, next) => {
|
|
@@ -12948,6 +14167,14 @@ function createApp(deps) {
|
|
|
12948
14167
|
};
|
|
12949
14168
|
const api = new Hono3();
|
|
12950
14169
|
api.use("*", authenticate);
|
|
14170
|
+
api.post("/images", limitBody(IMAGE_BODY_LIMIT_BYTES), async (c) => {
|
|
14171
|
+
if (c.get("actor").role !== USER_ROLE) {
|
|
14172
|
+
throw new BoardError("FORBIDDEN", "only the user posts an image's bytes");
|
|
14173
|
+
}
|
|
14174
|
+
const name = ImageNameSchema.optional().parse(c.req.query("name"));
|
|
14175
|
+
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
14176
|
+
return c.json(await board2.postImage(c.get("actor"), bytes, name));
|
|
14177
|
+
});
|
|
12951
14178
|
api.on(["POST", "PUT", "PATCH", "DELETE"], "*", limitBody(API_BODY_LIMIT_BYTES));
|
|
12952
14179
|
api.get("/me", (c) => {
|
|
12953
14180
|
const signIn = board2.signInOf(bearer2(c) ?? "");
|
|
@@ -13000,6 +14227,21 @@ function createApp(deps) {
|
|
|
13000
14227
|
}
|
|
13001
14228
|
return c.json(await quotas.read(runner, cli));
|
|
13002
14229
|
});
|
|
14230
|
+
for (const [verb, paused] of [
|
|
14231
|
+
["pause", true],
|
|
14232
|
+
["resume", false]
|
|
14233
|
+
]) {
|
|
14234
|
+
for (const path12 of [`/runners/:name/${verb}`, `/runners/:name/${verb}/:cli`]) {
|
|
14235
|
+
api.post(path12, async (c) => {
|
|
14236
|
+
const name = NameSchema.parse(c.req.param("name"));
|
|
14237
|
+
const cli = c.req.param("cli");
|
|
14238
|
+
return c.json(await board2.setRunnerPaused(c.get("actor"), name, {
|
|
14239
|
+
paused,
|
|
14240
|
+
...cli === void 0 ? {} : { cli: CliKindSchema.parse(cli) }
|
|
14241
|
+
}));
|
|
14242
|
+
});
|
|
14243
|
+
}
|
|
14244
|
+
}
|
|
13003
14245
|
api.post("/runners", async (c) => {
|
|
13004
14246
|
const body = RunnerBodySchema.parse(await c.req.json());
|
|
13005
14247
|
return c.json(await board2.addRunner(c.get("actor"), body.name));
|
|
@@ -13109,6 +14351,13 @@ function createApp(deps) {
|
|
|
13109
14351
|
return branchFile(c, project.slug, project.defaultBranch, file);
|
|
13110
14352
|
};
|
|
13111
14353
|
api.get("/projects/:slug/files", (c) => projectFile(c, ""));
|
|
14354
|
+
api.put("/projects/:slug/runner", async (c) => {
|
|
14355
|
+
const body = ProjectRunnerBodySchema.parse(await c.req.json());
|
|
14356
|
+
if (deps.runners === void 0) {
|
|
14357
|
+
throw new BoardError("NOT_FOUND", "this board has no runners to move a project between");
|
|
14358
|
+
}
|
|
14359
|
+
return c.json(await deps.runners.moveProject(c.get("actor"), NameSchema.parse(c.req.param("slug")), body.runner, { withoutBranches: body.withoutBranches }));
|
|
14360
|
+
});
|
|
13112
14361
|
api.get("/projects/:slug/files/:path{.+}", (c) => projectFile(c, c.req.param("path")));
|
|
13113
14362
|
api.get("/tasks/:id", async (c) => c.json((await board2.findTask(c.req.param("id"))).task));
|
|
13114
14363
|
api.get("/tasks/:id/thread", async (c) => c.json(await board2.listThread(c.req.param("id"))));
|
|
@@ -13286,6 +14535,7 @@ function createApp(deps) {
|
|
|
13286
14535
|
...deps.signIn,
|
|
13287
14536
|
limit: limited(SIGN_IN_LIMIT, "sign-ins")
|
|
13288
14537
|
}));
|
|
14538
|
+
app2.route("/.well-known", appleAppSiteAssociation(deps.signIn?.github ?? null));
|
|
13289
14539
|
if (deps.runners !== void 0) {
|
|
13290
14540
|
app2.route("/runner", runnerRoutes(board2, deps.runners, deps.enrollments, limited(ENROLL_LIMIT, "enrollment requests")));
|
|
13291
14541
|
}
|
|
@@ -13568,6 +14818,7 @@ var host = new TurnHost({
|
|
|
13568
14818
|
var runners = new RunnerHub({ board, host, version: VERSION, log });
|
|
13569
14819
|
board.watchTurns(() => host.inFlight());
|
|
13570
14820
|
board.readImagesWith((actor, file) => runners.readTurnImage(actor, file));
|
|
14821
|
+
board.openImagesWith((actor, image) => runners.writeTurnImage(actor, image));
|
|
13571
14822
|
var concurrency = config.concurrency ?? Number.POSITIVE_INFINITY;
|
|
13572
14823
|
var timings = parseTimings(JSON.parse(process.env["STELLARIS_TIMINGS"] ?? "{}"));
|
|
13573
14824
|
var scheduler = new Scheduler({ board, runner: runners, log, concurrency, timings });
|
|
@@ -13585,8 +14836,8 @@ var view = {
|
|
|
13585
14836
|
return scheduler.activeSignals;
|
|
13586
14837
|
}
|
|
13587
14838
|
};
|
|
13588
|
-
var webDir = process.env["STELLARIS_WEB_DIR"] ??
|
|
13589
|
-
var webBuilt = await access2(
|
|
14839
|
+
var webDir = process.env["STELLARIS_WEB_DIR"] ?? path11.resolve(import.meta.dirname, "../../web/dist");
|
|
14840
|
+
var webBuilt = await access2(path11.join(webDir, "index.html")).then(() => true, () => false);
|
|
13590
14841
|
var app = createApp({
|
|
13591
14842
|
board,
|
|
13592
14843
|
version: VERSION,
|
|
@@ -13601,7 +14852,7 @@ var app = createApp({
|
|
|
13601
14852
|
trustedProxies: config.trustedProxies,
|
|
13602
14853
|
log
|
|
13603
14854
|
});
|
|
13604
|
-
var server =
|
|
14855
|
+
var server = serveBoard({ fetch: app.fetch, port: config.port, hostname: config.host }, (info) => {
|
|
13605
14856
|
log.info({
|
|
13606
14857
|
host: info.address,
|
|
13607
14858
|
port: info.port,
|
|
@@ -13610,6 +14861,7 @@ var server = serve({ fetch: app.fetch, port: config.port, hostname: config.host
|
|
|
13610
14861
|
webDir,
|
|
13611
14862
|
webBuilt,
|
|
13612
14863
|
signIn: config.github === null ? "token" : `github (${config.github.users.join(", ")})`,
|
|
14864
|
+
appleAppId: config.github?.appleAppId ?? null,
|
|
13613
14865
|
trustedProxies: config.trustedProxies
|
|
13614
14866
|
}, "board server listening");
|
|
13615
14867
|
});
|