@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/cli/index.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// apps/cli/dist/index.js
|
|
4
4
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
5
|
-
import
|
|
5
|
+
import path9 from "node:path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// packages/board-core/dist/board.js
|
|
9
|
-
import { chmod as chmod2, cp, mkdir as
|
|
10
|
-
import
|
|
9
|
+
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";
|
|
10
|
+
import path8 from "node:path";
|
|
11
11
|
import { decodeTime, monotonicFactory } from "ulid";
|
|
12
12
|
import { z as z16 } from "zod";
|
|
13
13
|
|
|
@@ -65,7 +65,8 @@ var VERB_NAMES = [
|
|
|
65
65
|
"update_cron",
|
|
66
66
|
"remove_cron",
|
|
67
67
|
"remove_skill",
|
|
68
|
-
"upload_image"
|
|
68
|
+
"upload_image",
|
|
69
|
+
"read_image"
|
|
69
70
|
];
|
|
70
71
|
var VerbNameSchema = z2.enum(VERB_NAMES);
|
|
71
72
|
var CharterTriggerSchema = z2.enum(["user_post", "ops_event", "heartbeat"]);
|
|
@@ -95,6 +96,7 @@ var USER_NAME = "user";
|
|
|
95
96
|
var MEMBER_VERBS = [
|
|
96
97
|
"post_message",
|
|
97
98
|
"upload_image",
|
|
99
|
+
"read_image",
|
|
98
100
|
"read_inbox",
|
|
99
101
|
"search",
|
|
100
102
|
"open_thread",
|
|
@@ -225,7 +227,12 @@ var CronScheduleSchema = z3.union([
|
|
|
225
227
|
var CronRunSchema = z3.object({
|
|
226
228
|
turnId: UlidSchema,
|
|
227
229
|
at: IsoDateTimeSchema,
|
|
228
|
-
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()
|
|
229
236
|
});
|
|
230
237
|
var CronFrontmatterSchema = z3.object({
|
|
231
238
|
id: UlidSchema,
|
|
@@ -272,6 +279,16 @@ function shortestGapMs(schedule, societyZone, after) {
|
|
|
272
279
|
}
|
|
273
280
|
return shortest;
|
|
274
281
|
}
|
|
282
|
+
function firesInTask(cron2, task2) {
|
|
283
|
+
return cron2.scope === task2.project && cron2.thread === task2.id;
|
|
284
|
+
}
|
|
285
|
+
function cronFailure(cron2) {
|
|
286
|
+
return cron2.lastRun?.outcome === "failed" && cron2.lastRun.caughtUp === void 0 ? cron2.lastRun : null;
|
|
287
|
+
}
|
|
288
|
+
function cronStoppedAt(cron2) {
|
|
289
|
+
const times = [cron2.paused?.at, cron2.ended?.at, cronFailure(cron2)?.at].filter((at) => at !== void 0);
|
|
290
|
+
return times.reduce((first, at) => first === null || Date.parse(at) < Date.parse(first) ? at : first, null);
|
|
291
|
+
}
|
|
275
292
|
function nextFire(cron2, societyZone, floorMs = CRON_FLOOR_MS) {
|
|
276
293
|
if (cron2.ended !== void 0 || cron2.paused !== void 0) {
|
|
277
294
|
return null;
|
|
@@ -651,6 +668,7 @@ var MemberSchema = z5.object({
|
|
|
651
668
|
});
|
|
652
669
|
var RunnerOsSchema = z5.enum(["linux", "windows", "darwin"]);
|
|
653
670
|
var GitIdentityPartSchema = z5.enum(["name", "email"]);
|
|
671
|
+
var RunnerPauseSchema = z5.object({ at: IsoDateTimeSchema, by: NameSchema });
|
|
654
672
|
var RunnerSchema = z5.object({
|
|
655
673
|
name: NameSchema,
|
|
656
674
|
os: RunnerOsSchema,
|
|
@@ -669,8 +687,25 @@ var RunnerSchema = z5.object({
|
|
|
669
687
|
* Each CLI's plan quota as its login on this runner last reported it, from a turn's stream or a
|
|
670
688
|
* read; absent until one has.
|
|
671
689
|
*/
|
|
672
|
-
quotas: z5.partialRecord(CliKindSchema, CliQuotaSchema).optional()
|
|
690
|
+
quotas: z5.partialRecord(CliKindSchema, CliQuotaSchema).optional(),
|
|
691
|
+
/** The user's pause of the whole runner: no turn starts on it until the user resumes it. */
|
|
692
|
+
paused: RunnerPauseSchema.optional(),
|
|
693
|
+
/**
|
|
694
|
+
* The user's pauses of single CLIs on the runner, each apart from the runner's own: no turn of
|
|
695
|
+
* that CLI starts on it until the user resumes it.
|
|
696
|
+
*/
|
|
697
|
+
pausedClis: z5.partialRecord(CliKindSchema, RunnerPauseSchema).optional()
|
|
673
698
|
});
|
|
699
|
+
function runnerPause(runner2, cli) {
|
|
700
|
+
return runner2.paused ?? runner2.pausedClis?.[cli];
|
|
701
|
+
}
|
|
702
|
+
function heldOn(runners, runner2, cli) {
|
|
703
|
+
if (runner2 === void 0 || cli === null) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
const record = runners.find((each) => each.name === runner2);
|
|
707
|
+
return record !== void 0 && runnerPause(record, cli) !== void 0;
|
|
708
|
+
}
|
|
674
709
|
var ProposalKindSchema = z5.enum([
|
|
675
710
|
"role",
|
|
676
711
|
"member",
|
|
@@ -1014,7 +1049,21 @@ function imageTypeOf(bytes) {
|
|
|
1014
1049
|
return null;
|
|
1015
1050
|
}
|
|
1016
1051
|
var ImagePathSchema = z6.string().min(1).max(4096).refine((value) => !value.includes("\0"), { message: "a path with no NUL character" });
|
|
1052
|
+
var ImageNameSchema = z6.string().max(1024);
|
|
1053
|
+
function imageDescription(name) {
|
|
1054
|
+
return name.replaceAll(/[[\]\\\r\n`]/g, "");
|
|
1055
|
+
}
|
|
1017
1056
|
var IMAGE_SCHEME = "image:";
|
|
1057
|
+
var ImageRefSchema = z6.string().regex(/^(?:image:)?[0-9A-HJKMNP-TV-Z]{26}$/, "expected an image's id or image:<id>");
|
|
1058
|
+
function imageIdOf(ref) {
|
|
1059
|
+
return ref.startsWith(IMAGE_SCHEME) ? ref.slice(IMAGE_SCHEME.length) : ref;
|
|
1060
|
+
}
|
|
1061
|
+
var ImageFileSchema = z6.object({
|
|
1062
|
+
id: UlidSchema,
|
|
1063
|
+
type: ImageTypeSchema,
|
|
1064
|
+
size: z6.number().int().positive(),
|
|
1065
|
+
path: ImagePathSchema
|
|
1066
|
+
});
|
|
1018
1067
|
var UploadedImageSchema = z6.object({
|
|
1019
1068
|
id: UlidSchema,
|
|
1020
1069
|
type: ImageTypeSchema,
|
|
@@ -1179,6 +1228,10 @@ var VerbInputs = {
|
|
|
1179
1228
|
upload_image: z7.object({
|
|
1180
1229
|
/** The image file on the machine your turn runs on: absolute, or relative to your working directory. */
|
|
1181
1230
|
path: ImagePathSchema
|
|
1231
|
+
}),
|
|
1232
|
+
read_image: z7.object({
|
|
1233
|
+
/** The image's id, or its `image:<id>` address as a post shows it. */
|
|
1234
|
+
image_id: ImageRefSchema
|
|
1182
1235
|
})
|
|
1183
1236
|
};
|
|
1184
1237
|
|
|
@@ -1190,6 +1243,7 @@ var BOARD_EVENT_TYPES = [
|
|
|
1190
1243
|
"project.configured",
|
|
1191
1244
|
"project.archived",
|
|
1192
1245
|
"project.placed",
|
|
1246
|
+
"project.moved",
|
|
1193
1247
|
"agent.added",
|
|
1194
1248
|
"agent.configured",
|
|
1195
1249
|
"agent.placed",
|
|
@@ -1229,6 +1283,8 @@ var BOARD_EVENT_TYPES = [
|
|
|
1229
1283
|
"user.token_rotated",
|
|
1230
1284
|
"runner.changed",
|
|
1231
1285
|
"runner.quota",
|
|
1286
|
+
"runner.paused",
|
|
1287
|
+
"runner.resumed",
|
|
1232
1288
|
"ops.signal",
|
|
1233
1289
|
"paused.changed",
|
|
1234
1290
|
"wake.requested",
|
|
@@ -1370,6 +1426,16 @@ var RunnerHelloSchema = z9.object({
|
|
|
1370
1426
|
quotaReads: z9.boolean().default(false),
|
|
1371
1427
|
/** Whether the runner reads an image a turn running there uploads; one from before never does. */
|
|
1372
1428
|
imageReads: z9.boolean().default(false),
|
|
1429
|
+
/**
|
|
1430
|
+
* Whether the runner writes an image the board sends for a turn running there to open, for
|
|
1431
|
+
* `read_image`; one from before never does.
|
|
1432
|
+
*/
|
|
1433
|
+
imageWrites: z9.boolean().default(false),
|
|
1434
|
+
/**
|
|
1435
|
+
* Whether the runner carries a project's repository out to another runner and in from one, and
|
|
1436
|
+
* removes its copy once the project has moved; one from before never does.
|
|
1437
|
+
*/
|
|
1438
|
+
projectMoves: z9.boolean().default(false),
|
|
1373
1439
|
capabilities: z9.array(z9.string()).default([]),
|
|
1374
1440
|
/**
|
|
1375
1441
|
* What the runner's account lacks of the git identity citizens' own commits carry, checked
|
|
@@ -1471,8 +1537,19 @@ var TurnJobSchema = z9.object({
|
|
|
1471
1537
|
session: z9.string().min(1).nullable(),
|
|
1472
1538
|
/** The running total the session reported after its last turn, which a resumed session starts from. */
|
|
1473
1539
|
costSoFarUsd: z9.number().nonnegative(),
|
|
1540
|
+
/**
|
|
1541
|
+
* The session's running usage after its last turn, which a resumed session's own usage is
|
|
1542
|
+
* measured from. Absent when the board holds none for the session, and from a server from before.
|
|
1543
|
+
*/
|
|
1544
|
+
usageSoFar: UsageSchema.optional(),
|
|
1474
1545
|
/** The rendered charter, norms, memory core, and skills index, with `PATH_TOKENS` for places. */
|
|
1475
1546
|
instructions: z9.string(),
|
|
1547
|
+
/**
|
|
1548
|
+
* The hash of `instructions` without the first turn's section (`instructionsHash` in
|
|
1549
|
+
* `turn-host`): a warm process started on other instructions starts afresh, so a compaction in
|
|
1550
|
+
* it puts back the current ones. Absent from a server from before, when the text is compared.
|
|
1551
|
+
*/
|
|
1552
|
+
instructionsHash: z9.string().optional(),
|
|
1476
1553
|
prompt: z9.string(),
|
|
1477
1554
|
mcp: z9.object({ url: z9.string().min(1), token: z9.string().min(1) }),
|
|
1478
1555
|
limits: z9.object({
|
|
@@ -1499,6 +1576,17 @@ var TurnOutcomeSchema = z9.object({
|
|
|
1499
1576
|
costUsd: z9.number().nonnegative(),
|
|
1500
1577
|
/** The session's running total after this turn, for CLIs that report one. */
|
|
1501
1578
|
sessionCostUsd: z9.number().nonnegative().optional(),
|
|
1579
|
+
/**
|
|
1580
|
+
* The session's running usage after this turn, for CLIs that report one, else the job's
|
|
1581
|
+
* `usageSoFar`. A runner from before never sends it.
|
|
1582
|
+
*/
|
|
1583
|
+
sessionUsage: UsageSchema.optional(),
|
|
1584
|
+
/**
|
|
1585
|
+
* The whole input of the turn's latest model call in the session's own thread, cached or not:
|
|
1586
|
+
* how large the session had grown. Sub-agents' calls are left out. A turn that made no call,
|
|
1587
|
+
* and a runner from before, never send it.
|
|
1588
|
+
*/
|
|
1589
|
+
contextTokens: z9.number().int().nonnegative().optional(),
|
|
1502
1590
|
/**
|
|
1503
1591
|
* The session the CLI used, which differs from the job's for CLIs that assign their own ids. A
|
|
1504
1592
|
* turn whose CLI never started, because it failed or was stopped first, reports the job's own,
|
|
@@ -1646,6 +1734,13 @@ var TurnImageSchema = z9.discriminatedUnion("kind", [
|
|
|
1646
1734
|
}),
|
|
1647
1735
|
z9.object({ kind: z9.literal("refused"), why: ImageRefusalSchema, detail: z9.string() })
|
|
1648
1736
|
]);
|
|
1737
|
+
var TurnImageWriteSchema = z9.object({
|
|
1738
|
+
turnId: UlidSchema,
|
|
1739
|
+
id: UlidSchema,
|
|
1740
|
+
type: ImageTypeSchema,
|
|
1741
|
+
content: z9.string()
|
|
1742
|
+
});
|
|
1743
|
+
var TurnImageFileSchema = z9.object({ path: ImagePathSchema });
|
|
1649
1744
|
var WorkspaceConversationSchema = z9.discriminatedUnion("kind", [
|
|
1650
1745
|
z9.object({ kind: z9.literal("thread"), id: UlidSchema }),
|
|
1651
1746
|
z9.object({ kind: z9.literal("channel"), scope: NameSchema, name: NameSchema }),
|
|
@@ -1658,6 +1753,46 @@ var HeldWorkspaceSchema = z9.object({
|
|
|
1658
1753
|
});
|
|
1659
1754
|
var HeldWorkspacesSchema = z9.object({ workspaces: z9.array(HeldWorkspaceSchema) });
|
|
1660
1755
|
var SweepResultSchema = z9.object({ kept: z9.array(HeldWorkspaceSchema) });
|
|
1756
|
+
var MOVE_PACK_LIMIT_BYTES = 256 * 1024 * 1024;
|
|
1757
|
+
var MOVE_FLOOR_BYTES_PER_SECOND = 16 * 1024;
|
|
1758
|
+
var RepoRefsSchema = z9.record(z9.string().regex(/^refs\/(heads|tags)\/\S+$/), z9.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/));
|
|
1759
|
+
var ProjectExportSchema = z9.object({ move: UlidSchema, repo: ProjectRepoSchema });
|
|
1760
|
+
var HeldWorkSchema = z9.object({
|
|
1761
|
+
agent: NameSchema,
|
|
1762
|
+
conversation: WorkspaceConversationSchema.nullable(),
|
|
1763
|
+
uncommitted: z9.number().int().nonnegative(),
|
|
1764
|
+
onNoBranch: z9.number().int().nonnegative()
|
|
1765
|
+
});
|
|
1766
|
+
var ProjectExportedSchema = z9.discriminatedUnion("ok", [
|
|
1767
|
+
z9.object({
|
|
1768
|
+
ok: z9.literal(true),
|
|
1769
|
+
refs: RepoRefsSchema,
|
|
1770
|
+
bytes: z9.number().int().nonnegative()
|
|
1771
|
+
}),
|
|
1772
|
+
z9.object({
|
|
1773
|
+
ok: z9.literal(false),
|
|
1774
|
+
held: z9.array(HeldWorkSchema),
|
|
1775
|
+
bytes: z9.number().int().nonnegative()
|
|
1776
|
+
})
|
|
1777
|
+
]);
|
|
1778
|
+
var ProjectImportSchema = z9.object({
|
|
1779
|
+
move: UlidSchema,
|
|
1780
|
+
repo: ProjectRepoSchema,
|
|
1781
|
+
pullRequests: z9.boolean(),
|
|
1782
|
+
/** The packs' size the old runner measured, which the clone's clock is sized from; the cap if unsaid. */
|
|
1783
|
+
bytes: z9.number().int().nonnegative().optional()
|
|
1784
|
+
});
|
|
1785
|
+
var ProjectImportedSchema = z9.object({ refs: RepoRefsSchema });
|
|
1786
|
+
var ProjectReleaseSchema = z9.object({ slug: NameSchema });
|
|
1787
|
+
var ProjectReleasedSchema = z9.object({
|
|
1788
|
+
worktrees: z9.number().int().nonnegative(),
|
|
1789
|
+
clone: z9.boolean()
|
|
1790
|
+
});
|
|
1791
|
+
var ProjectMovedSchema = z9.object({
|
|
1792
|
+
project: ProjectSchema,
|
|
1793
|
+
carried: z9.number().int().nonnegative().nullable(),
|
|
1794
|
+
released: z9.object({ ok: z9.boolean(), detail: z9.string() }).nullable()
|
|
1795
|
+
});
|
|
1661
1796
|
var TurnSteerSchema = z9.object({ id: z9.uuid(), text: z9.string().min(1) });
|
|
1662
1797
|
var RunnerMessageSchema = z9.discriminatedUnion("type", [
|
|
1663
1798
|
z9.object({ type: z9.literal("turn"), job: TurnJobSchema }),
|
|
@@ -1685,6 +1820,15 @@ var RunnerMessageSchema = z9.discriminatedUnion("type", [
|
|
|
1685
1820
|
* whose hello says `imageReads`.
|
|
1686
1821
|
*/
|
|
1687
1822
|
z9.object({ type: z9.literal("image"), request: z9.string().min(1), read: TurnImageReadSchema }),
|
|
1823
|
+
/**
|
|
1824
|
+
* Answered with a `TurnImageFile`, or null when the turn is not running there. Sent only to
|
|
1825
|
+
* runners whose hello says `imageWrites`.
|
|
1826
|
+
*/
|
|
1827
|
+
z9.object({
|
|
1828
|
+
type: z9.literal("image_write"),
|
|
1829
|
+
request: z9.string().min(1),
|
|
1830
|
+
write: TurnImageWriteSchema
|
|
1831
|
+
}),
|
|
1688
1832
|
/** Answered with whether the CLI accepted it; that it took it comes as a `steered` event. */
|
|
1689
1833
|
z9.object({
|
|
1690
1834
|
type: z9.literal("steer"),
|
|
@@ -1694,6 +1838,19 @@ var RunnerMessageSchema = z9.discriminatedUnion("type", [
|
|
|
1694
1838
|
}),
|
|
1695
1839
|
/** Answered with whether the turn was there to stop; it ends `stopped`. */
|
|
1696
1840
|
z9.object({ type: z9.literal("stop"), request: z9.string().min(1), turnId: UlidSchema }),
|
|
1841
|
+
/**
|
|
1842
|
+
* Answered with a `ProjectExported`. Sent only to runners whose hello says `projectMoves`, as are
|
|
1843
|
+
* the import and the release.
|
|
1844
|
+
*/
|
|
1845
|
+
z9.object({ type: z9.literal("export"), request: z9.string().min(1), export: ProjectExportSchema }),
|
|
1846
|
+
/** Answered with a `ProjectImported`. */
|
|
1847
|
+
z9.object({ type: z9.literal("import"), request: z9.string().min(1), import: ProjectImportSchema }),
|
|
1848
|
+
/** Answered with a `ProjectReleased`. */
|
|
1849
|
+
z9.object({
|
|
1850
|
+
type: z9.literal("release"),
|
|
1851
|
+
request: z9.string().min(1),
|
|
1852
|
+
release: ProjectReleaseSchema
|
|
1853
|
+
}),
|
|
1697
1854
|
/**
|
|
1698
1855
|
* A conversation ended: the runner removes every citizen's workspace for it but those a turn
|
|
1699
1856
|
* still uses, and answers with a `SweepResult` naming those it kept for holding work on no branch.
|
|
@@ -1737,6 +1894,7 @@ var TriggerKindSchema = z10.enum([
|
|
|
1737
1894
|
"closing",
|
|
1738
1895
|
"cron"
|
|
1739
1896
|
]);
|
|
1897
|
+
var HeartbeatCauseSchema = z10.enum(["unread", "held", "waiting"]);
|
|
1740
1898
|
var TriggerSchema = z10.object({
|
|
1741
1899
|
kind: TriggerKindSchema,
|
|
1742
1900
|
from: NameSchema.optional(),
|
|
@@ -1747,7 +1905,9 @@ var TriggerSchema = z10.object({
|
|
|
1747
1905
|
/** The channel the waking message was posted in. */
|
|
1748
1906
|
channel: ChannelRefSchema.optional(),
|
|
1749
1907
|
/** The cron that came due, for a `cron` wake. */
|
|
1750
|
-
cronId: UlidSchema.optional()
|
|
1908
|
+
cronId: UlidSchema.optional(),
|
|
1909
|
+
/** What a `heartbeat` woke for; absent on records from before the board kept it. */
|
|
1910
|
+
heartbeat: HeartbeatCauseSchema.optional()
|
|
1751
1911
|
});
|
|
1752
1912
|
var TurnThreadSchema = z10.object({ id: UlidSchema, task: z10.boolean() });
|
|
1753
1913
|
var TurnDispatchSchema = z10.object({
|
|
@@ -1808,6 +1968,22 @@ var TurnRecordSchema = z10.object({
|
|
|
1808
1968
|
* resumed session starts from it, so the next turn's own cost is measured from here.
|
|
1809
1969
|
*/
|
|
1810
1970
|
sessionCostUsd: z10.number().nonnegative().optional(),
|
|
1971
|
+
/**
|
|
1972
|
+
* The running usage the CLI reported for the session after this turn, when the runner reports
|
|
1973
|
+
* one. A resumed session's next turn measures its own usage from here.
|
|
1974
|
+
*/
|
|
1975
|
+
sessionUsage: UsageSchema.optional(),
|
|
1976
|
+
/**
|
|
1977
|
+
* The SHA-256 of the instructions the session holds after this turn, the first turn's section
|
|
1978
|
+
* left out (`instructionsHash` in `turn-host`): those it began with, or the latest a prompt
|
|
1979
|
+
* carried. Absent when the board does not know them, as for records from before it kept them.
|
|
1980
|
+
*/
|
|
1981
|
+
sessionInstructionsHash: z10.string().optional(),
|
|
1982
|
+
/**
|
|
1983
|
+
* The whole input of the turn's latest model call in the session's own thread, as the runner
|
|
1984
|
+
* reported it: how large the session had grown. Absent for a turn that made no call.
|
|
1985
|
+
*/
|
|
1986
|
+
contextTokens: z10.number().int().nonnegative().optional(),
|
|
1811
1987
|
toolCalls: z10.number().int().nonnegative().default(0),
|
|
1812
1988
|
/** The model the CLI reported for this turn, else the model configured for the agent, else null. */
|
|
1813
1989
|
model: z10.string().nullable().default(null),
|
|
@@ -1821,10 +1997,19 @@ var TurnRecordSchema = z10.object({
|
|
|
1821
1997
|
transient: z10.boolean().optional(),
|
|
1822
1998
|
/** How many turns in a row, this one included, could not resume the session for a `retry`. */
|
|
1823
1999
|
resumeRetries: z10.number().int().positive().optional(),
|
|
1824
|
-
/**
|
|
2000
|
+
/**
|
|
2001
|
+
* Whether the server dropped the conversation's session after this turn, which the next starts
|
|
2002
|
+
* anew: one its CLI could not resume, or a society role's home after a reflection completed.
|
|
2003
|
+
*/
|
|
1825
2004
|
sessionDropped: z10.boolean().optional(),
|
|
1826
2005
|
/** The crons whose fire started the turn, which record how it ended. */
|
|
1827
|
-
crons: z10.array(UlidSchema).optional()
|
|
2006
|
+
crons: z10.array(UlidSchema).optional(),
|
|
2007
|
+
/**
|
|
2008
|
+
* The failed fires its prompt named: each of its citizen's crons in its conversation whose latest
|
|
2009
|
+
* fire's turn failed with nothing catching it up (`cronFailure`), but those it carries, with that
|
|
2010
|
+
* turn. Completing it catches each up.
|
|
2011
|
+
*/
|
|
2012
|
+
cronFailures: z10.array(z10.object({ cron: UlidSchema, turnId: UlidSchema })).optional()
|
|
1828
2013
|
});
|
|
1829
2014
|
var RunningTurnSchema = z10.object({
|
|
1830
2015
|
turnId: UlidSchema,
|
|
@@ -1866,6 +2051,8 @@ var TurnHistoryEntrySchema = z10.object({
|
|
|
1866
2051
|
/** The project whose channels' conversation the turn was in, for a turn in the society scope. */
|
|
1867
2052
|
place: NameSchema.optional(),
|
|
1868
2053
|
trigger: z10.string(),
|
|
2054
|
+
/** What a `heartbeat` woke for; absent on turns logged before the board kept it. */
|
|
2055
|
+
heartbeat: HeartbeatCauseSchema.optional(),
|
|
1869
2056
|
exitReason: z10.string().nullable(),
|
|
1870
2057
|
costUsd: z10.number(),
|
|
1871
2058
|
/** The tokens the turn used; absent for turns logged before turns recorded them. */
|
|
@@ -1936,7 +2123,7 @@ var METRICS_WINDOW_MS = {
|
|
|
1936
2123
|
"7d": 7 * 24 * 36e5,
|
|
1937
2124
|
all: null
|
|
1938
2125
|
};
|
|
1939
|
-
var READ_VERBS = ["read_inbox", "search", "get_task"];
|
|
2126
|
+
var READ_VERBS = ["read_inbox", "search", "get_task", "read_image"];
|
|
1940
2127
|
function isBoardAction(tool) {
|
|
1941
2128
|
const verb = /^mcp__board__(.+)$/.exec(tool)?.[1];
|
|
1942
2129
|
return verb !== void 0 && !READ_VERBS.some((read2) => read2 === verb);
|
|
@@ -1950,12 +2137,19 @@ var MetricsSchema = z12.object({
|
|
|
1950
2137
|
/**
|
|
1951
2138
|
* Finished turns that changed nothing on the board. Reflections are left out, since they write
|
|
1952
2139
|
* memory, not the board; turns without a transcript are counted apart, their actions unknown.
|
|
2140
|
+
* Heartbeats are split by what each woke for, and those recorded before the board kept it are a
|
|
2141
|
+
* row without one.
|
|
1953
2142
|
*/
|
|
1954
2143
|
idle: z12.object({
|
|
1955
2144
|
turns: CountSchema,
|
|
1956
2145
|
idle: CountSchema,
|
|
1957
2146
|
unknown: CountSchema,
|
|
1958
|
-
byTrigger: z12.array(z12.object({
|
|
2147
|
+
byTrigger: z12.array(z12.object({
|
|
2148
|
+
trigger: z12.string(),
|
|
2149
|
+
heartbeat: HeartbeatCauseSchema.optional(),
|
|
2150
|
+
turns: CountSchema,
|
|
2151
|
+
idle: CountSchema
|
|
2152
|
+
})),
|
|
1959
2153
|
byAgent: z12.array(z12.object({
|
|
1960
2154
|
agent: NameSchema,
|
|
1961
2155
|
role: NameSchema.nullable(),
|
|
@@ -2034,11 +2228,17 @@ var TokenUseSchema = z12.object({
|
|
|
2034
2228
|
// packages/shared/dist/config.js
|
|
2035
2229
|
import { z as z13 } from "zod";
|
|
2036
2230
|
var LogLevelSchema = z13.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
|
|
2231
|
+
var AppleAppIdSchema = z13.string().regex(/^[A-Z0-9]{10}\.[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*$/, "not <team id>.<bundle id>");
|
|
2037
2232
|
var GithubSignInSchema = z13.object({
|
|
2038
2233
|
clientId: z13.string().min(1),
|
|
2039
2234
|
clientSecret: z13.string().min(1),
|
|
2040
2235
|
/** Logins, lowercased; GitHub logins are case-insensitive. */
|
|
2041
|
-
users: z13.array(z13.string().min(1)).min(1)
|
|
2236
|
+
users: z13.array(z13.string().min(1)).min(1),
|
|
2237
|
+
/**
|
|
2238
|
+
* The iOS app that may sign in, which Apple's association file names so that only it can take the
|
|
2239
|
+
* board's `https` landing; null when no app signs in.
|
|
2240
|
+
*/
|
|
2241
|
+
appleAppId: AppleAppIdSchema.nullable()
|
|
2042
2242
|
});
|
|
2043
2243
|
var ServerConfigSchema = z13.object({
|
|
2044
2244
|
dataDir: z13.string().min(1),
|
|
@@ -2084,7 +2284,7 @@ var RunnerConfigSchema = z13.object({
|
|
|
2084
2284
|
});
|
|
2085
2285
|
|
|
2086
2286
|
// packages/shared/dist/version.js
|
|
2087
|
-
var STELLARIS_VERSION = "0.
|
|
2287
|
+
var STELLARIS_VERSION = "0.4.0";
|
|
2088
2288
|
|
|
2089
2289
|
// packages/shared/dist/mentions.js
|
|
2090
2290
|
import { parse, postprocess, preprocess } from "micromark";
|
|
@@ -2776,6 +2976,41 @@ function filesOf(lines, status) {
|
|
|
2776
2976
|
return [...files.values()];
|
|
2777
2977
|
}
|
|
2778
2978
|
|
|
2979
|
+
// packages/board-core/dist/moves.js
|
|
2980
|
+
import { mkdir as mkdir4, rm as rm2 } from "node:fs/promises";
|
|
2981
|
+
import path5 from "node:path";
|
|
2982
|
+
import { execa as execa2 } from "execa";
|
|
2983
|
+
var MoveRepos = class {
|
|
2984
|
+
root;
|
|
2985
|
+
constructor(root) {
|
|
2986
|
+
this.root = root;
|
|
2987
|
+
}
|
|
2988
|
+
/** Makes the move's empty repository, which checks every object a push brings, and answers its path. */
|
|
2989
|
+
async open(dir) {
|
|
2990
|
+
await mkdir4(this.root, { recursive: true });
|
|
2991
|
+
await git2(["init", "--quiet", "--bare", dir], this.root);
|
|
2992
|
+
await git2(["config", "receive.fsckObjects", "true"], dir);
|
|
2993
|
+
return dir;
|
|
2994
|
+
}
|
|
2995
|
+
async drop(dir) {
|
|
2996
|
+
await rm2(path5.resolve(this.root, dir), { recursive: true, force: true });
|
|
2997
|
+
}
|
|
2998
|
+
/** Removes every move's repository, as the server starts: a move never survives a restart. */
|
|
2999
|
+
async clear() {
|
|
3000
|
+
await rm2(this.root, { recursive: true, force: true });
|
|
3001
|
+
}
|
|
3002
|
+
};
|
|
3003
|
+
async function git2(args, cwd) {
|
|
3004
|
+
const result = await execa2("git", args, {
|
|
3005
|
+
cwd,
|
|
3006
|
+
reject: false,
|
|
3007
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
3008
|
+
});
|
|
3009
|
+
if (result.exitCode !== 0) {
|
|
3010
|
+
throw new Error(`git ${args[0] ?? ""} failed: ${result.stderr.trim()}`);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
|
|
2779
3014
|
// packages/board-core/dist/mentions.js
|
|
2780
3015
|
import vm from "node:vm";
|
|
2781
3016
|
var MENTION_PARSE_MS = 500;
|
|
@@ -2810,7 +3045,8 @@ var TurnPayload = z14.object({
|
|
|
2810
3045
|
thread: z14.string().optional(),
|
|
2811
3046
|
channel: z14.string().optional(),
|
|
2812
3047
|
place: z14.string().optional(),
|
|
2813
|
-
trigger: z14.string()
|
|
3048
|
+
trigger: z14.string(),
|
|
3049
|
+
heartbeat: HeartbeatCauseSchema.optional().catch(void 0)
|
|
2814
3050
|
});
|
|
2815
3051
|
var TaskPayload = z14.object({
|
|
2816
3052
|
taskId: z14.string(),
|
|
@@ -2910,13 +3146,12 @@ function computeMetrics(input) {
|
|
|
2910
3146
|
const nothing = actions === 0 ? 1 : 0;
|
|
2911
3147
|
turns += 1;
|
|
2912
3148
|
idle += nothing;
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
}
|
|
3149
|
+
const { trigger, heartbeat } = turn2.data;
|
|
3150
|
+
const key = heartbeat === void 0 ? trigger : `${trigger}\0${heartbeat}`;
|
|
3151
|
+
const row = turnRows.get(key) ?? { trigger, heartbeat, turns: 0, idle: 0 };
|
|
3152
|
+
turnRows.set(key, { ...row, turns: row.turns + 1, idle: row.idle + nothing });
|
|
3153
|
+
const own = agentRows.get(event.actor) ?? { turns: 0, idle: 0 };
|
|
3154
|
+
agentRows.set(event.actor, { turns: own.turns + 1, idle: own.idle + nothing });
|
|
2920
3155
|
break;
|
|
2921
3156
|
}
|
|
2922
3157
|
case "turn.started": {
|
|
@@ -3072,7 +3307,10 @@ function computeMetrics(input) {
|
|
|
3072
3307
|
turns,
|
|
3073
3308
|
idle,
|
|
3074
3309
|
unknown,
|
|
3075
|
-
byTrigger: [...turnRows].map((
|
|
3310
|
+
byTrigger: [...turnRows.values()].map(({ heartbeat, ...row }) => ({
|
|
3311
|
+
...row,
|
|
3312
|
+
...heartbeat === void 0 ? {} : { heartbeat }
|
|
3313
|
+
})).toSorted((a, b) => b.idle - a.idle || b.turns - a.turns),
|
|
3076
3314
|
byAgent: [...agentRows].map(([agent2, row]) => ({ agent: agent2, role: citizens.get(agent2)?.role ?? null, ...row })).toSorted((a, b) => b.idle - a.idle || b.turns - a.turns)
|
|
3077
3315
|
},
|
|
3078
3316
|
sentBack: {
|
|
@@ -3132,6 +3370,8 @@ var TurnHistoryPayloadSchema = z15.object({
|
|
|
3132
3370
|
channel: NameSchema.optional(),
|
|
3133
3371
|
place: NameSchema.optional(),
|
|
3134
3372
|
trigger: z15.string().default("manual"),
|
|
3373
|
+
// One a later board added reads as none, so the turn stays in the history.
|
|
3374
|
+
heartbeat: HeartbeatCauseSchema.optional().catch(void 0),
|
|
3135
3375
|
exitReason: z15.string().nullable().default(null),
|
|
3136
3376
|
costUsd: z15.number().default(0),
|
|
3137
3377
|
usage: UsageSchema.optional(),
|
|
@@ -3189,121 +3429,121 @@ var Mutex = class {
|
|
|
3189
3429
|
};
|
|
3190
3430
|
|
|
3191
3431
|
// packages/board-core/dist/paths.js
|
|
3192
|
-
import
|
|
3432
|
+
import path6 from "node:path";
|
|
3193
3433
|
var BoardPaths = class {
|
|
3194
3434
|
dataDir;
|
|
3195
3435
|
constructor(dataDir) {
|
|
3196
|
-
this.dataDir =
|
|
3436
|
+
this.dataDir = path6.resolve(dataDir);
|
|
3197
3437
|
}
|
|
3198
3438
|
// Board projection, also the storage of record; only the core library writes it.
|
|
3199
3439
|
get board() {
|
|
3200
|
-
return
|
|
3440
|
+
return path6.join(this.dataDir, "board");
|
|
3201
3441
|
}
|
|
3202
3442
|
get society() {
|
|
3203
|
-
return
|
|
3443
|
+
return path6.join(this.board, "society");
|
|
3204
3444
|
}
|
|
3205
3445
|
societyFile() {
|
|
3206
|
-
return
|
|
3446
|
+
return path6.join(this.society, "society.md");
|
|
3207
3447
|
}
|
|
3208
3448
|
societyChannels() {
|
|
3209
|
-
return
|
|
3449
|
+
return path6.join(this.society, "channels");
|
|
3210
3450
|
}
|
|
3211
3451
|
societyChannel(name) {
|
|
3212
|
-
return
|
|
3452
|
+
return path6.join(this.societyChannels(), name);
|
|
3213
3453
|
}
|
|
3214
3454
|
/** Threads whose channel is a society channel; project channels' threads live under the project. */
|
|
3215
3455
|
societyThreads() {
|
|
3216
|
-
return
|
|
3456
|
+
return path6.join(this.society, "threads");
|
|
3217
3457
|
}
|
|
3218
3458
|
societyKnowledge() {
|
|
3219
|
-
return
|
|
3459
|
+
return path6.join(this.society, "knowledge");
|
|
3220
3460
|
}
|
|
3221
3461
|
societyKnowledgeFile(topic) {
|
|
3222
|
-
return
|
|
3462
|
+
return path6.join(this.societyKnowledge(), `${topic}.md`);
|
|
3223
3463
|
}
|
|
3224
3464
|
/** Skills promoted to the society, one directory per skill holding SKILL.md. */
|
|
3225
3465
|
societySkills() {
|
|
3226
|
-
return
|
|
3466
|
+
return path6.join(this.society, "skills");
|
|
3227
3467
|
}
|
|
3228
3468
|
societySkill(name) {
|
|
3229
|
-
return
|
|
3469
|
+
return path6.join(this.societySkills(), name);
|
|
3230
3470
|
}
|
|
3231
3471
|
societySkillFile(name) {
|
|
3232
|
-
return
|
|
3472
|
+
return path6.join(this.societySkill(name), "SKILL.md");
|
|
3233
3473
|
}
|
|
3234
3474
|
roles() {
|
|
3235
|
-
return
|
|
3475
|
+
return path6.join(this.society, "roles");
|
|
3236
3476
|
}
|
|
3237
3477
|
role(name) {
|
|
3238
|
-
return
|
|
3478
|
+
return path6.join(this.roles(), `${name}.md`);
|
|
3239
3479
|
}
|
|
3240
3480
|
proposals() {
|
|
3241
|
-
return
|
|
3481
|
+
return path6.join(this.society, "proposals");
|
|
3242
3482
|
}
|
|
3243
3483
|
proposal(id) {
|
|
3244
|
-
return
|
|
3484
|
+
return path6.join(this.proposals(), `${id}.md`);
|
|
3245
3485
|
}
|
|
3246
3486
|
/** A skill proposal's folder, as the board took it from its proposer's home. */
|
|
3247
3487
|
proposalFiles(id) {
|
|
3248
|
-
return
|
|
3488
|
+
return path6.join(this.proposals(), id);
|
|
3249
3489
|
}
|
|
3250
3490
|
decisions() {
|
|
3251
|
-
return
|
|
3491
|
+
return path6.join(this.society, "decisions");
|
|
3252
3492
|
}
|
|
3253
3493
|
decision(id) {
|
|
3254
|
-
return
|
|
3494
|
+
return path6.join(this.decisions(), `${id}.md`);
|
|
3255
3495
|
}
|
|
3256
3496
|
runners() {
|
|
3257
|
-
return
|
|
3497
|
+
return path6.join(this.society, "runners");
|
|
3258
3498
|
}
|
|
3259
3499
|
runner(name) {
|
|
3260
|
-
return
|
|
3500
|
+
return path6.join(this.runners(), `${name}.md`);
|
|
3261
3501
|
}
|
|
3262
3502
|
members() {
|
|
3263
|
-
return
|
|
3503
|
+
return path6.join(this.society, "members");
|
|
3264
3504
|
}
|
|
3265
3505
|
/** Crons, one file each with its note as the body; ended ones stay. */
|
|
3266
3506
|
crons() {
|
|
3267
|
-
return
|
|
3507
|
+
return path6.join(this.society, "crons");
|
|
3268
3508
|
}
|
|
3269
3509
|
cron(id) {
|
|
3270
|
-
return
|
|
3510
|
+
return path6.join(this.crons(), `${id}.md`);
|
|
3271
3511
|
}
|
|
3272
3512
|
member(name) {
|
|
3273
|
-
return
|
|
3513
|
+
return path6.join(this.members(), `${name}.md`);
|
|
3274
3514
|
}
|
|
3275
3515
|
projects() {
|
|
3276
|
-
return
|
|
3516
|
+
return path6.join(this.board, "projects");
|
|
3277
3517
|
}
|
|
3278
3518
|
project(slug) {
|
|
3279
|
-
return
|
|
3519
|
+
return path6.join(this.projects(), slug);
|
|
3280
3520
|
}
|
|
3281
3521
|
projectFile(slug) {
|
|
3282
|
-
return
|
|
3522
|
+
return path6.join(this.project(slug), "project.md");
|
|
3283
3523
|
}
|
|
3284
3524
|
projectChannels(slug) {
|
|
3285
|
-
return
|
|
3525
|
+
return path6.join(this.project(slug), "channels");
|
|
3286
3526
|
}
|
|
3287
3527
|
projectChannel(slug, name) {
|
|
3288
|
-
return
|
|
3528
|
+
return path6.join(this.projectChannels(slug), name);
|
|
3289
3529
|
}
|
|
3290
3530
|
threads(slug) {
|
|
3291
|
-
return
|
|
3531
|
+
return path6.join(this.project(slug), "threads");
|
|
3292
3532
|
}
|
|
3293
3533
|
tasks(slug) {
|
|
3294
|
-
return
|
|
3534
|
+
return path6.join(this.project(slug), "tasks");
|
|
3295
3535
|
}
|
|
3296
3536
|
task(slug, id) {
|
|
3297
|
-
return
|
|
3537
|
+
return path6.join(this.tasks(slug), `${id}.md`);
|
|
3298
3538
|
}
|
|
3299
3539
|
projectKnowledge(slug) {
|
|
3300
|
-
return
|
|
3540
|
+
return path6.join(this.project(slug), "knowledge");
|
|
3301
3541
|
}
|
|
3302
3542
|
projectKnowledgeFile(slug, topic) {
|
|
3303
|
-
return
|
|
3543
|
+
return path6.join(this.projectKnowledge(slug), `${topic}.md`);
|
|
3304
3544
|
}
|
|
3305
3545
|
dashboard(slug) {
|
|
3306
|
-
return
|
|
3546
|
+
return path6.join(this.project(slug), "dashboard.md");
|
|
3307
3547
|
}
|
|
3308
3548
|
channelDir(ref) {
|
|
3309
3549
|
const parsed = parseChannelRef(ref);
|
|
@@ -3316,57 +3556,57 @@ var BoardPaths = class {
|
|
|
3316
3556
|
}
|
|
3317
3557
|
/** A thread's record, `<id>.md`, beside the directory of its messages, `<id>/`. */
|
|
3318
3558
|
threadFile(threads, id) {
|
|
3319
|
-
return
|
|
3559
|
+
return path6.join(threads, `${id}.md`);
|
|
3320
3560
|
}
|
|
3321
3561
|
threadMessages(threads, id) {
|
|
3322
|
-
return
|
|
3562
|
+
return path6.join(threads, id);
|
|
3323
3563
|
}
|
|
3324
3564
|
messageFile(dir, id, author) {
|
|
3325
|
-
return
|
|
3565
|
+
return path6.join(dir, `${id}-${author}.md`);
|
|
3326
3566
|
}
|
|
3327
3567
|
// Agent homes: authored by the agent, source of truth on the board server.
|
|
3328
3568
|
agents() {
|
|
3329
|
-
return
|
|
3569
|
+
return path6.join(this.dataDir, "agents");
|
|
3330
3570
|
}
|
|
3331
3571
|
agent(name) {
|
|
3332
|
-
return
|
|
3572
|
+
return path6.join(this.agents(), name);
|
|
3333
3573
|
}
|
|
3334
3574
|
agentFile(name) {
|
|
3335
|
-
return
|
|
3575
|
+
return path6.join(this.agent(name), "agent.json");
|
|
3336
3576
|
}
|
|
3337
3577
|
agentRole(name) {
|
|
3338
|
-
return
|
|
3578
|
+
return path6.join(this.agent(name), "role.md");
|
|
3339
3579
|
}
|
|
3340
3580
|
agentProfile(name) {
|
|
3341
|
-
return
|
|
3581
|
+
return path6.join(this.agent(name), "profile.md");
|
|
3342
3582
|
}
|
|
3343
3583
|
agentMemory(name) {
|
|
3344
|
-
return
|
|
3584
|
+
return path6.join(this.agent(name), "memory");
|
|
3345
3585
|
}
|
|
3346
3586
|
agentMemoryCore(name) {
|
|
3347
|
-
return
|
|
3587
|
+
return path6.join(this.agentMemory(name), "core.md");
|
|
3348
3588
|
}
|
|
3349
3589
|
/** Transcripts of the agent's finished turns, one JSON line per step. */
|
|
3350
3590
|
agentTurns(name) {
|
|
3351
|
-
return
|
|
3591
|
+
return path6.join(this.agent(name), "turns");
|
|
3352
3592
|
}
|
|
3353
3593
|
agentTranscript(name, turnId) {
|
|
3354
|
-
return
|
|
3594
|
+
return path6.join(this.agentTurns(name), `${turnId}.jsonl`);
|
|
3355
3595
|
}
|
|
3356
3596
|
agentSkills(name) {
|
|
3357
|
-
return
|
|
3597
|
+
return path6.join(this.agent(name), "skills");
|
|
3358
3598
|
}
|
|
3359
3599
|
agentSkill(name, skill2) {
|
|
3360
|
-
return
|
|
3600
|
+
return path6.join(this.agentSkills(name), skill2);
|
|
3361
3601
|
}
|
|
3362
3602
|
agentSkillFile(name, skill2) {
|
|
3363
|
-
return
|
|
3603
|
+
return path6.join(this.agentSkill(name, skill2), "SKILL.md");
|
|
3364
3604
|
}
|
|
3365
3605
|
agentProjects(name) {
|
|
3366
|
-
return
|
|
3606
|
+
return path6.join(this.agent(name), "projects");
|
|
3367
3607
|
}
|
|
3368
3608
|
agentProject(name, slug) {
|
|
3369
|
-
return
|
|
3609
|
+
return path6.join(this.agentProjects(name), slug);
|
|
3370
3610
|
}
|
|
3371
3611
|
/**
|
|
3372
3612
|
* A thread's, a channel's, or, in the society scope, a project's conversation's session and last
|
|
@@ -3374,46 +3614,56 @@ var BoardPaths = class {
|
|
|
3374
3614
|
*/
|
|
3375
3615
|
agentConversation(name, slug, part) {
|
|
3376
3616
|
if (part.startsWith("#")) {
|
|
3377
|
-
return
|
|
3617
|
+
return path6.join(this.agentProject(name, slug), "channels", part.slice(1));
|
|
3378
3618
|
}
|
|
3379
|
-
return part.startsWith("~") ?
|
|
3619
|
+
return part.startsWith("~") ? path6.join(this.agentProject(name, slug), "places", part.slice(1)) : path6.join(this.agentProject(name, slug), "threads", part);
|
|
3380
3620
|
}
|
|
3381
3621
|
agentCursors(name) {
|
|
3382
|
-
return
|
|
3622
|
+
return path6.join(this.agent(name), "cursors.json");
|
|
3383
3623
|
}
|
|
3384
3624
|
events() {
|
|
3385
|
-
return
|
|
3625
|
+
return path6.join(this.dataDir, "events");
|
|
3386
3626
|
}
|
|
3387
3627
|
eventLog() {
|
|
3388
|
-
return
|
|
3628
|
+
return path6.join(this.events(), "log.jsonl");
|
|
3389
3629
|
}
|
|
3390
3630
|
/** Images citizens uploaded, by id: outside the projection, which every runner mirrors. */
|
|
3391
3631
|
images() {
|
|
3392
|
-
return
|
|
3632
|
+
return path6.join(this.dataDir, "images");
|
|
3393
3633
|
}
|
|
3394
3634
|
image(id) {
|
|
3395
|
-
return
|
|
3635
|
+
return path6.join(this.images(), id);
|
|
3636
|
+
}
|
|
3637
|
+
/**
|
|
3638
|
+
* The repositories projects travel through between runners, one per move while it runs:
|
|
3639
|
+
* outside the projection, which every runner mirrors, and served only to the move's runners.
|
|
3640
|
+
*/
|
|
3641
|
+
moves() {
|
|
3642
|
+
return path6.join(this.dataDir, "moves");
|
|
3643
|
+
}
|
|
3644
|
+
move(id) {
|
|
3645
|
+
return path6.join(this.moves(), `${id}.git`);
|
|
3396
3646
|
}
|
|
3397
3647
|
state() {
|
|
3398
|
-
return
|
|
3648
|
+
return path6.join(this.dataDir, "state");
|
|
3399
3649
|
}
|
|
3400
3650
|
pausedFile() {
|
|
3401
|
-
return
|
|
3651
|
+
return path6.join(this.state(), "paused.json");
|
|
3402
3652
|
}
|
|
3403
3653
|
/** Removed knowledge topics, set aside outside the projection, by scope. */
|
|
3404
3654
|
removedKnowledge(scope) {
|
|
3405
|
-
return
|
|
3655
|
+
return path6.join(this.state(), "removed-knowledge", scope);
|
|
3406
3656
|
}
|
|
3407
3657
|
/** Skills removed from the society, set aside outside the projection, one directory per removal. */
|
|
3408
3658
|
removedSkills() {
|
|
3409
|
-
return
|
|
3659
|
+
return path6.join(this.state(), "removed-skills");
|
|
3410
3660
|
}
|
|
3411
3661
|
};
|
|
3412
3662
|
|
|
3413
3663
|
// packages/board-core/dist/skill-folder.js
|
|
3414
3664
|
import { constants } from "node:fs";
|
|
3415
3665
|
import { lstat, open as open2, readdir as readdir3, readlink, realpath } from "node:fs/promises";
|
|
3416
|
-
import
|
|
3666
|
+
import path7 from "node:path";
|
|
3417
3667
|
var SKILL_TEXT_LIMIT_CHARS = 1e5;
|
|
3418
3668
|
var SKILL_TEXT_LIMIT_BYTES = SKILL_TEXT_LIMIT_CHARS * 4;
|
|
3419
3669
|
function skillFileText(bytes) {
|
|
@@ -3424,8 +3674,8 @@ function skillFileText(bytes) {
|
|
|
3424
3674
|
};
|
|
3425
3675
|
}
|
|
3426
3676
|
function inside(root, file) {
|
|
3427
|
-
const relative =
|
|
3428
|
-
return relative !== ".." && !relative.startsWith(`..${
|
|
3677
|
+
const relative = path7.relative(root, file);
|
|
3678
|
+
return relative !== ".." && !relative.startsWith(`..${path7.sep}`) && !path7.isAbsolute(relative);
|
|
3429
3679
|
}
|
|
3430
3680
|
async function handleInside(root, handle) {
|
|
3431
3681
|
if (process.platform !== "linux") {
|
|
@@ -3435,7 +3685,7 @@ async function handleInside(root, handle) {
|
|
|
3435
3685
|
return file !== null && inside(root, file);
|
|
3436
3686
|
}
|
|
3437
3687
|
async function walk(root, prefix, found) {
|
|
3438
|
-
const entries = await readdir3(
|
|
3688
|
+
const entries = await readdir3(path7.join(root, ...prefix === "" ? [] : prefix.split("/")), {
|
|
3439
3689
|
withFileTypes: true
|
|
3440
3690
|
}).catch(() => []);
|
|
3441
3691
|
for (const entry of entries.toSorted((a, b) => a.name < b.name ? -1 : 1)) {
|
|
@@ -3456,7 +3706,7 @@ async function walk(root, prefix, found) {
|
|
|
3456
3706
|
async function readEntry(root, relative) {
|
|
3457
3707
|
let real;
|
|
3458
3708
|
try {
|
|
3459
|
-
real = await realpath(
|
|
3709
|
+
real = await realpath(path7.join(root, ...relative.split("/")));
|
|
3460
3710
|
} catch {
|
|
3461
3711
|
return null;
|
|
3462
3712
|
}
|
|
@@ -3547,7 +3797,10 @@ var SignInsSchema = z16.object({
|
|
|
3547
3797
|
expiresAt: IsoDateTimeSchema
|
|
3548
3798
|
})).default([])
|
|
3549
3799
|
});
|
|
3550
|
-
var TurnStartPayloadSchema = z16.object({
|
|
3800
|
+
var TurnStartPayloadSchema = z16.object({
|
|
3801
|
+
turnId: UlidSchema,
|
|
3802
|
+
heartbeat: TurnHistoryPayloadSchema.shape.heartbeat
|
|
3803
|
+
});
|
|
3551
3804
|
var CursorsSchema = z16.object({
|
|
3552
3805
|
digest: z16.string().nullable(),
|
|
3553
3806
|
scopes: z16.record(z16.string(), z16.string()).default({}),
|
|
@@ -3619,6 +3872,9 @@ function skillFolderOf(proposal2) {
|
|
|
3619
3872
|
function proposedFrom(origin, where) {
|
|
3620
3873
|
return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel && origin.place === where.place;
|
|
3621
3874
|
}
|
|
3875
|
+
function latestLease(a, b) {
|
|
3876
|
+
return a === null ? b : b === null || a >= b ? a : b;
|
|
3877
|
+
}
|
|
3622
3878
|
function megabytes(bytes) {
|
|
3623
3879
|
return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "");
|
|
3624
3880
|
}
|
|
@@ -3758,6 +4014,7 @@ var Board = class _Board {
|
|
|
3758
4014
|
/** The turns in flight, which the server hosting them supplies; none until it does. */
|
|
3759
4015
|
turnsInFlight = () => [];
|
|
3760
4016
|
imageSource = null;
|
|
4017
|
+
imageSink = null;
|
|
3761
4018
|
newId = monotonicFactory();
|
|
3762
4019
|
events;
|
|
3763
4020
|
actionCounts = /* @__PURE__ */ new Map();
|
|
@@ -3772,9 +4029,11 @@ var Board = class _Board {
|
|
|
3772
4029
|
signIns = /* @__PURE__ */ new Map();
|
|
3773
4030
|
files = new FileTree();
|
|
3774
4031
|
homes;
|
|
4032
|
+
moves;
|
|
3775
4033
|
constructor(dataDir, options) {
|
|
3776
4034
|
this.paths = new BoardPaths(dataDir);
|
|
3777
|
-
this.homes = new HomeRepos(
|
|
4035
|
+
this.homes = new HomeRepos(path8.join(this.paths.state(), "home-hooks"));
|
|
4036
|
+
this.moves = new MoveRepos(this.paths.moves());
|
|
3778
4037
|
this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
3779
4038
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3780
4039
|
this.events = new EventLog(this.paths.eventLog(), () => this.newId(), this.now);
|
|
@@ -3800,16 +4059,20 @@ var Board = class _Board {
|
|
|
3800
4059
|
await board.loadRunnerTokens();
|
|
3801
4060
|
await board.loadSignIns();
|
|
3802
4061
|
await board.homes.installHooks();
|
|
4062
|
+
await board.moves.clear();
|
|
3803
4063
|
return board;
|
|
3804
4064
|
}
|
|
3805
4065
|
/**
|
|
3806
4066
|
* Applies each one-time alignment an existing society has not had yet, and remembers it, so a
|
|
3807
4067
|
* member that later chooses otherwise keeps its choice. A new alignment is a name and a method in
|
|
3808
|
-
* this list; once every board has applied it, both are deleted.
|
|
4068
|
+
* this list; once every board has applied it, both are deleted.
|
|
3809
4069
|
*/
|
|
3810
4070
|
async applyAlignments() {
|
|
3811
|
-
const alignments = [
|
|
3812
|
-
|
|
4071
|
+
const alignments = [
|
|
4072
|
+
// Whoever may show an image in a post may open one a post shows.
|
|
4073
|
+
{ name: "image-reads-granted", apply: () => this.grantBeside("upload_image", "read_image") }
|
|
4074
|
+
];
|
|
4075
|
+
const file = path8.join(this.paths.state(), "alignments.json");
|
|
3813
4076
|
const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
|
|
3814
4077
|
const pending = alignments.filter(({ name }) => !applied.includes(name));
|
|
3815
4078
|
if (pending.length === 0) {
|
|
@@ -3820,6 +4083,17 @@ var Board = class _Board {
|
|
|
3820
4083
|
}
|
|
3821
4084
|
await writeJson(file, { applied: [...applied, ...pending.map(({ name }) => name)] });
|
|
3822
4085
|
}
|
|
4086
|
+
/**
|
|
4087
|
+
* Grants `verb` once to every charter granting `beside`, the work roles' charters in practice;
|
|
4088
|
+
* seed roles have it from their seed.
|
|
4089
|
+
*/
|
|
4090
|
+
async grantBeside(beside, verb) {
|
|
4091
|
+
for (const charter of await this.listRoles()) {
|
|
4092
|
+
if (charter.verbs.includes(beside) && !charter.verbs.includes(verb)) {
|
|
4093
|
+
await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
3823
4097
|
/** Writes every missing seed charter and aligns the existing ones with their seed. */
|
|
3824
4098
|
async ensureSeedRoles() {
|
|
3825
4099
|
await ensureDir(this.paths.roles());
|
|
@@ -4017,7 +4291,7 @@ ${charter.purpose}
|
|
|
4017
4291
|
});
|
|
4018
4292
|
}
|
|
4019
4293
|
signInsFile() {
|
|
4020
|
-
return
|
|
4294
|
+
return path8.join(this.paths.state(), "sign-ins.json");
|
|
4021
4295
|
}
|
|
4022
4296
|
async readSignIns() {
|
|
4023
4297
|
const file = this.signInsFile();
|
|
@@ -4114,7 +4388,7 @@ ${charter.purpose}
|
|
|
4114
4388
|
const files = await listFiles(this.paths.tasks(project2));
|
|
4115
4389
|
const tasks = [];
|
|
4116
4390
|
for (const file of files) {
|
|
4117
|
-
const doc = await readMarkdown(
|
|
4391
|
+
const doc = await readMarkdown(path8.join(this.paths.tasks(project2), file), TaskFrontmatterSchema);
|
|
4118
4392
|
tasks.push({ ...doc.data, body: doc.body });
|
|
4119
4393
|
}
|
|
4120
4394
|
return tasks;
|
|
@@ -4133,7 +4407,7 @@ ${charter.purpose}
|
|
|
4133
4407
|
const files = await listFiles(this.paths.proposals());
|
|
4134
4408
|
const proposals = [];
|
|
4135
4409
|
for (const file of files) {
|
|
4136
|
-
const doc = await readMarkdown(
|
|
4410
|
+
const doc = await readMarkdown(path8.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
|
|
4137
4411
|
proposals.push({ ...doc.data, body: doc.body });
|
|
4138
4412
|
}
|
|
4139
4413
|
return proposals;
|
|
@@ -4160,7 +4434,7 @@ ${charter.purpose}
|
|
|
4160
4434
|
async listMembers() {
|
|
4161
4435
|
const members = [];
|
|
4162
4436
|
for (const file of await listFiles(this.paths.members())) {
|
|
4163
|
-
const doc = await readMarkdown(
|
|
4437
|
+
const doc = await readMarkdown(path8.join(this.paths.members(), file), MemberSchema);
|
|
4164
4438
|
members.push({ ...doc.data, profile: doc.body });
|
|
4165
4439
|
}
|
|
4166
4440
|
return members;
|
|
@@ -4179,7 +4453,7 @@ ${charter.purpose}
|
|
|
4179
4453
|
const dir = project2 === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project2);
|
|
4180
4454
|
const topics = [];
|
|
4181
4455
|
for (const file of await listFiles(dir)) {
|
|
4182
|
-
topics.push(await this.knowledgeAt(
|
|
4456
|
+
topics.push(await this.knowledgeAt(path8.join(dir, file), project2, file.replace(/\.md$/, "")));
|
|
4183
4457
|
}
|
|
4184
4458
|
return topics;
|
|
4185
4459
|
}
|
|
@@ -4209,7 +4483,7 @@ ${charter.purpose}
|
|
|
4209
4483
|
const aside = this.paths.removedKnowledge(project2 ?? SOCIETY_SCOPE);
|
|
4210
4484
|
let highest = 0;
|
|
4211
4485
|
for (const file of (await listFiles(aside)).filter((name) => name.startsWith(`${topic}.`))) {
|
|
4212
|
-
const copy = await this.knowledgeAt(
|
|
4486
|
+
const copy = await this.knowledgeAt(path8.join(aside, file), project2, topic);
|
|
4213
4487
|
highest = Math.max(highest, copy.revision);
|
|
4214
4488
|
}
|
|
4215
4489
|
return highest;
|
|
@@ -4225,7 +4499,7 @@ ${charter.purpose}
|
|
|
4225
4499
|
async listSkillsIn(dir, scope) {
|
|
4226
4500
|
const skills = [];
|
|
4227
4501
|
for (const entry of await listDirs(dir)) {
|
|
4228
|
-
const file =
|
|
4502
|
+
const file = path8.join(dir, entry, "SKILL.md");
|
|
4229
4503
|
if (!await exists(file)) {
|
|
4230
4504
|
continue;
|
|
4231
4505
|
}
|
|
@@ -4315,7 +4589,7 @@ ${charter.purpose}
|
|
|
4315
4589
|
if (!wanted.delete(file.slice(0, 26))) {
|
|
4316
4590
|
continue;
|
|
4317
4591
|
}
|
|
4318
|
-
const { data } = await readMarkdown(
|
|
4592
|
+
const { data } = await readMarkdown(path8.join(dir, file), MessageFrontmatterSchema);
|
|
4319
4593
|
found.push(MessageRefSchema.parse(data));
|
|
4320
4594
|
}
|
|
4321
4595
|
if (wanted.size === 0) {
|
|
@@ -4347,7 +4621,7 @@ ${charter.purpose}
|
|
|
4347
4621
|
if (!file.endsWith(".md")) {
|
|
4348
4622
|
continue;
|
|
4349
4623
|
}
|
|
4350
|
-
const doc = await readMarkdown(
|
|
4624
|
+
const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
|
|
4351
4625
|
if (doc.data.state !== "open") {
|
|
4352
4626
|
continue;
|
|
4353
4627
|
}
|
|
@@ -4374,7 +4648,7 @@ ${charter.purpose}
|
|
|
4374
4648
|
const all = [];
|
|
4375
4649
|
for (const threads of await this.threadScopes()) {
|
|
4376
4650
|
for (const file of await listFiles(threads)) {
|
|
4377
|
-
const doc = await readMarkdown(
|
|
4651
|
+
const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
|
|
4378
4652
|
const messages = await listFiles(this.paths.threadMessages(threads, doc.data.id));
|
|
4379
4653
|
const last = messages.at(-1);
|
|
4380
4654
|
all.push({
|
|
@@ -4587,6 +4861,46 @@ ${DASHBOARD_LINE}
|
|
|
4587
4861
|
return next;
|
|
4588
4862
|
});
|
|
4589
4863
|
}
|
|
4864
|
+
/**
|
|
4865
|
+
* Pauses or resumes a runner, the user's alone: the whole runner, or with `cli` that CLI on it
|
|
4866
|
+
* alone, each apart from the other. While either holds, no turn it covers starts there; their
|
|
4867
|
+
* wakes wait in the scheduler's queue, and a turn under way finishes. Pausing what is paused, or
|
|
4868
|
+
* resuming what is not, changes nothing and logs nothing.
|
|
4869
|
+
*/
|
|
4870
|
+
async setRunnerPaused(actor, name, change) {
|
|
4871
|
+
this.assertUser(actor, "only the user may pause or resume a runner");
|
|
4872
|
+
const cli = change.cli === void 0 ? void 0 : CliKindSchema.parse(change.cli);
|
|
4873
|
+
return this.mutex.run(async () => {
|
|
4874
|
+
const file = this.paths.runner(name);
|
|
4875
|
+
if (!await exists(file)) {
|
|
4876
|
+
throw new BoardError("NOT_FOUND", `runner ${name} not found`);
|
|
4877
|
+
}
|
|
4878
|
+
const doc = await readMarkdown(file, RunnerSchema);
|
|
4879
|
+
const { paused, pausedClis, ...rest } = doc.data;
|
|
4880
|
+
if (cli !== void 0 && change.paused && !doc.data.clis.includes(cli)) {
|
|
4881
|
+
throw new BoardError("INVALID_STATE", `runner ${name} has no ${cli} to pause`);
|
|
4882
|
+
}
|
|
4883
|
+
const current = cli === void 0 ? paused : pausedClis?.[cli];
|
|
4884
|
+
if (current !== void 0 === change.paused) {
|
|
4885
|
+
return doc.data;
|
|
4886
|
+
}
|
|
4887
|
+
const pause = change.paused ? { at: this.now().toISOString(), by: actor.name } : void 0;
|
|
4888
|
+
const clis = cli === void 0 ? { ...pausedClis } : { ...pausedClis, [cli]: pause };
|
|
4889
|
+
const kept = Object.entries(clis).filter(([, each]) => each !== void 0);
|
|
4890
|
+
const whole = cli === void 0 ? pause : paused;
|
|
4891
|
+
const next = RunnerSchema.parse({
|
|
4892
|
+
...rest,
|
|
4893
|
+
...whole === void 0 ? {} : { paused: whole },
|
|
4894
|
+
...kept.length === 0 ? {} : { pausedClis: Object.fromEntries(kept) }
|
|
4895
|
+
});
|
|
4896
|
+
await writeMarkdown(file, next, doc.body);
|
|
4897
|
+
await this.events.append(change.paused ? "runner.paused" : "runner.resumed", actor.name, {
|
|
4898
|
+
name,
|
|
4899
|
+
...cli === void 0 ? {} : { cli }
|
|
4900
|
+
});
|
|
4901
|
+
return next;
|
|
4902
|
+
});
|
|
4903
|
+
}
|
|
4590
4904
|
/**
|
|
4591
4905
|
* Logs an operations signal as an `ops.signal` event. No channel carries it: the scheduler wakes
|
|
4592
4906
|
* its readers from the event, the runner lists it in their prompts, and the interface's log shows
|
|
@@ -4621,8 +4935,9 @@ ${DASHBOARD_LINE}
|
|
|
4621
4935
|
}
|
|
4622
4936
|
if (event.type === "turn.started") {
|
|
4623
4937
|
const start = TurnStartPayloadSchema.safeParse(event.payload);
|
|
4624
|
-
if (start.success)
|
|
4625
|
-
starts.set(start.data.turnId, event.ts);
|
|
4938
|
+
if (start.success) {
|
|
4939
|
+
starts.set(start.data.turnId, { ts: event.ts, heartbeat: start.data.heartbeat });
|
|
4940
|
+
}
|
|
4626
4941
|
continue;
|
|
4627
4942
|
}
|
|
4628
4943
|
if (event.type !== "turn.completed" && event.type !== "turn.failed") {
|
|
@@ -4630,17 +4945,19 @@ ${DASHBOARD_LINE}
|
|
|
4630
4945
|
}
|
|
4631
4946
|
const parsed = TurnHistoryPayloadSchema.safeParse(event.payload);
|
|
4632
4947
|
if (parsed.success) {
|
|
4633
|
-
const {
|
|
4634
|
-
const
|
|
4635
|
-
if (turnId !== void 0) {
|
|
4636
|
-
starts.delete(turnId);
|
|
4948
|
+
const { heartbeat: ended, ...turn2 } = parsed.data;
|
|
4949
|
+
const start = turn2.turnId === void 0 ? void 0 : starts.get(turn2.turnId);
|
|
4950
|
+
if (turn2.turnId !== void 0) {
|
|
4951
|
+
starts.delete(turn2.turnId);
|
|
4637
4952
|
}
|
|
4953
|
+
const heartbeat = ended ?? start?.heartbeat;
|
|
4638
4954
|
entries.push({
|
|
4639
4955
|
id: event.id,
|
|
4640
4956
|
ts: event.ts,
|
|
4641
4957
|
outcome: event.type === "turn.failed" ? "failed" : "completed",
|
|
4642
|
-
...
|
|
4643
|
-
...
|
|
4958
|
+
...turn2,
|
|
4959
|
+
...heartbeat === void 0 ? {} : { heartbeat },
|
|
4960
|
+
...start === void 0 ? {} : { startedAt: start.ts }
|
|
4644
4961
|
});
|
|
4645
4962
|
}
|
|
4646
4963
|
}
|
|
@@ -4670,14 +4987,31 @@ ${DASHBOARD_LINE}
|
|
|
4670
4987
|
return society;
|
|
4671
4988
|
});
|
|
4672
4989
|
}
|
|
4673
|
-
/**
|
|
4990
|
+
/**
|
|
4991
|
+
* Releases every claim whose lease has expired, but for a holder's that its own cron in the
|
|
4992
|
+
* task's thread keeps (`keptLease`). Called by the scheduler on its tick.
|
|
4993
|
+
*/
|
|
4674
4994
|
async expireLeases() {
|
|
4675
4995
|
return this.mutex.run(async () => {
|
|
4676
4996
|
const expired = [];
|
|
4677
4997
|
const now = this.now();
|
|
4998
|
+
let crons;
|
|
4999
|
+
let runners;
|
|
4678
5000
|
for (const project2 of await listDirs(this.paths.projects())) {
|
|
5001
|
+
let place;
|
|
4679
5002
|
for (const task2 of await this.listTasks(project2)) {
|
|
4680
|
-
if (task2.status
|
|
5003
|
+
if (task2.status !== "claimed") {
|
|
5004
|
+
continue;
|
|
5005
|
+
}
|
|
5006
|
+
crons ??= await this.listCrons();
|
|
5007
|
+
runners ??= await this.listRunners();
|
|
5008
|
+
place ??= await this.readProject(project2);
|
|
5009
|
+
const kept = latestLease(this.keptLease(task2, crons, now), await this.pausedLease(task2, runners, place.runner, now));
|
|
5010
|
+
if (kept !== null) {
|
|
5011
|
+
await this.writeTask(project2, { ...task2, leaseExpiresAt: kept });
|
|
5012
|
+
continue;
|
|
5013
|
+
}
|
|
5014
|
+
if (this.leaseExpired(task2, now)) {
|
|
4681
5015
|
const released = await this.writeTask(project2, {
|
|
4682
5016
|
...task2,
|
|
4683
5017
|
status: "open",
|
|
@@ -4746,20 +5080,24 @@ ${DASHBOARD_LINE}
|
|
|
4746
5080
|
* general channel, one of its other channels, one of its threads, or, in the society scope, a
|
|
4747
5081
|
* project's channels. The heartbeat asks, so that it wakes a member where its unread
|
|
4748
5082
|
* messages are and nowhere else. Society channels count only for a member woken in the society
|
|
4749
|
-
* scope; a project member reads them at its next turn.
|
|
5083
|
+
* scope; a project member reads them at its next turn. Some posts are in the digest but not
|
|
4750
5084
|
* counted, and are read when the member wakes for something else: a task step's, which wakes
|
|
4751
|
-
* whoever it hands work to
|
|
4752
|
-
*
|
|
4753
|
-
* nothing
|
|
4754
|
-
*
|
|
5085
|
+
* whoever it hands work to; the board's own, operations signals and landing announcements, whose
|
|
5086
|
+
* signals that call for action wake their readers directly; and a knowledge topic's notice, which
|
|
5087
|
+
* asks nothing of anyone. In a task's thread a post counts only for whoever the current stage is
|
|
5088
|
+
* for, its holder or, while nobody holds it, a member who may take it, and for the task's creator
|
|
5089
|
+
* when it mentions the user, so the creator can put a wait on the user in; a post that asks the
|
|
5090
|
+
* reader counts wherever it is. A closed thread counts for nothing: its conversation has ended,
|
|
5091
|
+
* and a turn there could only read its posts and be refused a reply, while a wake queued before
|
|
5092
|
+
* it closed, as for a mention, still runs there.
|
|
4755
5093
|
*/
|
|
4756
5094
|
async unreadByConversation(actor) {
|
|
4757
5095
|
const { cursors, upto } = await this.digestSnapshot(actor.name);
|
|
4758
5096
|
const counts = /* @__PURE__ */ new Map();
|
|
4759
|
-
const
|
|
5097
|
+
const threadCounts = /* @__PURE__ */ new Map();
|
|
4760
5098
|
const unscoped = { name: actor.name, role: actor.role };
|
|
4761
|
-
for (const { scope, part, message, news } of await this.digestSince(unscoped, cursors, upto)) {
|
|
4762
|
-
if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
|
|
5099
|
+
for (const { scope, part, message, news, asked } of await this.digestSince(unscoped, cursors, upto)) {
|
|
5100
|
+
if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name || message.notice?.kind === "knowledge") {
|
|
4763
5101
|
continue;
|
|
4764
5102
|
}
|
|
4765
5103
|
const posted = parseChannelRef(message.channel).project ?? SOCIETY_SCOPE;
|
|
@@ -4768,12 +5106,13 @@ ${DASHBOARD_LINE}
|
|
|
4768
5106
|
}
|
|
4769
5107
|
const thread2 = threadOfPart(part);
|
|
4770
5108
|
if (thread2 !== void 0) {
|
|
4771
|
-
let
|
|
4772
|
-
if (
|
|
4773
|
-
|
|
4774
|
-
|
|
5109
|
+
let counted = threadCounts.get(thread2);
|
|
5110
|
+
if (counted === void 0) {
|
|
5111
|
+
counted = await this.threadCount(actor, thread2);
|
|
5112
|
+
threadCounts.set(thread2, counted);
|
|
4775
5113
|
}
|
|
4776
|
-
|
|
5114
|
+
const forReader = counted === "every" || counted !== "none" && asked === true || counted === "asks-user" && message.mentions.includes(USER_NAME);
|
|
5115
|
+
if (!forReader) {
|
|
4777
5116
|
continue;
|
|
4778
5117
|
}
|
|
4779
5118
|
}
|
|
@@ -4926,7 +5265,7 @@ ${DASHBOARD_LINE}
|
|
|
4926
5265
|
include = participates;
|
|
4927
5266
|
}
|
|
4928
5267
|
if (include && message.author !== actor.name) {
|
|
4929
|
-
collected.push({ scope: filedIn, part, message });
|
|
5268
|
+
collected.push({ scope: filedIn, part, message, ...asked ? { asked: true } : {} });
|
|
4930
5269
|
}
|
|
4931
5270
|
}
|
|
4932
5271
|
return collected.toSorted(byId);
|
|
@@ -4969,7 +5308,7 @@ ${task2.body}`;
|
|
|
4969
5308
|
}
|
|
4970
5309
|
}
|
|
4971
5310
|
for (const file of await listFiles(this.paths.projectKnowledge(project2))) {
|
|
4972
|
-
const content = await readFile4(
|
|
5311
|
+
const content = await readFile4(path8.join(this.paths.projectKnowledge(project2), file), "utf8");
|
|
4973
5312
|
if (content.toLowerCase().includes(needle)) {
|
|
4974
5313
|
hits.push({
|
|
4975
5314
|
kind: "knowledge",
|
|
@@ -4990,7 +5329,7 @@ ${task2.body}`;
|
|
|
4990
5329
|
shared.push({
|
|
4991
5330
|
kind: "knowledge",
|
|
4992
5331
|
ref: `society/knowledge/${file}`,
|
|
4993
|
-
file:
|
|
5332
|
+
file: path8.join(this.paths.societyKnowledge(), file)
|
|
4994
5333
|
});
|
|
4995
5334
|
}
|
|
4996
5335
|
for (const skill2 of await this.listSocietySkills()) {
|
|
@@ -5003,7 +5342,7 @@ ${task2.body}`;
|
|
|
5003
5342
|
shared.push({
|
|
5004
5343
|
kind: "memory",
|
|
5005
5344
|
ref: `memory/${file}`,
|
|
5006
|
-
file:
|
|
5345
|
+
file: path8.join(this.paths.agentMemory(actor.name), file)
|
|
5007
5346
|
});
|
|
5008
5347
|
}
|
|
5009
5348
|
for (const entry of shared) {
|
|
@@ -5092,8 +5431,8 @@ ${current.body}`);
|
|
|
5092
5431
|
removedAt: this.now().toISOString()
|
|
5093
5432
|
});
|
|
5094
5433
|
const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
|
|
5095
|
-
await
|
|
5096
|
-
await rename3(file,
|
|
5434
|
+
await mkdir5(aside, { recursive: true });
|
|
5435
|
+
await rename3(file, path8.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
|
|
5097
5436
|
await this.events.append("knowledge.removed", actor.name, {
|
|
5098
5437
|
topic: args.topic,
|
|
5099
5438
|
project: args.project
|
|
@@ -5140,8 +5479,8 @@ ${current.body}`);
|
|
|
5140
5479
|
removedAt: this.now().toISOString()
|
|
5141
5480
|
});
|
|
5142
5481
|
const aside = this.paths.removedSkills();
|
|
5143
|
-
await
|
|
5144
|
-
await rename3(this.paths.societySkill(args.name),
|
|
5482
|
+
await mkdir5(aside, { recursive: true });
|
|
5483
|
+
await rename3(this.paths.societySkill(args.name), path8.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
|
|
5145
5484
|
await this.events.append("skill.removed", actor.name, {
|
|
5146
5485
|
name: args.name,
|
|
5147
5486
|
reason: args.reason
|
|
@@ -5203,12 +5542,30 @@ ${current.body}`);
|
|
|
5203
5542
|
throw new BoardError("NOT_FOUND", "this board has no runners to read an image from");
|
|
5204
5543
|
}
|
|
5205
5544
|
const bytes = await this.imageSource(actor, args.path);
|
|
5545
|
+
return this.keepImage(actor, bytes, args.path, path8.basename(args.path));
|
|
5546
|
+
}
|
|
5547
|
+
/**
|
|
5548
|
+
* Keeps an image the user posts from a composer, as its bytes, under the same checks as an image
|
|
5549
|
+
* a turn uploads. `name`, the file's, describes it in the markdown answered; without one the
|
|
5550
|
+
* description is empty, for the user to write.
|
|
5551
|
+
*/
|
|
5552
|
+
async postImage(actor, bytes, name) {
|
|
5553
|
+
this.assertUser(actor, "only the user posts an image's bytes; a turn uploads one with upload_image");
|
|
5554
|
+
const given = name === void 0 ? "" : ImageNameSchema.parse(name);
|
|
5555
|
+
return this.keepImage(actor, bytes, given === "" ? "the image" : given, given);
|
|
5556
|
+
}
|
|
5557
|
+
/**
|
|
5558
|
+
* Keeps `bytes` under a new id when they are a PNG, JPEG, GIF, or WebP of at most
|
|
5559
|
+
* `IMAGE_LIMIT_BYTES`, and answers the markdown that shows them, described by `name`. `what`
|
|
5560
|
+
* names the image in a refusal.
|
|
5561
|
+
*/
|
|
5562
|
+
async keepImage(actor, bytes, what, name) {
|
|
5206
5563
|
const type = imageTypeOf(bytes);
|
|
5207
5564
|
if (type === null) {
|
|
5208
|
-
throw new BoardError("VALIDATION", `${
|
|
5565
|
+
throw new BoardError("VALIDATION", `${what} is not a PNG, JPEG, GIF, or WebP image by its first bytes`);
|
|
5209
5566
|
}
|
|
5210
5567
|
if (bytes.byteLength > IMAGE_LIMIT_BYTES) {
|
|
5211
|
-
throw new BoardError("VALIDATION", `${
|
|
5568
|
+
throw new BoardError("VALIDATION", `${what} is ${bytes.byteLength} bytes, over the ${IMAGE_LIMIT_BYTES} an image may be`);
|
|
5212
5569
|
}
|
|
5213
5570
|
return this.mutex.run(async () => {
|
|
5214
5571
|
const id = this.newId();
|
|
@@ -5218,15 +5575,37 @@ ${current.body}`);
|
|
|
5218
5575
|
type,
|
|
5219
5576
|
size: bytes.byteLength
|
|
5220
5577
|
});
|
|
5221
|
-
const
|
|
5578
|
+
const description = imageDescription(name);
|
|
5222
5579
|
return {
|
|
5223
5580
|
id,
|
|
5224
5581
|
type,
|
|
5225
5582
|
size: bytes.byteLength,
|
|
5226
|
-
markdown: ``
|
|
5227
5584
|
};
|
|
5228
5585
|
});
|
|
5229
5586
|
}
|
|
5587
|
+
/**
|
|
5588
|
+
* Opens an image the board keeps for the calling turn, by its id or its `image:<id>` address:
|
|
5589
|
+
* the runner the turn runs on writes the bytes to a file in a folder of the turn's own in the
|
|
5590
|
+
* system temp folder, removed when the turn ends, and the board answers its path, for the
|
|
5591
|
+
* citizen to open with its own file tools, since a post reaches a turn as markdown and an image
|
|
5592
|
+
* in it as its address. Any image the board keeps may be opened: every post, brief, topic, and
|
|
5593
|
+
* dashboard that could show one is in the projection every turn reads.
|
|
5594
|
+
*/
|
|
5595
|
+
async openImage(actor, input) {
|
|
5596
|
+
const args = VerbInputs.read_image.parse(input);
|
|
5597
|
+
await this.authorize(actor, "read_image");
|
|
5598
|
+
if (actor.scope === void 0) {
|
|
5599
|
+
throw new BoardError("INVALID_STATE", "read_image writes the image on the machine a turn runs on, so only a turn can call it");
|
|
5600
|
+
}
|
|
5601
|
+
if (this.imageSink === null) {
|
|
5602
|
+
throw new BoardError("NOT_FOUND", "this board has no runners to write an image to");
|
|
5603
|
+
}
|
|
5604
|
+
const id = imageIdOf(args.image_id);
|
|
5605
|
+
const { type, bytes } = await this.readImage(id);
|
|
5606
|
+
const file = await this.imageSink(actor, { id, type, bytes });
|
|
5607
|
+
return { id, type, size: bytes.byteLength, path: file };
|
|
5608
|
+
}
|
|
5230
5609
|
/** An uploaded image's bytes and kind, which its first bytes tell. */
|
|
5231
5610
|
async readImage(id) {
|
|
5232
5611
|
const file = this.paths.image(UlidSchema.parse(id));
|
|
@@ -5244,7 +5623,7 @@ ${current.body}`);
|
|
|
5244
5623
|
async listCrons() {
|
|
5245
5624
|
const crons = [];
|
|
5246
5625
|
for (const file of await listFiles(this.paths.crons())) {
|
|
5247
|
-
crons.push(await this.readCronFile(
|
|
5626
|
+
crons.push(await this.readCronFile(path8.join(this.paths.crons(), file)));
|
|
5248
5627
|
}
|
|
5249
5628
|
return crons;
|
|
5250
5629
|
}
|
|
@@ -5572,6 +5951,13 @@ ${current.body}`);
|
|
|
5572
5951
|
readImagesWith(source) {
|
|
5573
5952
|
this.imageSource = source;
|
|
5574
5953
|
}
|
|
5954
|
+
/**
|
|
5955
|
+
* Where `read_image` writes an image to: the runner the calling turn runs on, through the server
|
|
5956
|
+
* that hosts the runners. Without one, as in the admin CLI, nothing can be opened.
|
|
5957
|
+
*/
|
|
5958
|
+
openImagesWith(sink) {
|
|
5959
|
+
this.imageSink = sink;
|
|
5960
|
+
}
|
|
5575
5961
|
/**
|
|
5576
5962
|
* Refuses to end conversations while another citizen's turn is in one of them: `cut` picks the
|
|
5577
5963
|
* turns that would lose theirs. The caller's own turn is exempt, since ending a conversation
|
|
@@ -5591,7 +5977,7 @@ ${current.body}`);
|
|
|
5591
5977
|
async openThreadIds(dir, keep) {
|
|
5592
5978
|
const ids = /* @__PURE__ */ new Set();
|
|
5593
5979
|
for (const file of await listFiles(dir)) {
|
|
5594
|
-
const doc = await readMarkdown(
|
|
5980
|
+
const doc = await readMarkdown(path8.join(dir, file), ThreadFrontmatterSchema);
|
|
5595
5981
|
if (doc.data.state === "open" && keep({ ...doc.data, body: doc.body })) {
|
|
5596
5982
|
ids.add(doc.data.id);
|
|
5597
5983
|
}
|
|
@@ -5599,12 +5985,12 @@ ${current.body}`);
|
|
|
5599
5985
|
return ids;
|
|
5600
5986
|
}
|
|
5601
5987
|
/**
|
|
5602
|
-
* Refuses to archive a project while a turn runs in it, in a citizen's conversation of
|
|
5603
|
-
* channels in the society scope, or in
|
|
5988
|
+
* Refuses to archive or move a project while a turn runs in it, in a citizen's conversation of
|
|
5989
|
+
* its channels in the society scope, or in one of its open threads.
|
|
5604
5990
|
*/
|
|
5605
|
-
async assertProjectQuiet(actor, slug) {
|
|
5991
|
+
async assertProjectQuiet(actor, slug, what = `project ${slug} cannot be archived`) {
|
|
5606
5992
|
const threads = await this.openThreadIds(this.paths.threads(slug), () => true);
|
|
5607
|
-
this.assertNoTurnCut(actor,
|
|
5993
|
+
this.assertNoTurnCut(actor, what, (turn2) => turn2.scope === slug || turn2.place === slug || turn2.thread !== void 0 && threads.has(turn2.thread));
|
|
5608
5994
|
}
|
|
5609
5995
|
async closeThread(actor, input) {
|
|
5610
5996
|
const args = VerbInputs.close_thread.parse(input);
|
|
@@ -6407,6 +6793,8 @@ ${rationale}`}`);
|
|
|
6407
6793
|
return this.removeCron(actor, VerbInputs.remove_cron.parse(input));
|
|
6408
6794
|
case "upload_image":
|
|
6409
6795
|
return this.uploadImage(actor, VerbInputs.upload_image.parse(input));
|
|
6796
|
+
case "read_image":
|
|
6797
|
+
return this.openImage(actor, VerbInputs.read_image.parse(input));
|
|
6410
6798
|
default:
|
|
6411
6799
|
throw new BoardError("VALIDATION", `unknown verb ${String(verb)}`);
|
|
6412
6800
|
}
|
|
@@ -6571,7 +6959,7 @@ ${rationale}`}`);
|
|
|
6571
6959
|
return part === void 0 ? this.paths.agentProject(agent2, scope) : this.paths.agentConversation(agent2, scope, part);
|
|
6572
6960
|
}
|
|
6573
6961
|
async readSessions(agent2, project2, part) {
|
|
6574
|
-
const file =
|
|
6962
|
+
const file = path8.join(this.sessionDir(agent2, project2, part), "sessions.json");
|
|
6575
6963
|
return await exists(file) ? readJson(file, SessionsFileSchema) : {};
|
|
6576
6964
|
}
|
|
6577
6965
|
/**
|
|
@@ -6582,7 +6970,7 @@ ${rationale}`}`);
|
|
|
6582
6970
|
await this.mutex.run(async () => {
|
|
6583
6971
|
const dir = this.sessionDir(agent2, project2, part);
|
|
6584
6972
|
await ensureDir(dir);
|
|
6585
|
-
const file =
|
|
6973
|
+
const file = path8.join(dir, "sessions.json");
|
|
6586
6974
|
const current = await exists(file) ? await readJson(file, SessionsFileSchema) : {};
|
|
6587
6975
|
const kept = current.runner === void 0 || current.runner === runner2 ? current : {};
|
|
6588
6976
|
await writeJson(file, { ...kept, [cli]: sessionId, runner: runner2 });
|
|
@@ -6594,7 +6982,7 @@ ${rationale}`}`);
|
|
|
6594
6982
|
*/
|
|
6595
6983
|
async dropSession(agent2, project2, cli, sessionId, part) {
|
|
6596
6984
|
return this.mutex.run(async () => {
|
|
6597
|
-
const file =
|
|
6985
|
+
const file = path8.join(this.sessionDir(agent2, project2, part), "sessions.json");
|
|
6598
6986
|
if (!await exists(file)) {
|
|
6599
6987
|
return false;
|
|
6600
6988
|
}
|
|
@@ -6609,7 +6997,7 @@ ${rationale}`}`);
|
|
|
6609
6997
|
}
|
|
6610
6998
|
/** The last turn of one conversation: the scope's home, a channel's, a project's, or a thread's. */
|
|
6611
6999
|
async readLastTurn(agent2, project2, part) {
|
|
6612
|
-
const file =
|
|
7000
|
+
const file = path8.join(this.sessionDir(agent2, project2, part), "last-turn.json");
|
|
6613
7001
|
return await exists(file) ? readJson(file, TurnRecordSchema) : null;
|
|
6614
7002
|
}
|
|
6615
7003
|
/** The latest turn a citizen took in a scope, in any of its conversations there. */
|
|
@@ -6617,9 +7005,9 @@ ${rationale}`}`);
|
|
|
6617
7005
|
let latest = await this.readLastTurn(agent2, project2);
|
|
6618
7006
|
const place = this.paths.agentProject(agent2, project2);
|
|
6619
7007
|
const parts = [
|
|
6620
|
-
...await listDirs(
|
|
6621
|
-
...(await listDirs(
|
|
6622
|
-
...(await listDirs(
|
|
7008
|
+
...await listDirs(path8.join(place, "threads")),
|
|
7009
|
+
...(await listDirs(path8.join(place, "channels"))).map((name) => `#${name}`),
|
|
7010
|
+
...(await listDirs(path8.join(place, "places"))).map((name) => `~${name}`)
|
|
6623
7011
|
];
|
|
6624
7012
|
for (const part of parts) {
|
|
6625
7013
|
const turn2 = await this.readLastTurn(agent2, project2, part);
|
|
@@ -6642,6 +7030,7 @@ ${rationale}`}`);
|
|
|
6642
7030
|
...parsed.channel === void 0 ? {} : { channel: parsed.channel },
|
|
6643
7031
|
...parsed.place === void 0 ? {} : { place: parsed.place },
|
|
6644
7032
|
trigger: parsed.trigger.kind,
|
|
7033
|
+
...parsed.trigger.heartbeat === void 0 ? {} : { heartbeat: parsed.trigger.heartbeat },
|
|
6645
7034
|
session: parsed.session,
|
|
6646
7035
|
runner: parsed.runner
|
|
6647
7036
|
});
|
|
@@ -6664,6 +7053,11 @@ ${rationale}`}`);
|
|
|
6664
7053
|
for (const id of parsed.crons ?? []) {
|
|
6665
7054
|
await this.recordCronRun(id, parsed, failed);
|
|
6666
7055
|
}
|
|
7056
|
+
if (parsed.exitReason === "completed" || parsed.exitReason === "blocked") {
|
|
7057
|
+
for (const shown of parsed.cronFailures ?? []) {
|
|
7058
|
+
await this.catchUpCron(shown, parsed);
|
|
7059
|
+
}
|
|
7060
|
+
}
|
|
6667
7061
|
if (parsed.exitReason === "stopped") {
|
|
6668
7062
|
await this.events.append("turn.stopped", parsed.stoppedBy ?? USER_NAME, {
|
|
6669
7063
|
...parsed.id === void 0 ? {} : { turnId: parsed.id },
|
|
@@ -6681,10 +7075,12 @@ ${rationale}`}`);
|
|
|
6681
7075
|
...parsed.channel === void 0 ? {} : { channel: parsed.channel },
|
|
6682
7076
|
...parsed.place === void 0 ? {} : { place: parsed.place },
|
|
6683
7077
|
trigger: parsed.trigger.kind,
|
|
7078
|
+
...parsed.trigger.heartbeat === void 0 ? {} : { heartbeat: parsed.trigger.heartbeat },
|
|
6684
7079
|
session: parsed.session,
|
|
6685
7080
|
exitReason: parsed.exitReason,
|
|
6686
7081
|
costUsd: parsed.costUsd,
|
|
6687
7082
|
...parsed.usage === null ? {} : { usage: parsed.usage },
|
|
7083
|
+
...parsed.contextTokens === void 0 ? {} : { contextTokens: parsed.contextTokens },
|
|
6688
7084
|
toolCalls: parsed.toolCalls,
|
|
6689
7085
|
model: parsed.model,
|
|
6690
7086
|
summary: parsed.status?.summary ?? null,
|
|
@@ -6803,6 +7199,27 @@ ${rationale}`}`);
|
|
|
6803
7199
|
failures: outcome === "failed" ? cron2.failures + 1 : outcome === "completed" ? 0 : cron2.failures
|
|
6804
7200
|
});
|
|
6805
7201
|
}
|
|
7202
|
+
/**
|
|
7203
|
+
* Marks a cron's failed fire caught up by a completed turn whose prompt named it, while that
|
|
7204
|
+
* failure is still its latest run. Its schedule and its run of failures stay as they are.
|
|
7205
|
+
*/
|
|
7206
|
+
async catchUpCron(shown, record) {
|
|
7207
|
+
if (record.id === void 0 || !await exists(this.paths.cron(shown.cron))) {
|
|
7208
|
+
return;
|
|
7209
|
+
}
|
|
7210
|
+
const cron2 = await this.readCron(shown.cron);
|
|
7211
|
+
const failure = cronFailure(cron2);
|
|
7212
|
+
if (failure === null || failure.turnId !== shown.turnId) {
|
|
7213
|
+
return;
|
|
7214
|
+
}
|
|
7215
|
+
await this.writeCron({
|
|
7216
|
+
...cron2,
|
|
7217
|
+
lastRun: {
|
|
7218
|
+
...failure,
|
|
7219
|
+
caughtUp: { turnId: record.id, at: record.endedAt ?? this.now().toISOString() }
|
|
7220
|
+
}
|
|
7221
|
+
});
|
|
7222
|
+
}
|
|
6806
7223
|
/**
|
|
6807
7224
|
* Whether a conversation with a workspace of its own has ended: a thread closed, a task ended
|
|
6808
7225
|
* (for one from before tasks had threads), a channel archived, or a project archived, which ends
|
|
@@ -6885,11 +7302,11 @@ ${rationale}`}`);
|
|
|
6885
7302
|
}
|
|
6886
7303
|
/** Small named state files under data/state, for cursors the scheduler must keep across restarts. */
|
|
6887
7304
|
async readState(name, schema, fallback) {
|
|
6888
|
-
const file =
|
|
7305
|
+
const file = path8.join(this.paths.state(), `${name}.json`);
|
|
6889
7306
|
return await exists(file) ? readJson(file, schema) : fallback;
|
|
6890
7307
|
}
|
|
6891
7308
|
async writeState(name, value) {
|
|
6892
|
-
await this.mutex.run(() => writeJson(
|
|
7309
|
+
await this.mutex.run(() => writeJson(path8.join(this.paths.state(), `${name}.json`), value));
|
|
6893
7310
|
}
|
|
6894
7311
|
/** The project's dashboard, empty at revision 0 when it has none. */
|
|
6895
7312
|
async readDashboard(slug) {
|
|
@@ -6920,7 +7337,7 @@ ${rationale}`}`);
|
|
|
6920
7337
|
async listRunners() {
|
|
6921
7338
|
const runners = [];
|
|
6922
7339
|
for (const file of await listFiles(this.paths.runners())) {
|
|
6923
|
-
runners.push((await readMarkdown(
|
|
7340
|
+
runners.push((await readMarkdown(path8.join(this.paths.runners(), file), RunnerSchema)).data);
|
|
6924
7341
|
}
|
|
6925
7342
|
return runners;
|
|
6926
7343
|
}
|
|
@@ -6972,7 +7389,7 @@ ${rationale}`}`);
|
|
|
6972
7389
|
return this.runnerTokens.get(hashToken(token)) ?? null;
|
|
6973
7390
|
}
|
|
6974
7391
|
runnerTokensFile() {
|
|
6975
|
-
return
|
|
7392
|
+
return path8.join(this.paths.state(), "runners.json");
|
|
6976
7393
|
}
|
|
6977
7394
|
async readRunnerTokens() {
|
|
6978
7395
|
const file = this.runnerTokensFile();
|
|
@@ -6986,7 +7403,8 @@ ${rationale}`}`);
|
|
|
6986
7403
|
}
|
|
6987
7404
|
/**
|
|
6988
7405
|
* Places a project on the runner that will hold its repository, unless it already lives on one,
|
|
6989
|
-
* and returns where it lives. Placement is once: every later turn of the project goes there
|
|
7406
|
+
* and returns where it lives. Placement is once: every later turn of the project goes there,
|
|
7407
|
+
* until the user moves the project (`recordProjectMove`).
|
|
6990
7408
|
*/
|
|
6991
7409
|
async placeProject(slug, runner2) {
|
|
6992
7410
|
return this.mutex.run(async () => {
|
|
@@ -7028,12 +7446,12 @@ ${rationale}`}`);
|
|
|
7028
7446
|
}
|
|
7029
7447
|
const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
7030
7448
|
if (entry.isDirectory()) {
|
|
7031
|
-
await walk2(
|
|
7449
|
+
await walk2(path8.join(dir, entry.name), relative);
|
|
7032
7450
|
continue;
|
|
7033
7451
|
}
|
|
7034
7452
|
const file = entry.isFile() ? conflictCopyOf(relative) : null;
|
|
7035
7453
|
if (file !== null) {
|
|
7036
|
-
const since = await this.homes.changedAt(home, relative) ?? (await stat3(
|
|
7454
|
+
const since = await this.homes.changedAt(home, relative) ?? (await stat3(path8.join(home, relative)).catch(() => null))?.mtime.toISOString();
|
|
7037
7455
|
if (since !== void 0) {
|
|
7038
7456
|
found.push({ path: relative, file, since });
|
|
7039
7457
|
}
|
|
@@ -7107,6 +7525,74 @@ ${rationale}`}`);
|
|
|
7107
7525
|
return next;
|
|
7108
7526
|
});
|
|
7109
7527
|
}
|
|
7528
|
+
/**
|
|
7529
|
+
* The board's half of moving a project to another runner, which only the user does, checked
|
|
7530
|
+
* before anything travels and while the hub holds the project's turns: the project is active and
|
|
7531
|
+
* lives on a runner other than `to`, which the board knows; no task there is landing, since its
|
|
7532
|
+
* landing runs on the old runner; and no turn runs in the project. Tasks in play move with their
|
|
7533
|
+
* branches, so the answer lists them for the capabilities the new runner must offer.
|
|
7534
|
+
*/
|
|
7535
|
+
async prepareProjectMove(actor, slug, to) {
|
|
7536
|
+
this.assertUser(actor, "only the user may move a project to another runner");
|
|
7537
|
+
const runner2 = NameSchema.parse(to);
|
|
7538
|
+
return this.mutex.run(async () => {
|
|
7539
|
+
const project2 = await this.readActiveProject(slug);
|
|
7540
|
+
const from = project2.runner;
|
|
7541
|
+
if (from === void 0) {
|
|
7542
|
+
throw new BoardError("INVALID_STATE", `project ${slug} lives on no runner yet; its first turn places it`);
|
|
7543
|
+
}
|
|
7544
|
+
if (from === runner2) {
|
|
7545
|
+
throw new BoardError("INVALID_STATE", `project ${slug} already lives on ${runner2}`);
|
|
7546
|
+
}
|
|
7547
|
+
await this.readRunner(runner2);
|
|
7548
|
+
const inPlay = (await this.listTasks(slug)).filter((task2) => task2.status === "open" || task2.status === "claimed");
|
|
7549
|
+
const landing = inPlay.filter((task2) => task2.completing);
|
|
7550
|
+
if (landing.length > 0) {
|
|
7551
|
+
throw new BoardError("INVALID_STATE", `project ${slug} cannot be moved while ${landing.length} task(s) land on ${from} (${landing.map((task2) => task2.id).join(", ")}); move it once they have landed`);
|
|
7552
|
+
}
|
|
7553
|
+
await this.assertProjectQuiet(actor, slug, `project ${slug} cannot be moved`);
|
|
7554
|
+
return { project: { ...project2, runner: from }, inPlay };
|
|
7555
|
+
});
|
|
7556
|
+
}
|
|
7557
|
+
/**
|
|
7558
|
+
* Records that a project moved, once its new runner holds every branch its old one had, or with
|
|
7559
|
+
* nothing carried when the user moved it while the old runner was away: every later turn of
|
|
7560
|
+
* the project goes to `to`. Refused when the project was archived or moved meanwhile.
|
|
7561
|
+
*/
|
|
7562
|
+
async recordProjectMove(actor, move) {
|
|
7563
|
+
this.assertUser(actor, "only the user may move a project to another runner");
|
|
7564
|
+
return this.mutex.run(async () => {
|
|
7565
|
+
const current = await this.readActiveProject(move.slug);
|
|
7566
|
+
if (current.runner !== move.from) {
|
|
7567
|
+
throw new BoardError("INVALID_STATE", `project ${move.slug} no longer lives on ${move.from}, so this move no longer applies`);
|
|
7568
|
+
}
|
|
7569
|
+
await this.readRunner(move.to);
|
|
7570
|
+
const project2 = await this.updateProject(move.slug, (each) => ({ ...each, runner: move.to }));
|
|
7571
|
+
await this.events.append("project.moved", actor.name, {
|
|
7572
|
+
slug: move.slug,
|
|
7573
|
+
from: move.from,
|
|
7574
|
+
to: move.to,
|
|
7575
|
+
carried: move.carried
|
|
7576
|
+
});
|
|
7577
|
+
return project2;
|
|
7578
|
+
});
|
|
7579
|
+
}
|
|
7580
|
+
/**
|
|
7581
|
+
* Makes the repository a move carries a project through, for the server's git route to serve,
|
|
7582
|
+
* and answers the move's id, which names it; `dropMoveRepo` removes it when the move ends.
|
|
7583
|
+
*/
|
|
7584
|
+
async openMoveRepo() {
|
|
7585
|
+
const id = this.newId();
|
|
7586
|
+
await this.moves.open(this.paths.move(id));
|
|
7587
|
+
return id;
|
|
7588
|
+
}
|
|
7589
|
+
async dropMoveRepo(id) {
|
|
7590
|
+
await this.moves.drop(this.paths.move(UlidSchema.parse(id)));
|
|
7591
|
+
}
|
|
7592
|
+
/** Where the moves' repositories live, which the server's git route serves from. */
|
|
7593
|
+
get moveRepos() {
|
|
7594
|
+
return this.paths.moves();
|
|
7595
|
+
}
|
|
7110
7596
|
/** The projection a runner mirrors for its agents to read, by relative path, with hashes. */
|
|
7111
7597
|
async boardManifest() {
|
|
7112
7598
|
return this.files.manifest(this.paths.board, () => true);
|
|
@@ -7117,7 +7603,7 @@ ${rationale}`}`);
|
|
|
7117
7603
|
async writeTurnRecord(record) {
|
|
7118
7604
|
const dir = this.sessionDir(record.agent, record.project, conversationPart(record));
|
|
7119
7605
|
await ensureDir(dir);
|
|
7120
|
-
await writeJson(
|
|
7606
|
+
await writeJson(path8.join(dir, "last-turn.json"), record);
|
|
7121
7607
|
}
|
|
7122
7608
|
// ---------------------------------------------------------------------------------------------
|
|
7123
7609
|
// Internals
|
|
@@ -7236,8 +7722,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7236
7722
|
async ensureAgentProject(name, slug) {
|
|
7237
7723
|
const dir = this.paths.agentProject(name, slug);
|
|
7238
7724
|
await ensureDir(dir);
|
|
7239
|
-
if (!await exists(
|
|
7240
|
-
await writeJson(
|
|
7725
|
+
if (!await exists(path8.join(dir, "sessions.json"))) {
|
|
7726
|
+
await writeJson(path8.join(dir, "sessions.json"), {});
|
|
7241
7727
|
}
|
|
7242
7728
|
}
|
|
7243
7729
|
/**
|
|
@@ -7479,11 +7965,11 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7479
7965
|
const file = this.paths.societySkillFile(skill2.name);
|
|
7480
7966
|
const replaced = await exists(file);
|
|
7481
7967
|
const taken = proposal2.skillFiles;
|
|
7482
|
-
const staged =
|
|
7968
|
+
const staged = path8.join(this.paths.proposalFiles(proposal2.id), "SKILL.md");
|
|
7483
7969
|
if (taken === void 0 || !await exists(staged)) {
|
|
7484
7970
|
throw new BoardError("INVALID_STATE", `proposal ${proposal2.id} holds no skill files`);
|
|
7485
7971
|
}
|
|
7486
|
-
await
|
|
7972
|
+
await rm3(dir, { recursive: true, force: true });
|
|
7487
7973
|
await cp(this.paths.proposalFiles(proposal2.id), dir, { recursive: true });
|
|
7488
7974
|
const { body } = await readLooseMarkdown(file);
|
|
7489
7975
|
await writeMarkdown(file, {
|
|
@@ -7536,10 +8022,10 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7536
8022
|
return `the frontmatter of ${folder}/SKILL.md in ${proposal2.proposedBy}'s home does not parse (${manual.error})`;
|
|
7537
8023
|
}
|
|
7538
8024
|
const dir = this.paths.proposalFiles(proposal2.id);
|
|
7539
|
-
await
|
|
8025
|
+
await rm3(dir, { recursive: true, force: true });
|
|
7540
8026
|
for (const entry of taken.files) {
|
|
7541
|
-
const target =
|
|
7542
|
-
await ensureDir(
|
|
8027
|
+
const target = path8.join(dir, ...entry.path.split("/"));
|
|
8028
|
+
await ensureDir(path8.dirname(target));
|
|
7543
8029
|
await writeFile4(target, contents.get(entry.path) ?? Buffer.alloc(0));
|
|
7544
8030
|
await chmod2(target, entry.executable ? 493 : 420);
|
|
7545
8031
|
}
|
|
@@ -7630,7 +8116,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7630
8116
|
if (!isSafeRelativePath(entry.path)) {
|
|
7631
8117
|
continue;
|
|
7632
8118
|
}
|
|
7633
|
-
const bytes = await readFile4(
|
|
8119
|
+
const bytes = await readFile4(path8.join(dir, ...entry.path.split("/"))).catch(() => null);
|
|
7634
8120
|
files.push({
|
|
7635
8121
|
...entry,
|
|
7636
8122
|
...bytes === null ? { text: null, truncated: false } : skillFileText(bytes)
|
|
@@ -7779,7 +8265,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7779
8265
|
this.assertNoTurnCut(actor, `${ref} cannot be archived`, (turn2) => turn2.channel === channel && turn2.scope === scope || turn2.thread !== void 0 && closing.has(turn2.thread));
|
|
7780
8266
|
const threadsClosed = [];
|
|
7781
8267
|
for (const file of await listFiles(threads)) {
|
|
7782
|
-
const doc = await readMarkdown(
|
|
8268
|
+
const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
|
|
7783
8269
|
if (doc.data.channel !== ref || doc.data.state !== "open") {
|
|
7784
8270
|
continue;
|
|
7785
8271
|
}
|
|
@@ -7911,7 +8397,7 @@ ${charter.purpose}
|
|
|
7911
8397
|
const threads = this.paths.threads(slug);
|
|
7912
8398
|
const threadsClosed = [];
|
|
7913
8399
|
for (const file of await listFiles(threads)) {
|
|
7914
|
-
const doc = await readMarkdown(
|
|
8400
|
+
const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
|
|
7915
8401
|
if (doc.data.state !== "open") {
|
|
7916
8402
|
continue;
|
|
7917
8403
|
}
|
|
@@ -7986,10 +8472,10 @@ ${charter.purpose}
|
|
|
7986
8472
|
...project2,
|
|
7987
8473
|
members: project2.members.filter((member) => member !== name)
|
|
7988
8474
|
}));
|
|
7989
|
-
const sessions =
|
|
8475
|
+
const sessions = path8.join(this.paths.agentProject(name, slug), "sessions.json");
|
|
7990
8476
|
if (await exists(sessions)) {
|
|
7991
8477
|
const archived = `sessions.archived.${ts.replace(/[:.]/g, "-")}.json`;
|
|
7992
|
-
await rename3(sessions,
|
|
8478
|
+
await rename3(sessions, path8.join(this.paths.agentProject(name, slug), archived));
|
|
7993
8479
|
}
|
|
7994
8480
|
}
|
|
7995
8481
|
this.tokenIndex.delete(current.tokenHash);
|
|
@@ -8103,7 +8589,7 @@ ${charter.purpose}
|
|
|
8103
8589
|
}
|
|
8104
8590
|
assertMayGate(actor, what) {
|
|
8105
8591
|
if (!PLANNING_ROLES.includes(actor.role)) {
|
|
8106
|
-
throw new BoardError("FORBIDDEN", `only the user, the steward, and the concierge may ${what}; ask one of them in the task's thread`);
|
|
8592
|
+
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`);
|
|
8107
8593
|
}
|
|
8108
8594
|
}
|
|
8109
8595
|
/** The user holds anything; everyone else must be a project member the current stage admits. */
|
|
@@ -8145,6 +8631,51 @@ ${charter.purpose}
|
|
|
8145
8631
|
leaseEnd(now) {
|
|
8146
8632
|
return new Date(now.getTime() + this.leaseMs).toISOString();
|
|
8147
8633
|
}
|
|
8634
|
+
/**
|
|
8635
|
+
* The lease a holder's own crons in the task's thread keep its claim to, when later than the one
|
|
8636
|
+
* it has, else null. While one of them is pending, the lease is renewed for a full lease once less
|
|
8637
|
+
* than half is left, so it never lapses between fires that may be hours apart; once every one is
|
|
8638
|
+
* paused, ended, or failed at its latest fire with nothing catching it up (`cronStoppedAt`), it
|
|
8639
|
+
* runs a full lease from when the last of them stopped.
|
|
8640
|
+
*/
|
|
8641
|
+
keptLease(task2, crons, now) {
|
|
8642
|
+
if (task2.leaseExpiresAt === void 0) {
|
|
8643
|
+
return null;
|
|
8644
|
+
}
|
|
8645
|
+
const lease = Date.parse(task2.leaseExpiresAt);
|
|
8646
|
+
let pending = false;
|
|
8647
|
+
let stopped = null;
|
|
8648
|
+
for (const cron2 of crons) {
|
|
8649
|
+
if (cron2.agent !== task2.claimedBy || !firesInTask(cron2, task2)) {
|
|
8650
|
+
continue;
|
|
8651
|
+
}
|
|
8652
|
+
const at = cronStoppedAt(cron2);
|
|
8653
|
+
if (at === null) {
|
|
8654
|
+
pending = true;
|
|
8655
|
+
} else {
|
|
8656
|
+
stopped = Math.max(stopped ?? 0, Date.parse(at));
|
|
8657
|
+
}
|
|
8658
|
+
}
|
|
8659
|
+
const kept = pending ? lease - now.getTime() < this.leaseMs / 2 ? now.getTime() + this.leaseMs : lease : stopped === null ? lease : stopped + this.leaseMs;
|
|
8660
|
+
return kept > lease ? new Date(kept).toISOString() : null;
|
|
8661
|
+
}
|
|
8662
|
+
/**
|
|
8663
|
+
* The lease a holder keeps while the user's pause holds its turns in the task's project
|
|
8664
|
+
* (`heldOn`), when later than the one it has, else null. It is renewed for a full lease once less
|
|
8665
|
+
* than half is left, as for a pending cron, so it never lapses while the holder cannot take a
|
|
8666
|
+
* turn there, and after the resume the holder has at least half a lease to come back to it.
|
|
8667
|
+
*/
|
|
8668
|
+
async pausedLease(task2, runners, runner2, now) {
|
|
8669
|
+
if (task2.claimedBy === void 0 || task2.leaseExpiresAt === void 0) {
|
|
8670
|
+
return null;
|
|
8671
|
+
}
|
|
8672
|
+
const holder = await this.readAgent(task2.claimedBy).catch(() => null);
|
|
8673
|
+
if (holder === null || !heldOn(runners, runner2, holder.cli)) {
|
|
8674
|
+
return null;
|
|
8675
|
+
}
|
|
8676
|
+
const lease = Date.parse(task2.leaseExpiresAt);
|
|
8677
|
+
return lease - now.getTime() < this.leaseMs / 2 ? this.leaseEnd(now) : null;
|
|
8678
|
+
}
|
|
8148
8679
|
leaseExpired(task2, now) {
|
|
8149
8680
|
return task2.leaseExpiresAt !== void 0 && new Date(task2.leaseExpiresAt).getTime() <= now.getTime();
|
|
8150
8681
|
}
|
|
@@ -8257,11 +8788,7 @@ ${charter.purpose}
|
|
|
8257
8788
|
if (task2.claimedBy === actor.name || task2.createdBy === actor.name || task2.stages.some((stage) => stage.agent === actor.name || stage.holders.includes(actor.name))) {
|
|
8258
8789
|
return true;
|
|
8259
8790
|
}
|
|
8260
|
-
|
|
8261
|
-
return false;
|
|
8262
|
-
}
|
|
8263
|
-
const agent2 = await this.readAgent(actor.name);
|
|
8264
|
-
return agent2.memberships.includes(task2.project) && mayHoldStage(actor, task2);
|
|
8791
|
+
return await this.mayTakeStage(actor, task2);
|
|
8265
8792
|
}
|
|
8266
8793
|
if (subject?.kind === "proposal") {
|
|
8267
8794
|
const proposal2 = await this.readProposal(subject.id);
|
|
@@ -8275,6 +8802,34 @@ ${charter.purpose}
|
|
|
8275
8802
|
}
|
|
8276
8803
|
return false;
|
|
8277
8804
|
}
|
|
8805
|
+
/** Whether a task's current stage waits for a holder and the member may take it. */
|
|
8806
|
+
async mayTakeStage(actor, task2) {
|
|
8807
|
+
if (task2.status !== "open" || task2.completing) {
|
|
8808
|
+
return false;
|
|
8809
|
+
}
|
|
8810
|
+
const agent2 = await this.readAgent(actor.name);
|
|
8811
|
+
return agent2.memberships.includes(task2.project) && mayHoldStage(actor, task2);
|
|
8812
|
+
}
|
|
8813
|
+
/**
|
|
8814
|
+
* Which of a thread's posts count toward a reader's heartbeat (see `unreadByConversation`). A
|
|
8815
|
+
* thread, or a task, the board cannot find counts as an open topic's would.
|
|
8816
|
+
*/
|
|
8817
|
+
async threadCount(actor, id) {
|
|
8818
|
+
const found = await this.tryFindThread(id);
|
|
8819
|
+
if (found?.thread.state === "closed") {
|
|
8820
|
+
return "none";
|
|
8821
|
+
}
|
|
8822
|
+
const subject = found?.thread.subject;
|
|
8823
|
+
const task2 = subject?.kind === "task" ? await this.findTask(subject.id).then((location) => location.task, () => null) : null;
|
|
8824
|
+
if (task2 === null) {
|
|
8825
|
+
return "every";
|
|
8826
|
+
}
|
|
8827
|
+
const stageIsTheirs = task2.status === "claimed" ? task2.claimedBy === actor.name : await this.mayTakeStage(actor, task2);
|
|
8828
|
+
if (stageIsTheirs) {
|
|
8829
|
+
return "every";
|
|
8830
|
+
}
|
|
8831
|
+
return task2.createdBy === actor.name ? "asks-user" : "asked";
|
|
8832
|
+
}
|
|
8278
8833
|
async threadScopes() {
|
|
8279
8834
|
const projects = await listDirs(this.paths.projects());
|
|
8280
8835
|
return [this.paths.societyThreads(), ...projects.map((slug) => this.paths.threads(slug))];
|
|
@@ -8330,7 +8885,7 @@ ${charter.purpose}
|
|
|
8330
8885
|
if (since !== null && id <= since || upto !== void 0 && id > upto) {
|
|
8331
8886
|
continue;
|
|
8332
8887
|
}
|
|
8333
|
-
const doc = await readMarkdown(
|
|
8888
|
+
const doc = await readMarkdown(path8.join(dir, file), MessageFrontmatterSchema);
|
|
8334
8889
|
messages.push({ ...doc.data, body: doc.body });
|
|
8335
8890
|
}
|
|
8336
8891
|
return messages;
|
|
@@ -8485,8 +9040,21 @@ runner.command("add <name>").description("Register a runner and print its token
|
|
|
8485
9040
|
runner.command("list").description("List runners and what they offer").action(async () => {
|
|
8486
9041
|
const board = await open3();
|
|
8487
9042
|
const runners = await board.listRunners();
|
|
8488
|
-
print(runners, () => runners.length === 0 ? "No runners yet; add one with `runner add <name>`." : runners.map((each) => `${each.name} ${each.status} ${each.os}${each.version === void 0 ? "" : ` v${each.version}`} ${each.clis.join(",") || "no CLIs yet"}${each.capabilities.length === 0 ? "" : ` ${each.capabilities.join(",")}`}`).join("\n"));
|
|
9043
|
+
print(runners, () => runners.length === 0 ? "No runners yet; add one with `runner add <name>`." : runners.map((each) => `${each.name} ${each.status}${each.paused === void 0 ? "" : " paused"} ${each.os}${each.version === void 0 ? "" : ` v${each.version}`} ${each.clis.join(",") || "no CLIs yet"}${each.capabilities.length === 0 ? "" : ` ${each.capabilities.join(",")}`}${Object.keys(each.pausedClis ?? {}).map((cli) => ` ${cli} paused`).join("")}`).join("\n"));
|
|
8489
9044
|
});
|
|
9045
|
+
for (const [verb, paused] of [
|
|
9046
|
+
["pause", true],
|
|
9047
|
+
["resume", false]
|
|
9048
|
+
]) {
|
|
9049
|
+
runner.command(`${verb} <name> [cli]`).description(paused ? "Start no more turns on a runner, or with one of its CLIs, until resumed; turns in flight finish" : "Let a paused runner, or a paused CLI on it, take turns again").action(async (name, cli) => {
|
|
9050
|
+
const board = await open3();
|
|
9051
|
+
const updated = await board.setRunnerPaused(board.userActor(), name, {
|
|
9052
|
+
paused,
|
|
9053
|
+
...cli === void 0 ? {} : { cli: CliKindSchema.parse(cli) }
|
|
9054
|
+
});
|
|
9055
|
+
print(updated, () => `${cli === void 0 ? name : `${cli} on ${name}`} ${verb}d`);
|
|
9056
|
+
});
|
|
9057
|
+
}
|
|
8490
9058
|
var agent = program.command("agent").description("Manage agents");
|
|
8491
9059
|
agent.command("add <name>").description("Add an agent and print its board token once").requiredOption("--role <role>", "role charter name").option("--cli <cli>", "claude or codex").option("--model <model>", "model to run the CLI with; the CLI's own default otherwise").option("--runner <name>", "runner preferred for its turns; any with its CLI otherwise").option("-p, --project <slug...>", "project memberships").action(async (name, opts) => {
|
|
8492
9060
|
const board = await open3();
|
|
@@ -8852,7 +9420,7 @@ knowledge.command("show <topic>").description("Print a knowledge topic").option(
|
|
|
8852
9420
|
});
|
|
8853
9421
|
knowledge.command("write <topic>").description("Write a knowledge topic as the user, from a file or standard input").requiredOption("--revision <n>", "the revision it replaces, as knowledge list shows it; 0 for a new topic").option("--project <slug>", "the project's topic instead of the society's").option("--file <path>", "read the body from this file instead of standard input").option("--as <agent>", "act as this agent instead of the user").action(async (topic, opts) => {
|
|
8854
9422
|
const board = await open3();
|
|
8855
|
-
const body = opts.file === void 0 ? await readStdin() : await readFile5(
|
|
9423
|
+
const body = opts.file === void 0 ? await readStdin() : await readFile5(path9.resolve(opts.file), "utf8");
|
|
8856
9424
|
const written = await board.writeKnowledge(await actorFor(board, opts.as), {
|
|
8857
9425
|
project: opts.project ?? null,
|
|
8858
9426
|
topic,
|