@kubitnodes/stellaris 0.3.26 → 0.3.27
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 +340 -131
- package/dist/server/index.js +808 -181
- package/package.json +1 -1
- package/web/dist/assets/{BackLink-UTiLF1J4.js → BackLink-qSxOMeEG.js} +1 -1
- package/web/dist/assets/{Button-Cp4cNRUX.js → Button-CxjWy598.js} +1 -1
- package/web/dist/assets/{ChannelForms-DD2B0tey.js → ChannelForms-BVidWMId.js} +1 -1
- package/web/dist/assets/{ChannelView-CY3oG9hZ.js → ChannelView-rqfk1Tyu.js} +1 -1
- package/web/dist/assets/{CitizenView-BNK5ro-6.js → CitizenView-xU99R8hS.js} +1 -1
- package/web/dist/assets/{CitizensView-WPQ6Jhl3.js → CitizensView-Bgw6PRSo.js} +1 -1
- package/web/dist/assets/{CliIcon-C7bwChMP.js → CliIcon-BB2NdLxN.js} +1 -1
- package/web/dist/assets/{CronsView-_pdgwX1I.js → CronsView-BrXrcfC6.js} +1 -1
- package/web/dist/assets/{FileTree-jyDUSu7g.js → FileTree-CHDmrOhY.js} +1 -1
- package/web/dist/assets/{FileView-DcQvlc8V.js → FileView-DUMfKYRW.js} +1 -1
- package/web/dist/assets/{KnowledgeView-DDl0HQPG.js → KnowledgeView-Qog6c92V.js} +1 -1
- package/web/dist/assets/{Markdown-DT3V-EVe.js → Markdown-C7RbUEZE.js} +1 -1
- package/web/dist/assets/{MessageItem-agtEugfG.js → MessageItem-DgWh5pJF.js} +1 -1
- package/web/dist/assets/{MetricsView-DiXyYeWP.js → MetricsView-DuOyISk1.js} +1 -1
- package/web/dist/assets/{NeedsYouView-CKmuYan_.js → NeedsYouView-CrmHXZAn.js} +1 -1
- package/web/dist/assets/{Overview-CKcTqsMM.js → Overview-xatORJK6.js} +1 -1
- package/web/dist/assets/{Pane-BaM0TdE6.js → Pane-BlIRwUtQ.js} +1 -1
- package/web/dist/assets/{Picker-C2ig-0dT.js → Picker-BSOB2JM_.js} +1 -1
- package/web/dist/assets/ProjectView-CVt28SoI.js +1 -0
- package/web/dist/assets/{ProposalView-B5yjTAcn.js → ProposalView-DJ0aJm7J.js} +1 -1
- package/web/dist/assets/{ProposalsView-p4KjwV9p.js → ProposalsView-C0r1ya8_.js} +1 -1
- package/web/dist/assets/{Quota-CfFGIql9.js → Quota-59QJZrPa.js} +1 -1
- package/web/dist/assets/{RunnersView-DdvwoOur.js → RunnersView-CcAAfw6y.js} +1 -1
- package/web/dist/assets/{SkillFileList-CgrCnuaC.js → SkillFileList-Cs_f3RPo.js} +1 -1
- package/web/dist/assets/{SkillView-Di5Z9h2L.js → SkillView-BvVx3LSL.js} +1 -1
- package/web/dist/assets/{SocietyView-BDiVxUC5.js → SocietyView-CJRrypjI.js} +1 -1
- package/web/dist/assets/{StickyList-BaV_KZ8x.js → StickyList-BbGq82Jj.js} +1 -1
- package/web/dist/assets/{SubjectThread-DD_ZCCnr.js → SubjectThread-DdkyhVqK.js} +1 -1
- package/web/dist/assets/{TaskView-CKcs-MOC.js → TaskView-CyEWpCvS.js} +1 -1
- package/web/dist/assets/{TasksView-oZMjncOU.js → TasksView-D10XP1uk.js} +1 -1
- package/web/dist/assets/{ThreadForms-CG5Y926b.js → ThreadForms-D94Jj0ES.js} +1 -1
- package/web/dist/assets/{ThreadItem-D-n2arFA.js → ThreadItem-CYmdCPOf.js} +1 -1
- package/web/dist/assets/{ThreadView-CT9vd0P4.js → ThreadView-D79G2zgF.js} +1 -1
- package/web/dist/assets/{citizen-BiHfph2A.js → citizen-CgA4vJjy.js} +1 -1
- package/web/dist/assets/{compose-DY1WL8zB.js → compose-3ksQaHF6.js} +1 -1
- package/web/dist/assets/format-DpQyO6r1.js +55 -0
- package/web/dist/assets/{governance-C6ez5MLh.js → governance-YialJVYK.js} +1 -1
- package/web/dist/assets/{index-CPXALUV6.js → index-DhuCp-VF.js} +3 -3
- package/web/dist/assets/{mentions-BEL5U6ra.js → mentions-Buc5vs-j.js} +1 -1
- package/web/dist/assets/{tasks-CAI8Eci7.js → tasks-DLTeaHys.js} +1 -1
- package/web/dist/assets/{useMatch-CINzyDWa.js → useMatch-DPEg8ohe.js} +1 -1
- package/web/dist/index.html +8 -8
- package/web/dist/assets/ProjectView-T2OHTh5j.js +0 -1
- package/web/dist/assets/format-CENiwkJC.js +0 -55
package/dist/server/index.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// apps/server/dist/index.js
|
|
4
4
|
import { access as access2 } from "node:fs/promises";
|
|
5
|
-
import
|
|
5
|
+
import path11 from "node:path";
|
|
6
6
|
import { serve } from "@hono/node-server";
|
|
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
|
|
|
@@ -1283,6 +1283,7 @@ var BOARD_EVENT_TYPES = [
|
|
|
1283
1283
|
"project.configured",
|
|
1284
1284
|
"project.archived",
|
|
1285
1285
|
"project.placed",
|
|
1286
|
+
"project.moved",
|
|
1286
1287
|
"agent.added",
|
|
1287
1288
|
"agent.configured",
|
|
1288
1289
|
"agent.placed",
|
|
@@ -1471,6 +1472,11 @@ var RunnerHelloSchema = z9.object({
|
|
|
1471
1472
|
quotaReads: z9.boolean().default(false),
|
|
1472
1473
|
/** Whether the runner reads an image a turn running there uploads; one from before never does. */
|
|
1473
1474
|
imageReads: z9.boolean().default(false),
|
|
1475
|
+
/**
|
|
1476
|
+
* Whether the runner carries a project's repository out to another runner and in from one, and
|
|
1477
|
+
* removes its copy once the project has moved; one from before never does.
|
|
1478
|
+
*/
|
|
1479
|
+
projectMoves: z9.boolean().default(false),
|
|
1474
1480
|
capabilities: z9.array(z9.string()).default([]),
|
|
1475
1481
|
/**
|
|
1476
1482
|
* What the runner's account lacks of the git identity citizens' own commits carry, checked
|
|
@@ -1580,6 +1586,11 @@ var TurnJobSchema = z9.object({
|
|
|
1580
1586
|
session: z9.string().min(1).nullable(),
|
|
1581
1587
|
/** The running total the session reported after its last turn, which a resumed session starts from. */
|
|
1582
1588
|
costSoFarUsd: z9.number().nonnegative(),
|
|
1589
|
+
/**
|
|
1590
|
+
* The session's running usage after its last turn, which a resumed session's own usage is
|
|
1591
|
+
* measured from. Absent when the board holds none for the session, and from a server from before.
|
|
1592
|
+
*/
|
|
1593
|
+
usageSoFar: UsageSchema.optional(),
|
|
1583
1594
|
/** The rendered charter, norms, memory core, and skills index, with `PATH_TOKENS` for places. */
|
|
1584
1595
|
instructions: z9.string(),
|
|
1585
1596
|
prompt: z9.string(),
|
|
@@ -1608,6 +1619,17 @@ var TurnOutcomeSchema = z9.object({
|
|
|
1608
1619
|
costUsd: z9.number().nonnegative(),
|
|
1609
1620
|
/** The session's running total after this turn, for CLIs that report one. */
|
|
1610
1621
|
sessionCostUsd: z9.number().nonnegative().optional(),
|
|
1622
|
+
/**
|
|
1623
|
+
* The session's running usage after this turn, for CLIs that report one, else the job's
|
|
1624
|
+
* `usageSoFar`. A runner from before never sends it.
|
|
1625
|
+
*/
|
|
1626
|
+
sessionUsage: UsageSchema.optional(),
|
|
1627
|
+
/**
|
|
1628
|
+
* The whole input of the turn's latest model call in the session's own thread, cached or not:
|
|
1629
|
+
* how large the session had grown. Sub-agents' calls are left out. A turn that made no call,
|
|
1630
|
+
* and a runner from before, never send it.
|
|
1631
|
+
*/
|
|
1632
|
+
contextTokens: z9.number().int().nonnegative().optional(),
|
|
1611
1633
|
/**
|
|
1612
1634
|
* The session the CLI used, which differs from the job's for CLIs that assign their own ids. A
|
|
1613
1635
|
* turn whose CLI never started, because it failed or was stopped first, reports the job's own,
|
|
@@ -1776,6 +1798,43 @@ var HeldWorkspaceSchema = z9.object({
|
|
|
1776
1798
|
});
|
|
1777
1799
|
var HeldWorkspacesSchema = z9.object({ workspaces: z9.array(HeldWorkspaceSchema) });
|
|
1778
1800
|
var SweepResultSchema = z9.object({ kept: z9.array(HeldWorkspaceSchema) });
|
|
1801
|
+
var MOVE_PACK_LIMIT_BYTES = 256 * 1024 * 1024;
|
|
1802
|
+
var RepoRefsSchema = z9.record(z9.string().regex(/^refs\/(heads|tags)\/\S+$/), z9.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/));
|
|
1803
|
+
var ProjectExportSchema = z9.object({ move: UlidSchema, repo: ProjectRepoSchema });
|
|
1804
|
+
var HeldWorkSchema = z9.object({
|
|
1805
|
+
agent: NameSchema,
|
|
1806
|
+
conversation: WorkspaceConversationSchema.nullable(),
|
|
1807
|
+
uncommitted: z9.number().int().nonnegative(),
|
|
1808
|
+
onNoBranch: z9.number().int().nonnegative()
|
|
1809
|
+
});
|
|
1810
|
+
var ProjectExportedSchema = z9.discriminatedUnion("ok", [
|
|
1811
|
+
z9.object({
|
|
1812
|
+
ok: z9.literal(true),
|
|
1813
|
+
refs: RepoRefsSchema,
|
|
1814
|
+
bytes: z9.number().int().nonnegative()
|
|
1815
|
+
}),
|
|
1816
|
+
z9.object({
|
|
1817
|
+
ok: z9.literal(false),
|
|
1818
|
+
held: z9.array(HeldWorkSchema),
|
|
1819
|
+
bytes: z9.number().int().nonnegative()
|
|
1820
|
+
})
|
|
1821
|
+
]);
|
|
1822
|
+
var ProjectImportSchema = z9.object({
|
|
1823
|
+
move: UlidSchema,
|
|
1824
|
+
repo: ProjectRepoSchema,
|
|
1825
|
+
pullRequests: z9.boolean()
|
|
1826
|
+
});
|
|
1827
|
+
var ProjectImportedSchema = z9.object({ refs: RepoRefsSchema });
|
|
1828
|
+
var ProjectReleaseSchema = z9.object({ slug: NameSchema });
|
|
1829
|
+
var ProjectReleasedSchema = z9.object({
|
|
1830
|
+
worktrees: z9.number().int().nonnegative(),
|
|
1831
|
+
clone: z9.boolean()
|
|
1832
|
+
});
|
|
1833
|
+
var ProjectMovedSchema = z9.object({
|
|
1834
|
+
project: ProjectSchema,
|
|
1835
|
+
carried: z9.number().int().nonnegative().nullable(),
|
|
1836
|
+
released: z9.object({ ok: z9.boolean(), detail: z9.string() }).nullable()
|
|
1837
|
+
});
|
|
1779
1838
|
var TurnSteerSchema = z9.object({ id: z9.uuid(), text: z9.string().min(1) });
|
|
1780
1839
|
var RunnerMessageSchema = z9.discriminatedUnion("type", [
|
|
1781
1840
|
z9.object({ type: z9.literal("turn"), job: TurnJobSchema }),
|
|
@@ -1812,6 +1871,19 @@ var RunnerMessageSchema = z9.discriminatedUnion("type", [
|
|
|
1812
1871
|
}),
|
|
1813
1872
|
/** Answered with whether the turn was there to stop; it ends `stopped`. */
|
|
1814
1873
|
z9.object({ type: z9.literal("stop"), request: z9.string().min(1), turnId: UlidSchema }),
|
|
1874
|
+
/**
|
|
1875
|
+
* Answered with a `ProjectExported`. Sent only to runners whose hello says `projectMoves`, as are
|
|
1876
|
+
* the import and the release.
|
|
1877
|
+
*/
|
|
1878
|
+
z9.object({ type: z9.literal("export"), request: z9.string().min(1), export: ProjectExportSchema }),
|
|
1879
|
+
/** Answered with a `ProjectImported`. */
|
|
1880
|
+
z9.object({ type: z9.literal("import"), request: z9.string().min(1), import: ProjectImportSchema }),
|
|
1881
|
+
/** Answered with a `ProjectReleased`. */
|
|
1882
|
+
z9.object({
|
|
1883
|
+
type: z9.literal("release"),
|
|
1884
|
+
request: z9.string().min(1),
|
|
1885
|
+
release: ProjectReleaseSchema
|
|
1886
|
+
}),
|
|
1815
1887
|
/**
|
|
1816
1888
|
* A conversation ended: the runner removes every citizen's workspace for it but those a turn
|
|
1817
1889
|
* still uses, and answers with a `SweepResult` naming those it kept for holding work on no branch.
|
|
@@ -1942,6 +2014,16 @@ var TurnRecordSchema = z10.object({
|
|
|
1942
2014
|
* resumed session starts from it, so the next turn's own cost is measured from here.
|
|
1943
2015
|
*/
|
|
1944
2016
|
sessionCostUsd: z10.number().nonnegative().optional(),
|
|
2017
|
+
/**
|
|
2018
|
+
* The running usage the CLI reported for the session after this turn, when the runner reports
|
|
2019
|
+
* one. A resumed session's next turn measures its own usage from here.
|
|
2020
|
+
*/
|
|
2021
|
+
sessionUsage: UsageSchema.optional(),
|
|
2022
|
+
/**
|
|
2023
|
+
* The whole input of the turn's latest model call in the session's own thread, as the runner
|
|
2024
|
+
* reported it: how large the session had grown. Absent for a turn that made no call.
|
|
2025
|
+
*/
|
|
2026
|
+
contextTokens: z10.number().int().nonnegative().optional(),
|
|
1945
2027
|
toolCalls: z10.number().int().nonnegative().default(0),
|
|
1946
2028
|
/** The model the CLI reported for this turn, else the model configured for the agent, else null. */
|
|
1947
2029
|
model: z10.string().nullable().default(null),
|
|
@@ -1955,7 +2037,10 @@ var TurnRecordSchema = z10.object({
|
|
|
1955
2037
|
transient: z10.boolean().optional(),
|
|
1956
2038
|
/** How many turns in a row, this one included, could not resume the session for a `retry`. */
|
|
1957
2039
|
resumeRetries: z10.number().int().positive().optional(),
|
|
1958
|
-
/**
|
|
2040
|
+
/**
|
|
2041
|
+
* Whether the server dropped the conversation's session after this turn, which the next starts
|
|
2042
|
+
* anew: one its CLI could not resume, or a society role's home after a reflection completed.
|
|
2043
|
+
*/
|
|
1959
2044
|
sessionDropped: z10.boolean().optional(),
|
|
1960
2045
|
/** The crons whose fire started the turn, which record how it ended. */
|
|
1961
2046
|
crons: z10.array(UlidSchema).optional()
|
|
@@ -2171,11 +2256,17 @@ var TokenUseSchema = z12.object({
|
|
|
2171
2256
|
// packages/shared/dist/config.js
|
|
2172
2257
|
import { z as z13 } from "zod";
|
|
2173
2258
|
var LogLevelSchema = z13.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
|
|
2259
|
+
var AppleAppIdSchema = z13.string().regex(/^[A-Z0-9]{10}\.[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*$/, "not <team id>.<bundle id>");
|
|
2174
2260
|
var GithubSignInSchema = z13.object({
|
|
2175
2261
|
clientId: z13.string().min(1),
|
|
2176
2262
|
clientSecret: z13.string().min(1),
|
|
2177
2263
|
/** Logins, lowercased; GitHub logins are case-insensitive. */
|
|
2178
|
-
users: z13.array(z13.string().min(1)).min(1)
|
|
2264
|
+
users: z13.array(z13.string().min(1)).min(1),
|
|
2265
|
+
/**
|
|
2266
|
+
* The iOS app that may sign in, which Apple's association file names so that only it can take the
|
|
2267
|
+
* board's `https` landing; null when no app signs in.
|
|
2268
|
+
*/
|
|
2269
|
+
appleAppId: AppleAppIdSchema.nullable()
|
|
2179
2270
|
});
|
|
2180
2271
|
var ServerConfigSchema = z13.object({
|
|
2181
2272
|
dataDir: z13.string().min(1),
|
|
@@ -2216,13 +2307,20 @@ function githubSignIn(env) {
|
|
|
2216
2307
|
const clientId = env["STELLARIS_GITHUB_CLIENT_ID"] ?? "";
|
|
2217
2308
|
const clientSecret = env["STELLARIS_GITHUB_CLIENT_SECRET"] ?? "";
|
|
2218
2309
|
const users = list(env["STELLARIS_GITHUB_USERS"]).map((login) => login.toLowerCase());
|
|
2310
|
+
const appleAppId = env["STELLARIS_APPLE_APP_ID"]?.trim() || null;
|
|
2311
|
+
if (appleAppId !== null && !AppleAppIdSchema.safeParse(appleAppId).success) {
|
|
2312
|
+
throw new Error(`STELLARIS_APPLE_APP_ID is <team id>.<bundle id>, not ${JSON.stringify(appleAppId)}`);
|
|
2313
|
+
}
|
|
2219
2314
|
if (clientId === "" && clientSecret === "" && users.length === 0) {
|
|
2315
|
+
if (appleAppId !== null) {
|
|
2316
|
+
throw new Error("STELLARIS_APPLE_APP_ID needs GitHub sign-in");
|
|
2317
|
+
}
|
|
2220
2318
|
return null;
|
|
2221
2319
|
}
|
|
2222
2320
|
if (clientId === "" || clientSecret === "" || users.length === 0) {
|
|
2223
2321
|
throw new Error("GitHub sign-in needs STELLARIS_GITHUB_CLIENT_ID, STELLARIS_GITHUB_CLIENT_SECRET, and STELLARIS_GITHUB_USERS together");
|
|
2224
2322
|
}
|
|
2225
|
-
return GithubSignInSchema.parse({ clientId, clientSecret, users });
|
|
2323
|
+
return GithubSignInSchema.parse({ clientId, clientSecret, users, appleAppId });
|
|
2226
2324
|
}
|
|
2227
2325
|
function loadServerConfig(env) {
|
|
2228
2326
|
return ServerConfigSchema.parse({
|
|
@@ -2257,7 +2355,7 @@ var RunnerConfigSchema = z13.object({
|
|
|
2257
2355
|
});
|
|
2258
2356
|
|
|
2259
2357
|
// packages/shared/dist/version.js
|
|
2260
|
-
var STELLARIS_VERSION = "0.3.
|
|
2358
|
+
var STELLARIS_VERSION = "0.3.27";
|
|
2261
2359
|
|
|
2262
2360
|
// packages/shared/dist/mentions.js
|
|
2263
2361
|
import { parse, postprocess, preprocess } from "micromark";
|
|
@@ -2952,6 +3050,41 @@ function filesOf(lines, status) {
|
|
|
2952
3050
|
return [...files.values()];
|
|
2953
3051
|
}
|
|
2954
3052
|
|
|
3053
|
+
// packages/board-core/dist/moves.js
|
|
3054
|
+
import { mkdir as mkdir4, rm as rm2 } from "node:fs/promises";
|
|
3055
|
+
import path5 from "node:path";
|
|
3056
|
+
import { execa as execa2 } from "execa";
|
|
3057
|
+
var MoveRepos = class {
|
|
3058
|
+
root;
|
|
3059
|
+
constructor(root) {
|
|
3060
|
+
this.root = root;
|
|
3061
|
+
}
|
|
3062
|
+
/** Makes the move's empty repository, which checks every object a push brings, and answers its path. */
|
|
3063
|
+
async open(dir) {
|
|
3064
|
+
await mkdir4(this.root, { recursive: true });
|
|
3065
|
+
await git2(["init", "--quiet", "--bare", dir], this.root);
|
|
3066
|
+
await git2(["config", "receive.fsckObjects", "true"], dir);
|
|
3067
|
+
return dir;
|
|
3068
|
+
}
|
|
3069
|
+
async drop(dir) {
|
|
3070
|
+
await rm2(path5.resolve(this.root, dir), { recursive: true, force: true });
|
|
3071
|
+
}
|
|
3072
|
+
/** Removes every move's repository, as the server starts: a move never survives a restart. */
|
|
3073
|
+
async clear() {
|
|
3074
|
+
await rm2(this.root, { recursive: true, force: true });
|
|
3075
|
+
}
|
|
3076
|
+
};
|
|
3077
|
+
async function git2(args, cwd) {
|
|
3078
|
+
const result = await execa2("git", args, {
|
|
3079
|
+
cwd,
|
|
3080
|
+
reject: false,
|
|
3081
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
3082
|
+
});
|
|
3083
|
+
if (result.exitCode !== 0) {
|
|
3084
|
+
throw new Error(`git ${args[0] ?? ""} failed: ${result.stderr.trim()}`);
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
|
|
2955
3088
|
// packages/board-core/dist/mentions.js
|
|
2956
3089
|
import vm from "node:vm";
|
|
2957
3090
|
var MENTION_PARSE_MS = 500;
|
|
@@ -3365,121 +3498,121 @@ var Mutex = class {
|
|
|
3365
3498
|
};
|
|
3366
3499
|
|
|
3367
3500
|
// packages/board-core/dist/paths.js
|
|
3368
|
-
import
|
|
3501
|
+
import path6 from "node:path";
|
|
3369
3502
|
var BoardPaths = class {
|
|
3370
3503
|
dataDir;
|
|
3371
3504
|
constructor(dataDir) {
|
|
3372
|
-
this.dataDir =
|
|
3505
|
+
this.dataDir = path6.resolve(dataDir);
|
|
3373
3506
|
}
|
|
3374
3507
|
// Board projection, also the storage of record; only the core library writes it.
|
|
3375
3508
|
get board() {
|
|
3376
|
-
return
|
|
3509
|
+
return path6.join(this.dataDir, "board");
|
|
3377
3510
|
}
|
|
3378
3511
|
get society() {
|
|
3379
|
-
return
|
|
3512
|
+
return path6.join(this.board, "society");
|
|
3380
3513
|
}
|
|
3381
3514
|
societyFile() {
|
|
3382
|
-
return
|
|
3515
|
+
return path6.join(this.society, "society.md");
|
|
3383
3516
|
}
|
|
3384
3517
|
societyChannels() {
|
|
3385
|
-
return
|
|
3518
|
+
return path6.join(this.society, "channels");
|
|
3386
3519
|
}
|
|
3387
3520
|
societyChannel(name) {
|
|
3388
|
-
return
|
|
3521
|
+
return path6.join(this.societyChannels(), name);
|
|
3389
3522
|
}
|
|
3390
3523
|
/** Threads whose channel is a society channel; project channels' threads live under the project. */
|
|
3391
3524
|
societyThreads() {
|
|
3392
|
-
return
|
|
3525
|
+
return path6.join(this.society, "threads");
|
|
3393
3526
|
}
|
|
3394
3527
|
societyKnowledge() {
|
|
3395
|
-
return
|
|
3528
|
+
return path6.join(this.society, "knowledge");
|
|
3396
3529
|
}
|
|
3397
3530
|
societyKnowledgeFile(topic) {
|
|
3398
|
-
return
|
|
3531
|
+
return path6.join(this.societyKnowledge(), `${topic}.md`);
|
|
3399
3532
|
}
|
|
3400
3533
|
/** Skills promoted to the society, one directory per skill holding SKILL.md. */
|
|
3401
3534
|
societySkills() {
|
|
3402
|
-
return
|
|
3535
|
+
return path6.join(this.society, "skills");
|
|
3403
3536
|
}
|
|
3404
3537
|
societySkill(name) {
|
|
3405
|
-
return
|
|
3538
|
+
return path6.join(this.societySkills(), name);
|
|
3406
3539
|
}
|
|
3407
3540
|
societySkillFile(name) {
|
|
3408
|
-
return
|
|
3541
|
+
return path6.join(this.societySkill(name), "SKILL.md");
|
|
3409
3542
|
}
|
|
3410
3543
|
roles() {
|
|
3411
|
-
return
|
|
3544
|
+
return path6.join(this.society, "roles");
|
|
3412
3545
|
}
|
|
3413
3546
|
role(name) {
|
|
3414
|
-
return
|
|
3547
|
+
return path6.join(this.roles(), `${name}.md`);
|
|
3415
3548
|
}
|
|
3416
3549
|
proposals() {
|
|
3417
|
-
return
|
|
3550
|
+
return path6.join(this.society, "proposals");
|
|
3418
3551
|
}
|
|
3419
3552
|
proposal(id) {
|
|
3420
|
-
return
|
|
3553
|
+
return path6.join(this.proposals(), `${id}.md`);
|
|
3421
3554
|
}
|
|
3422
3555
|
/** A skill proposal's folder, as the board took it from its proposer's home. */
|
|
3423
3556
|
proposalFiles(id) {
|
|
3424
|
-
return
|
|
3557
|
+
return path6.join(this.proposals(), id);
|
|
3425
3558
|
}
|
|
3426
3559
|
decisions() {
|
|
3427
|
-
return
|
|
3560
|
+
return path6.join(this.society, "decisions");
|
|
3428
3561
|
}
|
|
3429
3562
|
decision(id) {
|
|
3430
|
-
return
|
|
3563
|
+
return path6.join(this.decisions(), `${id}.md`);
|
|
3431
3564
|
}
|
|
3432
3565
|
runners() {
|
|
3433
|
-
return
|
|
3566
|
+
return path6.join(this.society, "runners");
|
|
3434
3567
|
}
|
|
3435
3568
|
runner(name) {
|
|
3436
|
-
return
|
|
3569
|
+
return path6.join(this.runners(), `${name}.md`);
|
|
3437
3570
|
}
|
|
3438
3571
|
members() {
|
|
3439
|
-
return
|
|
3572
|
+
return path6.join(this.society, "members");
|
|
3440
3573
|
}
|
|
3441
3574
|
/** Crons, one file each with its note as the body; ended ones stay. */
|
|
3442
3575
|
crons() {
|
|
3443
|
-
return
|
|
3576
|
+
return path6.join(this.society, "crons");
|
|
3444
3577
|
}
|
|
3445
3578
|
cron(id) {
|
|
3446
|
-
return
|
|
3579
|
+
return path6.join(this.crons(), `${id}.md`);
|
|
3447
3580
|
}
|
|
3448
3581
|
member(name) {
|
|
3449
|
-
return
|
|
3582
|
+
return path6.join(this.members(), `${name}.md`);
|
|
3450
3583
|
}
|
|
3451
3584
|
projects() {
|
|
3452
|
-
return
|
|
3585
|
+
return path6.join(this.board, "projects");
|
|
3453
3586
|
}
|
|
3454
3587
|
project(slug) {
|
|
3455
|
-
return
|
|
3588
|
+
return path6.join(this.projects(), slug);
|
|
3456
3589
|
}
|
|
3457
3590
|
projectFile(slug) {
|
|
3458
|
-
return
|
|
3591
|
+
return path6.join(this.project(slug), "project.md");
|
|
3459
3592
|
}
|
|
3460
3593
|
projectChannels(slug) {
|
|
3461
|
-
return
|
|
3594
|
+
return path6.join(this.project(slug), "channels");
|
|
3462
3595
|
}
|
|
3463
3596
|
projectChannel(slug, name) {
|
|
3464
|
-
return
|
|
3597
|
+
return path6.join(this.projectChannels(slug), name);
|
|
3465
3598
|
}
|
|
3466
3599
|
threads(slug) {
|
|
3467
|
-
return
|
|
3600
|
+
return path6.join(this.project(slug), "threads");
|
|
3468
3601
|
}
|
|
3469
3602
|
tasks(slug) {
|
|
3470
|
-
return
|
|
3603
|
+
return path6.join(this.project(slug), "tasks");
|
|
3471
3604
|
}
|
|
3472
3605
|
task(slug, id) {
|
|
3473
|
-
return
|
|
3606
|
+
return path6.join(this.tasks(slug), `${id}.md`);
|
|
3474
3607
|
}
|
|
3475
3608
|
projectKnowledge(slug) {
|
|
3476
|
-
return
|
|
3609
|
+
return path6.join(this.project(slug), "knowledge");
|
|
3477
3610
|
}
|
|
3478
3611
|
projectKnowledgeFile(slug, topic) {
|
|
3479
|
-
return
|
|
3612
|
+
return path6.join(this.projectKnowledge(slug), `${topic}.md`);
|
|
3480
3613
|
}
|
|
3481
3614
|
dashboard(slug) {
|
|
3482
|
-
return
|
|
3615
|
+
return path6.join(this.project(slug), "dashboard.md");
|
|
3483
3616
|
}
|
|
3484
3617
|
channelDir(ref) {
|
|
3485
3618
|
const parsed = parseChannelRef(ref);
|
|
@@ -3492,57 +3625,57 @@ var BoardPaths = class {
|
|
|
3492
3625
|
}
|
|
3493
3626
|
/** A thread's record, `<id>.md`, beside the directory of its messages, `<id>/`. */
|
|
3494
3627
|
threadFile(threads, id) {
|
|
3495
|
-
return
|
|
3628
|
+
return path6.join(threads, `${id}.md`);
|
|
3496
3629
|
}
|
|
3497
3630
|
threadMessages(threads, id) {
|
|
3498
|
-
return
|
|
3631
|
+
return path6.join(threads, id);
|
|
3499
3632
|
}
|
|
3500
3633
|
messageFile(dir, id, author) {
|
|
3501
|
-
return
|
|
3634
|
+
return path6.join(dir, `${id}-${author}.md`);
|
|
3502
3635
|
}
|
|
3503
3636
|
// Agent homes: authored by the agent, source of truth on the board server.
|
|
3504
3637
|
agents() {
|
|
3505
|
-
return
|
|
3638
|
+
return path6.join(this.dataDir, "agents");
|
|
3506
3639
|
}
|
|
3507
3640
|
agent(name) {
|
|
3508
|
-
return
|
|
3641
|
+
return path6.join(this.agents(), name);
|
|
3509
3642
|
}
|
|
3510
3643
|
agentFile(name) {
|
|
3511
|
-
return
|
|
3644
|
+
return path6.join(this.agent(name), "agent.json");
|
|
3512
3645
|
}
|
|
3513
3646
|
agentRole(name) {
|
|
3514
|
-
return
|
|
3647
|
+
return path6.join(this.agent(name), "role.md");
|
|
3515
3648
|
}
|
|
3516
3649
|
agentProfile(name) {
|
|
3517
|
-
return
|
|
3650
|
+
return path6.join(this.agent(name), "profile.md");
|
|
3518
3651
|
}
|
|
3519
3652
|
agentMemory(name) {
|
|
3520
|
-
return
|
|
3653
|
+
return path6.join(this.agent(name), "memory");
|
|
3521
3654
|
}
|
|
3522
3655
|
agentMemoryCore(name) {
|
|
3523
|
-
return
|
|
3656
|
+
return path6.join(this.agentMemory(name), "core.md");
|
|
3524
3657
|
}
|
|
3525
3658
|
/** Transcripts of the agent's finished turns, one JSON line per step. */
|
|
3526
3659
|
agentTurns(name) {
|
|
3527
|
-
return
|
|
3660
|
+
return path6.join(this.agent(name), "turns");
|
|
3528
3661
|
}
|
|
3529
3662
|
agentTranscript(name, turnId) {
|
|
3530
|
-
return
|
|
3663
|
+
return path6.join(this.agentTurns(name), `${turnId}.jsonl`);
|
|
3531
3664
|
}
|
|
3532
3665
|
agentSkills(name) {
|
|
3533
|
-
return
|
|
3666
|
+
return path6.join(this.agent(name), "skills");
|
|
3534
3667
|
}
|
|
3535
3668
|
agentSkill(name, skill) {
|
|
3536
|
-
return
|
|
3669
|
+
return path6.join(this.agentSkills(name), skill);
|
|
3537
3670
|
}
|
|
3538
3671
|
agentSkillFile(name, skill) {
|
|
3539
|
-
return
|
|
3672
|
+
return path6.join(this.agentSkill(name, skill), "SKILL.md");
|
|
3540
3673
|
}
|
|
3541
3674
|
agentProjects(name) {
|
|
3542
|
-
return
|
|
3675
|
+
return path6.join(this.agent(name), "projects");
|
|
3543
3676
|
}
|
|
3544
3677
|
agentProject(name, slug) {
|
|
3545
|
-
return
|
|
3678
|
+
return path6.join(this.agentProjects(name), slug);
|
|
3546
3679
|
}
|
|
3547
3680
|
/**
|
|
3548
3681
|
* A thread's, a channel's, or, in the society scope, a project's conversation's session and last
|
|
@@ -3550,46 +3683,56 @@ var BoardPaths = class {
|
|
|
3550
3683
|
*/
|
|
3551
3684
|
agentConversation(name, slug, part) {
|
|
3552
3685
|
if (part.startsWith("#")) {
|
|
3553
|
-
return
|
|
3686
|
+
return path6.join(this.agentProject(name, slug), "channels", part.slice(1));
|
|
3554
3687
|
}
|
|
3555
|
-
return part.startsWith("~") ?
|
|
3688
|
+
return part.startsWith("~") ? path6.join(this.agentProject(name, slug), "places", part.slice(1)) : path6.join(this.agentProject(name, slug), "threads", part);
|
|
3556
3689
|
}
|
|
3557
3690
|
agentCursors(name) {
|
|
3558
|
-
return
|
|
3691
|
+
return path6.join(this.agent(name), "cursors.json");
|
|
3559
3692
|
}
|
|
3560
3693
|
events() {
|
|
3561
|
-
return
|
|
3694
|
+
return path6.join(this.dataDir, "events");
|
|
3562
3695
|
}
|
|
3563
3696
|
eventLog() {
|
|
3564
|
-
return
|
|
3697
|
+
return path6.join(this.events(), "log.jsonl");
|
|
3565
3698
|
}
|
|
3566
3699
|
/** Images citizens uploaded, by id: outside the projection, which every runner mirrors. */
|
|
3567
3700
|
images() {
|
|
3568
|
-
return
|
|
3701
|
+
return path6.join(this.dataDir, "images");
|
|
3569
3702
|
}
|
|
3570
3703
|
image(id) {
|
|
3571
|
-
return
|
|
3704
|
+
return path6.join(this.images(), id);
|
|
3705
|
+
}
|
|
3706
|
+
/**
|
|
3707
|
+
* The repositories projects travel through between runners, one per move while it runs:
|
|
3708
|
+
* outside the projection, which every runner mirrors, and served only to the move's runners.
|
|
3709
|
+
*/
|
|
3710
|
+
moves() {
|
|
3711
|
+
return path6.join(this.dataDir, "moves");
|
|
3712
|
+
}
|
|
3713
|
+
move(id) {
|
|
3714
|
+
return path6.join(this.moves(), `${id}.git`);
|
|
3572
3715
|
}
|
|
3573
3716
|
state() {
|
|
3574
|
-
return
|
|
3717
|
+
return path6.join(this.dataDir, "state");
|
|
3575
3718
|
}
|
|
3576
3719
|
pausedFile() {
|
|
3577
|
-
return
|
|
3720
|
+
return path6.join(this.state(), "paused.json");
|
|
3578
3721
|
}
|
|
3579
3722
|
/** Removed knowledge topics, set aside outside the projection, by scope. */
|
|
3580
3723
|
removedKnowledge(scope) {
|
|
3581
|
-
return
|
|
3724
|
+
return path6.join(this.state(), "removed-knowledge", scope);
|
|
3582
3725
|
}
|
|
3583
3726
|
/** Skills removed from the society, set aside outside the projection, one directory per removal. */
|
|
3584
3727
|
removedSkills() {
|
|
3585
|
-
return
|
|
3728
|
+
return path6.join(this.state(), "removed-skills");
|
|
3586
3729
|
}
|
|
3587
3730
|
};
|
|
3588
3731
|
|
|
3589
3732
|
// packages/board-core/dist/skill-folder.js
|
|
3590
3733
|
import { constants } from "node:fs";
|
|
3591
3734
|
import { lstat, open as open2, readdir as readdir3, readlink, realpath } from "node:fs/promises";
|
|
3592
|
-
import
|
|
3735
|
+
import path7 from "node:path";
|
|
3593
3736
|
var SKILL_TEXT_LIMIT_CHARS = 1e5;
|
|
3594
3737
|
var SKILL_TEXT_LIMIT_BYTES = SKILL_TEXT_LIMIT_CHARS * 4;
|
|
3595
3738
|
function skillFileText(bytes) {
|
|
@@ -3600,8 +3743,8 @@ function skillFileText(bytes) {
|
|
|
3600
3743
|
};
|
|
3601
3744
|
}
|
|
3602
3745
|
function inside(root, file) {
|
|
3603
|
-
const relative =
|
|
3604
|
-
return relative !== ".." && !relative.startsWith(`..${
|
|
3746
|
+
const relative = path7.relative(root, file);
|
|
3747
|
+
return relative !== ".." && !relative.startsWith(`..${path7.sep}`) && !path7.isAbsolute(relative);
|
|
3605
3748
|
}
|
|
3606
3749
|
async function handleInside(root, handle) {
|
|
3607
3750
|
if (process.platform !== "linux") {
|
|
@@ -3611,7 +3754,7 @@ async function handleInside(root, handle) {
|
|
|
3611
3754
|
return file !== null && inside(root, file);
|
|
3612
3755
|
}
|
|
3613
3756
|
async function walk(root, prefix, found) {
|
|
3614
|
-
const entries = await readdir3(
|
|
3757
|
+
const entries = await readdir3(path7.join(root, ...prefix === "" ? [] : prefix.split("/")), {
|
|
3615
3758
|
withFileTypes: true
|
|
3616
3759
|
}).catch(() => []);
|
|
3617
3760
|
for (const entry of entries.toSorted((a, b) => a.name < b.name ? -1 : 1)) {
|
|
@@ -3632,7 +3775,7 @@ async function walk(root, prefix, found) {
|
|
|
3632
3775
|
async function readEntry(root, relative) {
|
|
3633
3776
|
let real;
|
|
3634
3777
|
try {
|
|
3635
|
-
real = await realpath(
|
|
3778
|
+
real = await realpath(path7.join(root, ...relative.split("/")));
|
|
3636
3779
|
} catch {
|
|
3637
3780
|
return null;
|
|
3638
3781
|
}
|
|
@@ -3948,9 +4091,11 @@ var Board = class _Board {
|
|
|
3948
4091
|
signIns = /* @__PURE__ */ new Map();
|
|
3949
4092
|
files = new FileTree();
|
|
3950
4093
|
homes;
|
|
4094
|
+
moves;
|
|
3951
4095
|
constructor(dataDir, options) {
|
|
3952
4096
|
this.paths = new BoardPaths(dataDir);
|
|
3953
|
-
this.homes = new HomeRepos(
|
|
4097
|
+
this.homes = new HomeRepos(path8.join(this.paths.state(), "home-hooks"));
|
|
4098
|
+
this.moves = new MoveRepos(this.paths.moves());
|
|
3954
4099
|
this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
3955
4100
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3956
4101
|
this.events = new EventLog(this.paths.eventLog(), () => this.newId(), this.now);
|
|
@@ -3976,6 +4121,7 @@ var Board = class _Board {
|
|
|
3976
4121
|
await board2.loadRunnerTokens();
|
|
3977
4122
|
await board2.loadSignIns();
|
|
3978
4123
|
await board2.homes.installHooks();
|
|
4124
|
+
await board2.moves.clear();
|
|
3979
4125
|
return board2;
|
|
3980
4126
|
}
|
|
3981
4127
|
/**
|
|
@@ -3985,7 +4131,7 @@ var Board = class _Board {
|
|
|
3985
4131
|
*/
|
|
3986
4132
|
async applyAlignments() {
|
|
3987
4133
|
const alignments = [];
|
|
3988
|
-
const file =
|
|
4134
|
+
const file = path8.join(this.paths.state(), "alignments.json");
|
|
3989
4135
|
const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
|
|
3990
4136
|
const pending = alignments.filter(({ name }) => !applied.includes(name));
|
|
3991
4137
|
if (pending.length === 0) {
|
|
@@ -4193,7 +4339,7 @@ ${charter.purpose}
|
|
|
4193
4339
|
});
|
|
4194
4340
|
}
|
|
4195
4341
|
signInsFile() {
|
|
4196
|
-
return
|
|
4342
|
+
return path8.join(this.paths.state(), "sign-ins.json");
|
|
4197
4343
|
}
|
|
4198
4344
|
async readSignIns() {
|
|
4199
4345
|
const file = this.signInsFile();
|
|
@@ -4290,7 +4436,7 @@ ${charter.purpose}
|
|
|
4290
4436
|
const files = await listFiles(this.paths.tasks(project));
|
|
4291
4437
|
const tasks = [];
|
|
4292
4438
|
for (const file of files) {
|
|
4293
|
-
const doc = await readMarkdown(
|
|
4439
|
+
const doc = await readMarkdown(path8.join(this.paths.tasks(project), file), TaskFrontmatterSchema);
|
|
4294
4440
|
tasks.push({ ...doc.data, body: doc.body });
|
|
4295
4441
|
}
|
|
4296
4442
|
return tasks;
|
|
@@ -4309,7 +4455,7 @@ ${charter.purpose}
|
|
|
4309
4455
|
const files = await listFiles(this.paths.proposals());
|
|
4310
4456
|
const proposals = [];
|
|
4311
4457
|
for (const file of files) {
|
|
4312
|
-
const doc = await readMarkdown(
|
|
4458
|
+
const doc = await readMarkdown(path8.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
|
|
4313
4459
|
proposals.push({ ...doc.data, body: doc.body });
|
|
4314
4460
|
}
|
|
4315
4461
|
return proposals;
|
|
@@ -4336,7 +4482,7 @@ ${charter.purpose}
|
|
|
4336
4482
|
async listMembers() {
|
|
4337
4483
|
const members = [];
|
|
4338
4484
|
for (const file of await listFiles(this.paths.members())) {
|
|
4339
|
-
const doc = await readMarkdown(
|
|
4485
|
+
const doc = await readMarkdown(path8.join(this.paths.members(), file), MemberSchema);
|
|
4340
4486
|
members.push({ ...doc.data, profile: doc.body });
|
|
4341
4487
|
}
|
|
4342
4488
|
return members;
|
|
@@ -4355,7 +4501,7 @@ ${charter.purpose}
|
|
|
4355
4501
|
const dir = project === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project);
|
|
4356
4502
|
const topics = [];
|
|
4357
4503
|
for (const file of await listFiles(dir)) {
|
|
4358
|
-
topics.push(await this.knowledgeAt(
|
|
4504
|
+
topics.push(await this.knowledgeAt(path8.join(dir, file), project, file.replace(/\.md$/, "")));
|
|
4359
4505
|
}
|
|
4360
4506
|
return topics;
|
|
4361
4507
|
}
|
|
@@ -4385,7 +4531,7 @@ ${charter.purpose}
|
|
|
4385
4531
|
const aside = this.paths.removedKnowledge(project ?? SOCIETY_SCOPE);
|
|
4386
4532
|
let highest = 0;
|
|
4387
4533
|
for (const file of (await listFiles(aside)).filter((name) => name.startsWith(`${topic}.`))) {
|
|
4388
|
-
const copy = await this.knowledgeAt(
|
|
4534
|
+
const copy = await this.knowledgeAt(path8.join(aside, file), project, topic);
|
|
4389
4535
|
highest = Math.max(highest, copy.revision);
|
|
4390
4536
|
}
|
|
4391
4537
|
return highest;
|
|
@@ -4401,7 +4547,7 @@ ${charter.purpose}
|
|
|
4401
4547
|
async listSkillsIn(dir, scope) {
|
|
4402
4548
|
const skills = [];
|
|
4403
4549
|
for (const entry of await listDirs(dir)) {
|
|
4404
|
-
const file =
|
|
4550
|
+
const file = path8.join(dir, entry, "SKILL.md");
|
|
4405
4551
|
if (!await exists(file)) {
|
|
4406
4552
|
continue;
|
|
4407
4553
|
}
|
|
@@ -4491,7 +4637,7 @@ ${charter.purpose}
|
|
|
4491
4637
|
if (!wanted.delete(file.slice(0, 26))) {
|
|
4492
4638
|
continue;
|
|
4493
4639
|
}
|
|
4494
|
-
const { data } = await readMarkdown(
|
|
4640
|
+
const { data } = await readMarkdown(path8.join(dir, file), MessageFrontmatterSchema);
|
|
4495
4641
|
found.push(MessageRefSchema.parse(data));
|
|
4496
4642
|
}
|
|
4497
4643
|
if (wanted.size === 0) {
|
|
@@ -4523,7 +4669,7 @@ ${charter.purpose}
|
|
|
4523
4669
|
if (!file.endsWith(".md")) {
|
|
4524
4670
|
continue;
|
|
4525
4671
|
}
|
|
4526
|
-
const doc = await readMarkdown(
|
|
4672
|
+
const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
|
|
4527
4673
|
if (doc.data.state !== "open") {
|
|
4528
4674
|
continue;
|
|
4529
4675
|
}
|
|
@@ -4550,7 +4696,7 @@ ${charter.purpose}
|
|
|
4550
4696
|
const all = [];
|
|
4551
4697
|
for (const threads of await this.threadScopes()) {
|
|
4552
4698
|
for (const file of await listFiles(threads)) {
|
|
4553
|
-
const doc = await readMarkdown(
|
|
4699
|
+
const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
|
|
4554
4700
|
const messages = await listFiles(this.paths.threadMessages(threads, doc.data.id));
|
|
4555
4701
|
const last = messages.at(-1);
|
|
4556
4702
|
all.push({
|
|
@@ -5145,7 +5291,7 @@ ${task.body}`;
|
|
|
5145
5291
|
}
|
|
5146
5292
|
}
|
|
5147
5293
|
for (const file of await listFiles(this.paths.projectKnowledge(project))) {
|
|
5148
|
-
const content = await readFile4(
|
|
5294
|
+
const content = await readFile4(path8.join(this.paths.projectKnowledge(project), file), "utf8");
|
|
5149
5295
|
if (content.toLowerCase().includes(needle)) {
|
|
5150
5296
|
hits.push({
|
|
5151
5297
|
kind: "knowledge",
|
|
@@ -5166,7 +5312,7 @@ ${task.body}`;
|
|
|
5166
5312
|
shared.push({
|
|
5167
5313
|
kind: "knowledge",
|
|
5168
5314
|
ref: `society/knowledge/${file}`,
|
|
5169
|
-
file:
|
|
5315
|
+
file: path8.join(this.paths.societyKnowledge(), file)
|
|
5170
5316
|
});
|
|
5171
5317
|
}
|
|
5172
5318
|
for (const skill of await this.listSocietySkills()) {
|
|
@@ -5179,7 +5325,7 @@ ${task.body}`;
|
|
|
5179
5325
|
shared.push({
|
|
5180
5326
|
kind: "memory",
|
|
5181
5327
|
ref: `memory/${file}`,
|
|
5182
|
-
file:
|
|
5328
|
+
file: path8.join(this.paths.agentMemory(actor.name), file)
|
|
5183
5329
|
});
|
|
5184
5330
|
}
|
|
5185
5331
|
for (const entry of shared) {
|
|
@@ -5268,8 +5414,8 @@ ${current.body}`);
|
|
|
5268
5414
|
removedAt: this.now().toISOString()
|
|
5269
5415
|
});
|
|
5270
5416
|
const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
|
|
5271
|
-
await
|
|
5272
|
-
await rename3(file,
|
|
5417
|
+
await mkdir5(aside, { recursive: true });
|
|
5418
|
+
await rename3(file, path8.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
|
|
5273
5419
|
await this.events.append("knowledge.removed", actor.name, {
|
|
5274
5420
|
topic: args.topic,
|
|
5275
5421
|
project: args.project
|
|
@@ -5316,8 +5462,8 @@ ${current.body}`);
|
|
|
5316
5462
|
removedAt: this.now().toISOString()
|
|
5317
5463
|
});
|
|
5318
5464
|
const aside = this.paths.removedSkills();
|
|
5319
|
-
await
|
|
5320
|
-
await rename3(this.paths.societySkill(args.name),
|
|
5465
|
+
await mkdir5(aside, { recursive: true });
|
|
5466
|
+
await rename3(this.paths.societySkill(args.name), path8.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
|
|
5321
5467
|
await this.events.append("skill.removed", actor.name, {
|
|
5322
5468
|
name: args.name,
|
|
5323
5469
|
reason: args.reason
|
|
@@ -5394,7 +5540,7 @@ ${current.body}`);
|
|
|
5394
5540
|
type,
|
|
5395
5541
|
size: bytes.byteLength
|
|
5396
5542
|
});
|
|
5397
|
-
const name =
|
|
5543
|
+
const name = path8.basename(args.path).replaceAll(/[[\]\\\r\n]/g, "");
|
|
5398
5544
|
return {
|
|
5399
5545
|
id,
|
|
5400
5546
|
type,
|
|
@@ -5420,7 +5566,7 @@ ${current.body}`);
|
|
|
5420
5566
|
async listCrons() {
|
|
5421
5567
|
const crons = [];
|
|
5422
5568
|
for (const file of await listFiles(this.paths.crons())) {
|
|
5423
|
-
crons.push(await this.readCronFile(
|
|
5569
|
+
crons.push(await this.readCronFile(path8.join(this.paths.crons(), file)));
|
|
5424
5570
|
}
|
|
5425
5571
|
return crons;
|
|
5426
5572
|
}
|
|
@@ -5767,7 +5913,7 @@ ${current.body}`);
|
|
|
5767
5913
|
async openThreadIds(dir, keep) {
|
|
5768
5914
|
const ids = /* @__PURE__ */ new Set();
|
|
5769
5915
|
for (const file of await listFiles(dir)) {
|
|
5770
|
-
const doc = await readMarkdown(
|
|
5916
|
+
const doc = await readMarkdown(path8.join(dir, file), ThreadFrontmatterSchema);
|
|
5771
5917
|
if (doc.data.state === "open" && keep({ ...doc.data, body: doc.body })) {
|
|
5772
5918
|
ids.add(doc.data.id);
|
|
5773
5919
|
}
|
|
@@ -5775,12 +5921,12 @@ ${current.body}`);
|
|
|
5775
5921
|
return ids;
|
|
5776
5922
|
}
|
|
5777
5923
|
/**
|
|
5778
|
-
* Refuses to archive a project while a turn runs in it, in a citizen's conversation of
|
|
5779
|
-
* channels in the society scope, or in
|
|
5924
|
+
* Refuses to archive or move a project while a turn runs in it, in a citizen's conversation of
|
|
5925
|
+
* its channels in the society scope, or in one of its open threads.
|
|
5780
5926
|
*/
|
|
5781
|
-
async assertProjectQuiet(actor, slug) {
|
|
5927
|
+
async assertProjectQuiet(actor, slug, what = `project ${slug} cannot be archived`) {
|
|
5782
5928
|
const threads = await this.openThreadIds(this.paths.threads(slug), () => true);
|
|
5783
|
-
this.assertNoTurnCut(actor,
|
|
5929
|
+
this.assertNoTurnCut(actor, what, (turn) => turn.scope === slug || turn.place === slug || turn.thread !== void 0 && threads.has(turn.thread));
|
|
5784
5930
|
}
|
|
5785
5931
|
async closeThread(actor, input) {
|
|
5786
5932
|
const args = VerbInputs.close_thread.parse(input);
|
|
@@ -6747,7 +6893,7 @@ ${rationale}`}`);
|
|
|
6747
6893
|
return part === void 0 ? this.paths.agentProject(agent, scope) : this.paths.agentConversation(agent, scope, part);
|
|
6748
6894
|
}
|
|
6749
6895
|
async readSessions(agent, project, part) {
|
|
6750
|
-
const file =
|
|
6896
|
+
const file = path8.join(this.sessionDir(agent, project, part), "sessions.json");
|
|
6751
6897
|
return await exists(file) ? readJson(file, SessionsFileSchema) : {};
|
|
6752
6898
|
}
|
|
6753
6899
|
/**
|
|
@@ -6758,7 +6904,7 @@ ${rationale}`}`);
|
|
|
6758
6904
|
await this.mutex.run(async () => {
|
|
6759
6905
|
const dir = this.sessionDir(agent, project, part);
|
|
6760
6906
|
await ensureDir(dir);
|
|
6761
|
-
const file =
|
|
6907
|
+
const file = path8.join(dir, "sessions.json");
|
|
6762
6908
|
const current = await exists(file) ? await readJson(file, SessionsFileSchema) : {};
|
|
6763
6909
|
const kept = current.runner === void 0 || current.runner === runner ? current : {};
|
|
6764
6910
|
await writeJson(file, { ...kept, [cli]: sessionId, runner });
|
|
@@ -6770,7 +6916,7 @@ ${rationale}`}`);
|
|
|
6770
6916
|
*/
|
|
6771
6917
|
async dropSession(agent, project, cli, sessionId, part) {
|
|
6772
6918
|
return this.mutex.run(async () => {
|
|
6773
|
-
const file =
|
|
6919
|
+
const file = path8.join(this.sessionDir(agent, project, part), "sessions.json");
|
|
6774
6920
|
if (!await exists(file)) {
|
|
6775
6921
|
return false;
|
|
6776
6922
|
}
|
|
@@ -6785,7 +6931,7 @@ ${rationale}`}`);
|
|
|
6785
6931
|
}
|
|
6786
6932
|
/** The last turn of one conversation: the scope's home, a channel's, a project's, or a thread's. */
|
|
6787
6933
|
async readLastTurn(agent, project, part) {
|
|
6788
|
-
const file =
|
|
6934
|
+
const file = path8.join(this.sessionDir(agent, project, part), "last-turn.json");
|
|
6789
6935
|
return await exists(file) ? readJson(file, TurnRecordSchema) : null;
|
|
6790
6936
|
}
|
|
6791
6937
|
/** The latest turn a citizen took in a scope, in any of its conversations there. */
|
|
@@ -6793,9 +6939,9 @@ ${rationale}`}`);
|
|
|
6793
6939
|
let latest = await this.readLastTurn(agent, project);
|
|
6794
6940
|
const place = this.paths.agentProject(agent, project);
|
|
6795
6941
|
const parts = [
|
|
6796
|
-
...await listDirs(
|
|
6797
|
-
...(await listDirs(
|
|
6798
|
-
...(await listDirs(
|
|
6942
|
+
...await listDirs(path8.join(place, "threads")),
|
|
6943
|
+
...(await listDirs(path8.join(place, "channels"))).map((name) => `#${name}`),
|
|
6944
|
+
...(await listDirs(path8.join(place, "places"))).map((name) => `~${name}`)
|
|
6799
6945
|
];
|
|
6800
6946
|
for (const part of parts) {
|
|
6801
6947
|
const turn = await this.readLastTurn(agent, project, part);
|
|
@@ -6861,6 +7007,7 @@ ${rationale}`}`);
|
|
|
6861
7007
|
exitReason: parsed.exitReason,
|
|
6862
7008
|
costUsd: parsed.costUsd,
|
|
6863
7009
|
...parsed.usage === null ? {} : { usage: parsed.usage },
|
|
7010
|
+
...parsed.contextTokens === void 0 ? {} : { contextTokens: parsed.contextTokens },
|
|
6864
7011
|
toolCalls: parsed.toolCalls,
|
|
6865
7012
|
model: parsed.model,
|
|
6866
7013
|
summary: parsed.status?.summary ?? null,
|
|
@@ -7061,11 +7208,11 @@ ${rationale}`}`);
|
|
|
7061
7208
|
}
|
|
7062
7209
|
/** Small named state files under data/state, for cursors the scheduler must keep across restarts. */
|
|
7063
7210
|
async readState(name, schema, fallback) {
|
|
7064
|
-
const file =
|
|
7211
|
+
const file = path8.join(this.paths.state(), `${name}.json`);
|
|
7065
7212
|
return await exists(file) ? readJson(file, schema) : fallback;
|
|
7066
7213
|
}
|
|
7067
7214
|
async writeState(name, value) {
|
|
7068
|
-
await this.mutex.run(() => writeJson(
|
|
7215
|
+
await this.mutex.run(() => writeJson(path8.join(this.paths.state(), `${name}.json`), value));
|
|
7069
7216
|
}
|
|
7070
7217
|
/** The project's dashboard, empty at revision 0 when it has none. */
|
|
7071
7218
|
async readDashboard(slug) {
|
|
@@ -7096,7 +7243,7 @@ ${rationale}`}`);
|
|
|
7096
7243
|
async listRunners() {
|
|
7097
7244
|
const runners2 = [];
|
|
7098
7245
|
for (const file of await listFiles(this.paths.runners())) {
|
|
7099
|
-
runners2.push((await readMarkdown(
|
|
7246
|
+
runners2.push((await readMarkdown(path8.join(this.paths.runners(), file), RunnerSchema)).data);
|
|
7100
7247
|
}
|
|
7101
7248
|
return runners2;
|
|
7102
7249
|
}
|
|
@@ -7148,7 +7295,7 @@ ${rationale}`}`);
|
|
|
7148
7295
|
return this.runnerTokens.get(hashToken(token)) ?? null;
|
|
7149
7296
|
}
|
|
7150
7297
|
runnerTokensFile() {
|
|
7151
|
-
return
|
|
7298
|
+
return path8.join(this.paths.state(), "runners.json");
|
|
7152
7299
|
}
|
|
7153
7300
|
async readRunnerTokens() {
|
|
7154
7301
|
const file = this.runnerTokensFile();
|
|
@@ -7162,7 +7309,8 @@ ${rationale}`}`);
|
|
|
7162
7309
|
}
|
|
7163
7310
|
/**
|
|
7164
7311
|
* Places a project on the runner that will hold its repository, unless it already lives on one,
|
|
7165
|
-
* and returns where it lives. Placement is once: every later turn of the project goes there
|
|
7312
|
+
* and returns where it lives. Placement is once: every later turn of the project goes there,
|
|
7313
|
+
* until the user moves the project (`recordProjectMove`).
|
|
7166
7314
|
*/
|
|
7167
7315
|
async placeProject(slug, runner) {
|
|
7168
7316
|
return this.mutex.run(async () => {
|
|
@@ -7204,12 +7352,12 @@ ${rationale}`}`);
|
|
|
7204
7352
|
}
|
|
7205
7353
|
const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
7206
7354
|
if (entry.isDirectory()) {
|
|
7207
|
-
await walk2(
|
|
7355
|
+
await walk2(path8.join(dir, entry.name), relative);
|
|
7208
7356
|
continue;
|
|
7209
7357
|
}
|
|
7210
7358
|
const file = entry.isFile() ? conflictCopyOf(relative) : null;
|
|
7211
7359
|
if (file !== null) {
|
|
7212
|
-
const since = await this.homes.changedAt(home, relative) ?? (await stat3(
|
|
7360
|
+
const since = await this.homes.changedAt(home, relative) ?? (await stat3(path8.join(home, relative)).catch(() => null))?.mtime.toISOString();
|
|
7213
7361
|
if (since !== void 0) {
|
|
7214
7362
|
found.push({ path: relative, file, since });
|
|
7215
7363
|
}
|
|
@@ -7283,6 +7431,74 @@ ${rationale}`}`);
|
|
|
7283
7431
|
return next;
|
|
7284
7432
|
});
|
|
7285
7433
|
}
|
|
7434
|
+
/**
|
|
7435
|
+
* The board's half of moving a project to another runner, which only the user does, checked
|
|
7436
|
+
* before anything travels and while the hub holds the project's turns: the project is active and
|
|
7437
|
+
* lives on a runner other than `to`, which the board knows; no task there is landing, since its
|
|
7438
|
+
* landing runs on the old runner; and no turn runs in the project. Tasks in play move with their
|
|
7439
|
+
* branches, so the answer lists them for the capabilities the new runner must offer.
|
|
7440
|
+
*/
|
|
7441
|
+
async prepareProjectMove(actor, slug, to) {
|
|
7442
|
+
this.assertUser(actor, "only the user may move a project to another runner");
|
|
7443
|
+
const runner = NameSchema.parse(to);
|
|
7444
|
+
return this.mutex.run(async () => {
|
|
7445
|
+
const project = await this.readActiveProject(slug);
|
|
7446
|
+
const from = project.runner;
|
|
7447
|
+
if (from === void 0) {
|
|
7448
|
+
throw new BoardError("INVALID_STATE", `project ${slug} lives on no runner yet; its first turn places it`);
|
|
7449
|
+
}
|
|
7450
|
+
if (from === runner) {
|
|
7451
|
+
throw new BoardError("INVALID_STATE", `project ${slug} already lives on ${runner}`);
|
|
7452
|
+
}
|
|
7453
|
+
await this.readRunner(runner);
|
|
7454
|
+
const inPlay2 = (await this.listTasks(slug)).filter((task) => task.status === "open" || task.status === "claimed");
|
|
7455
|
+
const landing = inPlay2.filter((task) => task.completing);
|
|
7456
|
+
if (landing.length > 0) {
|
|
7457
|
+
throw new BoardError("INVALID_STATE", `project ${slug} cannot be moved while ${landing.length} task(s) land on ${from} (${landing.map((task) => task.id).join(", ")}); move it once they have landed`);
|
|
7458
|
+
}
|
|
7459
|
+
await this.assertProjectQuiet(actor, slug, `project ${slug} cannot be moved`);
|
|
7460
|
+
return { project: { ...project, runner: from }, inPlay: inPlay2 };
|
|
7461
|
+
});
|
|
7462
|
+
}
|
|
7463
|
+
/**
|
|
7464
|
+
* Records that a project moved, once its new runner holds every branch its old one had, or with
|
|
7465
|
+
* nothing carried when the user moved it while the old runner was away: every later turn of
|
|
7466
|
+
* the project goes to `to`. Refused when the project was archived or moved meanwhile.
|
|
7467
|
+
*/
|
|
7468
|
+
async recordProjectMove(actor, move) {
|
|
7469
|
+
this.assertUser(actor, "only the user may move a project to another runner");
|
|
7470
|
+
return this.mutex.run(async () => {
|
|
7471
|
+
const current = await this.readActiveProject(move.slug);
|
|
7472
|
+
if (current.runner !== move.from) {
|
|
7473
|
+
throw new BoardError("INVALID_STATE", `project ${move.slug} no longer lives on ${move.from}, so this move no longer applies`);
|
|
7474
|
+
}
|
|
7475
|
+
await this.readRunner(move.to);
|
|
7476
|
+
const project = await this.updateProject(move.slug, (each) => ({ ...each, runner: move.to }));
|
|
7477
|
+
await this.events.append("project.moved", actor.name, {
|
|
7478
|
+
slug: move.slug,
|
|
7479
|
+
from: move.from,
|
|
7480
|
+
to: move.to,
|
|
7481
|
+
carried: move.carried
|
|
7482
|
+
});
|
|
7483
|
+
return project;
|
|
7484
|
+
});
|
|
7485
|
+
}
|
|
7486
|
+
/**
|
|
7487
|
+
* Makes the repository a move carries a project through, for the server's git route to serve,
|
|
7488
|
+
* and answers the move's id, which names it; `dropMoveRepo` removes it when the move ends.
|
|
7489
|
+
*/
|
|
7490
|
+
async openMoveRepo() {
|
|
7491
|
+
const id = this.newId();
|
|
7492
|
+
await this.moves.open(this.paths.move(id));
|
|
7493
|
+
return id;
|
|
7494
|
+
}
|
|
7495
|
+
async dropMoveRepo(id) {
|
|
7496
|
+
await this.moves.drop(this.paths.move(UlidSchema.parse(id)));
|
|
7497
|
+
}
|
|
7498
|
+
/** Where the moves' repositories live, which the server's git route serves from. */
|
|
7499
|
+
get moveRepos() {
|
|
7500
|
+
return this.paths.moves();
|
|
7501
|
+
}
|
|
7286
7502
|
/** The projection a runner mirrors for its agents to read, by relative path, with hashes. */
|
|
7287
7503
|
async boardManifest() {
|
|
7288
7504
|
return this.files.manifest(this.paths.board, () => true);
|
|
@@ -7293,7 +7509,7 @@ ${rationale}`}`);
|
|
|
7293
7509
|
async writeTurnRecord(record) {
|
|
7294
7510
|
const dir = this.sessionDir(record.agent, record.project, conversationPart(record));
|
|
7295
7511
|
await ensureDir(dir);
|
|
7296
|
-
await writeJson(
|
|
7512
|
+
await writeJson(path8.join(dir, "last-turn.json"), record);
|
|
7297
7513
|
}
|
|
7298
7514
|
// ---------------------------------------------------------------------------------------------
|
|
7299
7515
|
// Internals
|
|
@@ -7412,8 +7628,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7412
7628
|
async ensureAgentProject(name, slug) {
|
|
7413
7629
|
const dir = this.paths.agentProject(name, slug);
|
|
7414
7630
|
await ensureDir(dir);
|
|
7415
|
-
if (!await exists(
|
|
7416
|
-
await writeJson(
|
|
7631
|
+
if (!await exists(path8.join(dir, "sessions.json"))) {
|
|
7632
|
+
await writeJson(path8.join(dir, "sessions.json"), {});
|
|
7417
7633
|
}
|
|
7418
7634
|
}
|
|
7419
7635
|
/**
|
|
@@ -7655,11 +7871,11 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7655
7871
|
const file = this.paths.societySkillFile(skill.name);
|
|
7656
7872
|
const replaced = await exists(file);
|
|
7657
7873
|
const taken = proposal.skillFiles;
|
|
7658
|
-
const staged =
|
|
7874
|
+
const staged = path8.join(this.paths.proposalFiles(proposal.id), "SKILL.md");
|
|
7659
7875
|
if (taken === void 0 || !await exists(staged)) {
|
|
7660
7876
|
throw new BoardError("INVALID_STATE", `proposal ${proposal.id} holds no skill files`);
|
|
7661
7877
|
}
|
|
7662
|
-
await
|
|
7878
|
+
await rm3(dir, { recursive: true, force: true });
|
|
7663
7879
|
await cp(this.paths.proposalFiles(proposal.id), dir, { recursive: true });
|
|
7664
7880
|
const { body } = await readLooseMarkdown(file);
|
|
7665
7881
|
await writeMarkdown(file, {
|
|
@@ -7712,10 +7928,10 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7712
7928
|
return `the frontmatter of ${folder}/SKILL.md in ${proposal.proposedBy}'s home does not parse (${manual.error})`;
|
|
7713
7929
|
}
|
|
7714
7930
|
const dir = this.paths.proposalFiles(proposal.id);
|
|
7715
|
-
await
|
|
7931
|
+
await rm3(dir, { recursive: true, force: true });
|
|
7716
7932
|
for (const entry of taken.files) {
|
|
7717
|
-
const target =
|
|
7718
|
-
await ensureDir(
|
|
7933
|
+
const target = path8.join(dir, ...entry.path.split("/"));
|
|
7934
|
+
await ensureDir(path8.dirname(target));
|
|
7719
7935
|
await writeFile4(target, contents.get(entry.path) ?? Buffer.alloc(0));
|
|
7720
7936
|
await chmod2(target, entry.executable ? 493 : 420);
|
|
7721
7937
|
}
|
|
@@ -7806,7 +8022,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7806
8022
|
if (!isSafeRelativePath(entry.path)) {
|
|
7807
8023
|
continue;
|
|
7808
8024
|
}
|
|
7809
|
-
const bytes = await readFile4(
|
|
8025
|
+
const bytes = await readFile4(path8.join(dir, ...entry.path.split("/"))).catch(() => null);
|
|
7810
8026
|
files.push({
|
|
7811
8027
|
...entry,
|
|
7812
8028
|
...bytes === null ? { text: null, truncated: false } : skillFileText(bytes)
|
|
@@ -7955,7 +8171,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7955
8171
|
this.assertNoTurnCut(actor, `${ref} cannot be archived`, (turn) => turn.channel === channel && turn.scope === scope || turn.thread !== void 0 && closing.has(turn.thread));
|
|
7956
8172
|
const threadsClosed = [];
|
|
7957
8173
|
for (const file of await listFiles(threads)) {
|
|
7958
|
-
const doc = await readMarkdown(
|
|
8174
|
+
const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
|
|
7959
8175
|
if (doc.data.channel !== ref || doc.data.state !== "open") {
|
|
7960
8176
|
continue;
|
|
7961
8177
|
}
|
|
@@ -8087,7 +8303,7 @@ ${charter.purpose}
|
|
|
8087
8303
|
const threads = this.paths.threads(slug);
|
|
8088
8304
|
const threadsClosed = [];
|
|
8089
8305
|
for (const file of await listFiles(threads)) {
|
|
8090
|
-
const doc = await readMarkdown(
|
|
8306
|
+
const doc = await readMarkdown(path8.join(threads, file), ThreadFrontmatterSchema);
|
|
8091
8307
|
if (doc.data.state !== "open") {
|
|
8092
8308
|
continue;
|
|
8093
8309
|
}
|
|
@@ -8162,10 +8378,10 @@ ${charter.purpose}
|
|
|
8162
8378
|
...project,
|
|
8163
8379
|
members: project.members.filter((member) => member !== name)
|
|
8164
8380
|
}));
|
|
8165
|
-
const sessions =
|
|
8381
|
+
const sessions = path8.join(this.paths.agentProject(name, slug), "sessions.json");
|
|
8166
8382
|
if (await exists(sessions)) {
|
|
8167
8383
|
const archived = `sessions.archived.${ts.replace(/[:.]/g, "-")}.json`;
|
|
8168
|
-
await rename3(sessions,
|
|
8384
|
+
await rename3(sessions, path8.join(this.paths.agentProject(name, slug), archived));
|
|
8169
8385
|
}
|
|
8170
8386
|
}
|
|
8171
8387
|
this.tokenIndex.delete(current.tokenHash);
|
|
@@ -8506,7 +8722,7 @@ ${charter.purpose}
|
|
|
8506
8722
|
if (since !== null && id <= since || upto !== void 0 && id > upto) {
|
|
8507
8723
|
continue;
|
|
8508
8724
|
}
|
|
8509
|
-
const doc = await readMarkdown(
|
|
8725
|
+
const doc = await readMarkdown(path8.join(dir, file), MessageFrontmatterSchema);
|
|
8510
8726
|
messages.push({ ...doc.data, body: doc.body });
|
|
8511
8727
|
}
|
|
8512
8728
|
return messages;
|
|
@@ -8634,6 +8850,7 @@ var DEFAULT_TIMINGS = Object.freeze({
|
|
|
8634
8850
|
homeConflictMs: 24 * 36e5,
|
|
8635
8851
|
scaleCooldownMs: 36e5,
|
|
8636
8852
|
reflectionMs: 24 * 36e5,
|
|
8853
|
+
sessionCapTokens: 3e5,
|
|
8637
8854
|
transientRetryMs: [6e4, 3 * 6e4, 10 * 6e4]
|
|
8638
8855
|
});
|
|
8639
8856
|
var SchedulerTimingsSchema = z17.object({
|
|
@@ -8652,6 +8869,7 @@ var SchedulerTimingsSchema = z17.object({
|
|
|
8652
8869
|
homeConflictMs: z17.number().positive(),
|
|
8653
8870
|
scaleCooldownMs: z17.number().positive(),
|
|
8654
8871
|
reflectionMs: z17.number().positive(),
|
|
8872
|
+
sessionCapTokens: z17.number().int().positive(),
|
|
8655
8873
|
transientRetryMs: z17.array(z17.number().positive())
|
|
8656
8874
|
}).partial();
|
|
8657
8875
|
function parseTimings(input) {
|
|
@@ -8694,7 +8912,12 @@ var StateSchema = z17.object({
|
|
|
8694
8912
|
costSinceReport: z17.number().nonnegative().default(0),
|
|
8695
8913
|
turnsSinceReport: z17.number().int().nonnegative().default(0),
|
|
8696
8914
|
/** When each member last reflected, or was first seen; the cadence counts from here. */
|
|
8697
|
-
lastReflection: z17.record(z17.string(), z17.string()).default({})
|
|
8915
|
+
lastReflection: z17.record(z17.string(), z17.string()).default({}),
|
|
8916
|
+
/**
|
|
8917
|
+
* The home session each society member last asked for an early reflection for, once per
|
|
8918
|
+
* session, and whether that reflection still waits to be queued.
|
|
8919
|
+
*/
|
|
8920
|
+
earlyReflection: z17.record(z17.string(), z17.object({ session: z17.string(), due: z17.boolean() })).default({})
|
|
8698
8921
|
});
|
|
8699
8922
|
function messageOf(trigger) {
|
|
8700
8923
|
return trigger.kind === "mention" || trigger.kind === "user_post" ? trigger.messageId : void 0;
|
|
@@ -9038,6 +9261,7 @@ var Scheduler = class {
|
|
|
9038
9261
|
const cost = payload["costUsd"];
|
|
9039
9262
|
this.state.costSinceReport += typeof cost === "number" ? cost : 0;
|
|
9040
9263
|
this.state.turnsSinceReport += 1;
|
|
9264
|
+
await this.checkSessionSize(event);
|
|
9041
9265
|
return;
|
|
9042
9266
|
}
|
|
9043
9267
|
case "wake.requested": {
|
|
@@ -9766,9 +9990,44 @@ var Scheduler = class {
|
|
|
9766
9990
|
item.dispatch = rest.length === 0 ? dispatch : { ...dispatch, crons: rest };
|
|
9767
9991
|
}
|
|
9768
9992
|
}
|
|
9993
|
+
/**
|
|
9994
|
+
* A society role's home session that a turn left at `sessionCapTokens` or more, by its latest
|
|
9995
|
+
* model call, asks for a reflection ahead of the cadence, which `checkReflections` queues once
|
|
9996
|
+
* none of the member's turns is queued or running; the reflection's end starts the home afresh
|
|
9997
|
+
* (`TurnHost`), so its history goes only once a reflection saved what to keep. A session asks
|
|
9998
|
+
* once, so a reflection that fails leaves the next to the cadence, and none asks while a
|
|
9999
|
+
* reflection is queued or running.
|
|
10000
|
+
*/
|
|
10001
|
+
async checkSessionSize(event) {
|
|
10002
|
+
const { payload } = event;
|
|
10003
|
+
const tokens = payload["contextTokens"];
|
|
10004
|
+
const session = stringOf(payload["session"]);
|
|
10005
|
+
if (typeof tokens !== "number" || tokens < this.timings.sessionCapTokens || session === null || payload["project"] !== SOCIETY_SCOPE || payload["thread"] !== void 0 || payload["channel"] !== void 0 || payload["place"] !== void 0 || payload["trigger"] === "reflection" || payload["sessionDropped"] === true) {
|
|
10006
|
+
return;
|
|
10007
|
+
}
|
|
10008
|
+
const agent = await this.tryReadAgent(event.actor);
|
|
10009
|
+
if (agent === null || agent.status !== "active") {
|
|
10010
|
+
return;
|
|
10011
|
+
}
|
|
10012
|
+
const charter = await this.board.readRole(agent.role).catch(() => null);
|
|
10013
|
+
if (charter === null || !charter.societyScope || !charter.reflects) {
|
|
10014
|
+
return;
|
|
10015
|
+
}
|
|
10016
|
+
if (this.state.earlyReflection[agent.name]?.session === session) {
|
|
10017
|
+
return;
|
|
10018
|
+
}
|
|
10019
|
+
const ofAgent = (key) => parseSessionKey(key)?.agent === agent.name;
|
|
10020
|
+
const reflecting = [...this.pending].some(([key, item]) => ofAgent(key) && item.dispatch.trigger.kind === "reflection") || [...this.exclusive].some(([key, kind]) => ofAgent(key) && kind === "reflection");
|
|
10021
|
+
if (reflecting) {
|
|
10022
|
+
return;
|
|
10023
|
+
}
|
|
10024
|
+
this.state.earlyReflection[agent.name] = { session, due: true };
|
|
10025
|
+
this.log.info({ agent: agent.name, contextTokens: tokens, cap: this.timings.sessionCapTokens }, "home session past the cap; a reflection is due");
|
|
10026
|
+
}
|
|
9769
10027
|
/**
|
|
9770
10028
|
* Every `reflectionMs`, a member whose charter reflects takes a reflection turn in the scope of
|
|
9771
|
-
* its latest working turn, unless it has not worked since its last reflection
|
|
10029
|
+
* its latest working turn, unless it has not worked since its last reflection; a society member
|
|
10030
|
+
* whose home session passed the cap takes one in its home ahead of the cadence, which restarts it.
|
|
9772
10031
|
*/
|
|
9773
10032
|
async checkReflections(now) {
|
|
9774
10033
|
for (const agent of await this.board.listAgents()) {
|
|
@@ -9779,6 +10038,15 @@ var Scheduler = class {
|
|
|
9779
10038
|
if (!charter.reflects) {
|
|
9780
10039
|
continue;
|
|
9781
10040
|
}
|
|
10041
|
+
const early = this.state.earlyReflection[agent.name];
|
|
10042
|
+
if (early?.due === true) {
|
|
10043
|
+
if (!this.busy(agent.name)) {
|
|
10044
|
+
this.state.lastReflection[agent.name] = iso(now);
|
|
10045
|
+
this.state.earlyReflection[agent.name] = { ...early, due: false };
|
|
10046
|
+
this.enqueue(agent.name, SOCIETY_SCOPE, { kind: "reflection", reason: "home session past the cap" }, now);
|
|
10047
|
+
}
|
|
10048
|
+
continue;
|
|
10049
|
+
}
|
|
9782
10050
|
const last = this.state.lastReflection[agent.name];
|
|
9783
10051
|
if (last === void 0) {
|
|
9784
10052
|
this.state.lastReflection[agent.name] = iso(now);
|
|
@@ -9792,8 +10060,7 @@ var Scheduler = class {
|
|
|
9792
10060
|
this.state.lastReflection[agent.name] = iso(now);
|
|
9793
10061
|
continue;
|
|
9794
10062
|
}
|
|
9795
|
-
|
|
9796
|
-
if (busy) {
|
|
10063
|
+
if (this.busy(agent.name)) {
|
|
9797
10064
|
continue;
|
|
9798
10065
|
}
|
|
9799
10066
|
this.state.lastReflection[agent.name] = iso(now);
|
|
@@ -9803,6 +10070,13 @@ var Scheduler = class {
|
|
|
9803
10070
|
this.enqueue(agent.name, latest.scope, { kind: "reflection", reason: "scheduled reflection" }, now);
|
|
9804
10071
|
}
|
|
9805
10072
|
}
|
|
10073
|
+
/**
|
|
10074
|
+
* Whether a member has a turn queued or running anywhere, so a reflection waits for the next tick
|
|
10075
|
+
* rather than merge into a working turn or run beside turns writing the memory it consolidates.
|
|
10076
|
+
*/
|
|
10077
|
+
busy(agent) {
|
|
10078
|
+
return [...this.pending.keys(), ...this.running.keys()].some((key) => parseSessionKey(key)?.agent === agent);
|
|
10079
|
+
}
|
|
9806
10080
|
/** The member's most recent turn that was not itself a reflection, with the scope it ran in. */
|
|
9807
10081
|
async latestWorkingTurn(agent) {
|
|
9808
10082
|
let latest = null;
|
|
@@ -10249,6 +10523,10 @@ var Scheduler = class {
|
|
|
10249
10523
|
this.exclusive.set(key, "onboarding");
|
|
10250
10524
|
} else if (dispatch.trigger.kind === "reflection") {
|
|
10251
10525
|
this.exclusive.set(key, "reflection");
|
|
10526
|
+
const early = this.state.earlyReflection[dispatch.agent];
|
|
10527
|
+
if (early?.due === true) {
|
|
10528
|
+
this.state.earlyReflection[dispatch.agent] = { ...early, due: false };
|
|
10529
|
+
}
|
|
10252
10530
|
}
|
|
10253
10531
|
}
|
|
10254
10532
|
}
|
|
@@ -10382,7 +10660,7 @@ var TURN_CONTRACT = [
|
|
|
10382
10660
|
"- Work only inside your worktree. In a task's conversation it is a worktree of its own on the task's branch `task/<id>`: commit your work there. The branch is checked out in one worktree at a time and kept for the turns of whoever holds the task's stage, so a turn of anyone else there, or one that starts while another turn has the branch, works detached at its tip, which Your workspace then says; its commits go on the branch by a fast-forward when a turn in the task ends with the branch free, and the task's thread is told of commits that no longer build on its tip. After each turn the runner commits what you left on the branch and lets it go for the next holder. Your home worktree is on your own branch, `agent/<name>`, for work tied to no task; nothing lands from it by itself. In a proposal's, a topic's, or a channel's conversation it is a worktree of its own, detached at the project's default branch, which goes when the thread closes or the channel is archived, so work that should last goes on a task's branch. Before each turn the runner fetches the project's remote and brings what holds no work of yours up to the default branch: your own branch when it has no commits of its own, a task's branch nobody has worked on, and a detached worktree with nothing uncommitted; what it leaves where it is, and why, is under Your workspace in the prompt. It never merges or commits for you.",
|
|
10383
10661
|
"- Posts that would wake this conversation may arrive while you work, delivered into the turn under a heading that says so and formatted like the digest. They are part of this turn: act on them now, after the step in hand, or not at all, as with the digest. The user may also stop a turn; the next turn is told.",
|
|
10384
10662
|
"- Silence is allowed. If the digest needs no reply, post nothing.",
|
|
10385
|
-
"- To come back to something later, set a cron with create_cron: a note on what to do then, and a five-field cron expression or one time with at. It wakes you in this conversation unless you name another, at most every fifteen minutes, and every fire is a paid turn, so end a cron with remove_cron once it has done its job. Never wait by
|
|
10663
|
+
"- To come back to something later, set a cron with create_cron: a note on what to do then, and a five-field cron expression or one time with at. It wakes you in this conversation unless you name another, at most every fifteen minutes, and every fire is a paid turn, so end a cron with remove_cron once it has done its job. Never wait by sleeping or leaving a process running: a turn's board token ends with the turn, so nothing it leaves behind can act on the board. While you check your own stage's work on a schedule, such as a running job, CI, or a deploy, keep holding the stage: the cron sets when you check, and your turns in the task's thread renew its lease. Release a stage only to hand it on, or once its task's blocked_by names the tasks it waits on: the stage then wakes nobody until the last of them ends. Any other wait on someone else, such as the user or an outside party, is a stage of its own, not a held stage. The society's crons are under society/crons/ in the board projection.",
|
|
10386
10664
|
"- An @mention wakes the citizen named, and every wake costs a turn. Address someone with @ only when you need them to act; when you merely refer to citizens, write their names plainly. A name in a code span or a code block is no mention, so code keeps its @ as written. @user wakes nobody, since the user takes no turns; it is what puts a question before the user, so use it only when you need the user to answer or decide.",
|
|
10387
10665
|
"- A thread is a conversation off a channel that reaches only its participants and whoever is mentioned in it. Every task has one, opened with it under the task's id on the channel of its project it was filed in, general unless it was filed in another: talk about a task there, with post_message and its thread_id, and the notes you give advance_task and update_task are posted there as you. Its participants are the task's creator, its stage holders and assignees, whoever may take the stage that waits, and anyone who posted; it closes when the task ends. Every proposal has one too, in governance, opened with its pitch and closed by its decision. Open other threads with open_thread on a channel with a title, for any other topic; close_thread posts your summary to its channel.",
|
|
10388
10666
|
"- To show an image in a post, such as a screenshot or a chart, save it anywhere you can read it, such as a folder from `mktemp -d`, call upload_image with its path, and put the markdown it returns in the post, ``, with what the image shows as its alt text: the board keeps the image, so it shows in any conversation after the file is gone. It takes a PNG, JPEG, GIF, or WebP of at most 8 MB from your worktree, your home, or the system temp folder. An image committed on a task's branch, linked by its path in the task's worktree, still shows in place, and so does one at an https address. Never put an image or any other file into a message as base64 or a data: URL: the board does not show it, and every reader of the conversation gets it in full in their prompt.",
|
|
@@ -10399,7 +10677,7 @@ var PLANNING = [
|
|
|
10399
10677
|
"- Name stages by what gets done. Plan the next few steps rather than everything, and reshape with plan_task as the work teaches you.",
|
|
10400
10678
|
"- Assign a stage to a role when anyone in it could do it, to a citizen only when it must be them, and to nobody when anyone in the project could. A citizen named on a stage must be a member of the task's project; add them first. A stage wakes everyone it is open to when it comes up, so the narrower the assignee, the fewer turns it costs.",
|
|
10401
10679
|
"- Reshape rather than force: another round is an inserted stage, and a wait on something outside the society is a stage named for what it waits on. When a check finds work unfinished, the holder of the current stage sends the task back to an earlier stage with update_task, and so may the user, the steward, and the concierge; anyone else asks the concierge in the task's thread.",
|
|
10402
|
-
"- A task that cannot start until other tasks finish names them with update_task blocked_by. While any of them is in play,
|
|
10680
|
+
"- A task that cannot start until other tasks finish names them with update_task blocked_by, and whoever holds its stage then releases it, since the heartbeat still wakes a holder. While any of them is in play, a stage nobody holds wakes nobody, neither when it comes up nor on a heartbeat; once the last has ended, done or abandoned, the stage wakes as if it had just come up, or wakes its holder. Nobody needs to clear the block or mention anyone when they end; change blocked_by only when the plan changes.",
|
|
10403
10681
|
"- A gated stage is an independent check: nobody who held an earlier stage of the task may hold it, and only the user, the steward, and the concierge may add, remove, move, reassign, or ungate it. Ask them in the task's thread when a gate should change.",
|
|
10404
10682
|
"- Commit your work on the task's branch before you advance, and say what you did in the advance note; it is the handover the next holder reads in the task's thread."
|
|
10405
10683
|
].join("\n");
|
|
@@ -10467,9 +10745,10 @@ var REFLECTION_STEPS = [
|
|
|
10467
10745
|
"- Consolidate memory/core.md: keep it to lessons that still hold, about sixty lines at most, and move detail worth keeping to memory/<topic>.md in your home, which the board's search covers for you alone.",
|
|
10468
10746
|
'- Extract a skill: a procedure you have followed twice is a candidate, and one that makes a citizen better at a kind of work on any project goes to skills/<name>/SKILL.md, frontmatter with `name` and a one-line `description` and then the steps, with any script or reference it uses in the same folder. Propose it with kind "skill" when more than one citizen would use it and nothing in it is tied to one place; one only you use stays yours.',
|
|
10469
10747
|
"- Refresh profile.md: one paragraph on what you do well and what to send your way; the roster the front desk routes with is built from it.",
|
|
10470
|
-
"- Share: how a codebase behaves goes in its AGENTS.md or README through a task like any change to the code, so propose it in that task's thread or the project channel; a durable fact about this scope as a place (runners, access, flow, where results live) goes through write_knowledge; a norm the whole society should follow goes to the steward as a plain post in general."
|
|
10471
|
-
"- Report memoryUpdated: true in the status object when core.md or a skill changed."
|
|
10748
|
+
"- Share: how a codebase behaves goes in its AGENTS.md or README through a task like any change to the code, so propose it in that task's thread or the project channel; a durable fact about this scope as a place (runners, access, flow, where results live) goes through write_knowledge; a norm the whole society should follow goes to the steward as a plain post in general."
|
|
10472
10749
|
];
|
|
10750
|
+
var SAVE_WHAT_IS_OPEN = "- This conversation's session ends with this turn, and the next turn here starts a new one that knows only your home's files and the board: write anything still open into memory/core.md, or the archive it points to, before you end.";
|
|
10751
|
+
var REFLECTION_REPORT = "- Report memoryUpdated: true in the status object when core.md or a skill changed.";
|
|
10473
10752
|
var MAX_BODY_CHARS = 1500;
|
|
10474
10753
|
var MAX_PROFILE_CHARS = 200;
|
|
10475
10754
|
function clip(text) {
|
|
@@ -10654,7 +10933,7 @@ function buildTurnPrompt(input) {
|
|
|
10654
10933
|
lines.push("", WORKSPACE_TOKEN);
|
|
10655
10934
|
const conflicts = input.conflicts ?? [];
|
|
10656
10935
|
if (dispatch.trigger.kind === "reflection") {
|
|
10657
|
-
lines.push("", "## Reflection", "", REFLECTION_INTRO, ...conflicts.length > 0 ? [RECONCILE] : [], ...REFLECTION_STEPS);
|
|
10936
|
+
lines.push("", "## Reflection", "", REFLECTION_INTRO, ...conflicts.length > 0 ? [RECONCILE] : [], ...REFLECTION_STEPS, ...input.afreshAfterReflection === true ? [SAVE_WHAT_IS_OPEN] : [], REFLECTION_REPORT);
|
|
10658
10937
|
}
|
|
10659
10938
|
if (input.onboarding !== null) {
|
|
10660
10939
|
lines.push("", "## First turn", "", renderOnboardingPreamble(input.onboarding));
|
|
@@ -10668,6 +10947,9 @@ function buildTurnPrompt(input) {
|
|
|
10668
10947
|
"Its CLI could not resume this conversation's session, so the session was dropped and this turn starts a new one: nothing of your earlier turns here carries over but what is on the board and in your home."
|
|
10669
10948
|
] : [], "The worktree may hold uncommitted changes. Run git status before doing anything else.");
|
|
10670
10949
|
}
|
|
10950
|
+
if (last?.trigger.kind === "reflection" && last.exitReason === "completed" && last.sessionDropped === true) {
|
|
10951
|
+
lines.push("", "## Your session started afresh", "", "Your last turn here was a reflection, and this conversation's session ended with it, so this turn starts a new one: what you kept is in memory/core.md and the archive it points to, and what your earlier turns did is on the board, in your posts and the threads and tasks you worked in, which search and the board's files reach.");
|
|
10952
|
+
}
|
|
10671
10953
|
const society = input.societyView;
|
|
10672
10954
|
if (society !== void 0 && society !== null) {
|
|
10673
10955
|
lines.push("", "## The society", "", `Each ask is a conversation of its own, and others may be in a turn at the same time. Before proposing a citizen, a role, or a project, look at the proposals still waiting under ${PATH_TOKENS.board}/society/proposals/: another ask may have proposed it already.`, "", "### Projects", "");
|
|
@@ -10684,7 +10966,7 @@ function buildTurnPrompt(input) {
|
|
|
10684
10966
|
}
|
|
10685
10967
|
const machines = input.runners;
|
|
10686
10968
|
if (machines !== void 0 && machines !== null) {
|
|
10687
|
-
lines.push("", "## Runners", "", "The machines turns run on. A project lives on the runner that takes its first turn, one that offers every capability the project and that turn's task require, and
|
|
10969
|
+
lines.push("", "## Runners", "", "The machines turns run on. A project lives on the runner that takes its first turn, one that offers every capability the project and that turn's task require, and moves only when the user moves it. A task's required_capabilities must name capabilities exactly as a runner below offers them; a task needing one its project's runner lacks waits. A runner's quota is what each CLI's login there last reported of its plan, in percent of each window; a CLI limited until a time fails its turns there until then.", "");
|
|
10688
10970
|
if (machines.runners.length === 0) {
|
|
10689
10971
|
lines.push("None registered yet.");
|
|
10690
10972
|
}
|
|
@@ -10907,7 +11189,10 @@ var TurnHost = class {
|
|
|
10907
11189
|
const newSession = recorded === void 0;
|
|
10908
11190
|
const lastTurn = await this.board.readLastTurn(agent.name, dispatch.project, part);
|
|
10909
11191
|
const costSoFarUsd = !newSession && lastTurn?.session === recorded ? lastTurn.sessionCostUsd ?? lastTurn.costUsd : 0;
|
|
11192
|
+
const usageSoFar = !newSession && lastTurn?.session === recorded ? lastTurn.sessionUsage : void 0;
|
|
10910
11193
|
const resumeRetries = !newSession && lastTurn?.session === recorded && lastTurn.resumeFailed === "retry" ? lastTurn.resumeRetries ?? 1 : 0;
|
|
11194
|
+
const afreshAfterReflection = charter.societyScope && agent.role !== USER_ROLE && societyScope && part === void 0;
|
|
11195
|
+
const afterReflection = lastTurn?.trigger.kind === "reflection" && lastTurn.sessionDropped === true;
|
|
10911
11196
|
const digest = await this.board.readDigest({
|
|
10912
11197
|
...actor,
|
|
10913
11198
|
scope: dispatch.project,
|
|
@@ -10919,7 +11204,7 @@ var TurnHost = class {
|
|
|
10919
11204
|
const messages = threadSoFar ?? digest.messages;
|
|
10920
11205
|
const cursorAfter = threadSoFar?.at(-1)?.id ?? digest.cursor;
|
|
10921
11206
|
const held = task === null ? [] : (await this.board.heldClaims(agent.name)).filter((each) => each.id === task.id);
|
|
10922
|
-
const onboarding = dispatch.onboarding || newSession && part === void 0 ? {
|
|
11207
|
+
const onboarding = dispatch.onboarding || newSession && part === void 0 && !afterReflection ? {
|
|
10923
11208
|
agentName: agent.name,
|
|
10924
11209
|
roleSummary: charter.purpose,
|
|
10925
11210
|
project: dispatch.project,
|
|
@@ -10998,6 +11283,7 @@ var TurnHost = class {
|
|
|
10998
11283
|
project,
|
|
10999
11284
|
lastTurn,
|
|
11000
11285
|
onboarding,
|
|
11286
|
+
afreshAfterReflection,
|
|
11001
11287
|
societyView,
|
|
11002
11288
|
runners: runners2,
|
|
11003
11289
|
knowledge,
|
|
@@ -11010,7 +11296,8 @@ var TurnHost = class {
|
|
|
11010
11296
|
const record = await this.board.beginTurn({
|
|
11011
11297
|
...base,
|
|
11012
11298
|
session: recorded ?? null,
|
|
11013
|
-
sessionCostUsd: costSoFarUsd
|
|
11299
|
+
sessionCostUsd: costSoFarUsd,
|
|
11300
|
+
...usageSoFar === void 0 ? {} : { sessionUsage: usageSoFar }
|
|
11014
11301
|
});
|
|
11015
11302
|
if (record.id === void 0) {
|
|
11016
11303
|
throw new Error("the board gave the turn no id");
|
|
@@ -11028,6 +11315,7 @@ var TurnHost = class {
|
|
|
11028
11315
|
...place === void 0 ? {} : { place },
|
|
11029
11316
|
session: recorded ?? null,
|
|
11030
11317
|
costSoFarUsd,
|
|
11318
|
+
...usageSoFar === void 0 ? {} : { usageSoFar },
|
|
11031
11319
|
instructions,
|
|
11032
11320
|
prompt,
|
|
11033
11321
|
mcp: { url: this.mcpUrl, token },
|
|
@@ -11057,6 +11345,7 @@ var TurnHost = class {
|
|
|
11057
11345
|
transcript: [],
|
|
11058
11346
|
coldToken: residentKey === null ? token : null,
|
|
11059
11347
|
residentKey: residentKey === null ? null : `${runner.name}|${residentKey}`,
|
|
11348
|
+
afreshAfterReflection,
|
|
11060
11349
|
keepLeases,
|
|
11061
11350
|
done,
|
|
11062
11351
|
fail
|
|
@@ -11220,8 +11509,10 @@ var TurnHost = class {
|
|
|
11220
11509
|
const part = conversationPart(job);
|
|
11221
11510
|
const resumeRetries = outcome.resumeFailed === "retry" ? turn.resumeRetries + 1 : void 0;
|
|
11222
11511
|
const dropSession = job.session !== null && (outcome.resumeFailed === "lost" || (resumeRetries ?? 0) >= RESUME_RETRIES);
|
|
11512
|
+
const reflected = turn.afreshAfterReflection && turn.record.trigger.kind === "reflection" && outcome.exitReason === "completed";
|
|
11513
|
+
const { sessionUsage: _started, ...begun } = turn.record;
|
|
11223
11514
|
const finished = {
|
|
11224
|
-
...
|
|
11515
|
+
...begun,
|
|
11225
11516
|
session: outcome.session,
|
|
11226
11517
|
endedAt: this.now().toISOString(),
|
|
11227
11518
|
exitReason: outcome.exitReason,
|
|
@@ -11230,6 +11521,8 @@ var TurnHost = class {
|
|
|
11230
11521
|
usage: outcome.usage,
|
|
11231
11522
|
costUsd: outcome.costUsd,
|
|
11232
11523
|
sessionCostUsd: outcome.sessionCostUsd ?? turn.costSoFarUsd,
|
|
11524
|
+
...outcome.sessionUsage === void 0 ? {} : { sessionUsage: outcome.sessionUsage },
|
|
11525
|
+
...outcome.contextTokens === void 0 ? {} : { contextTokens: outcome.contextTokens },
|
|
11233
11526
|
toolCalls: turn.transcript.filter((entry) => entry.event.type === "tool_call").length,
|
|
11234
11527
|
model: outcome.model ?? turn.configuredModel,
|
|
11235
11528
|
...outcome.work === null ? {} : { work: outcome.work },
|
|
@@ -11237,7 +11530,7 @@ var TurnHost = class {
|
|
|
11237
11530
|
...outcome.resumeFailed === void 0 ? {} : { resumeFailed: outcome.resumeFailed },
|
|
11238
11531
|
...outcome.transient === true ? { transient: true } : {},
|
|
11239
11532
|
...resumeRetries === void 0 ? {} : { resumeRetries },
|
|
11240
|
-
...dropSession ? { sessionDropped: true } : {}
|
|
11533
|
+
...dropSession || reflected ? { sessionDropped: true } : {}
|
|
11241
11534
|
};
|
|
11242
11535
|
const ended = await this.conversationEnded(job);
|
|
11243
11536
|
const closing = turn.record.trigger.kind === "closing";
|
|
@@ -11255,6 +11548,11 @@ var TurnHost = class {
|
|
|
11255
11548
|
resumeFailed: outcome.resumeFailed,
|
|
11256
11549
|
resumeRetries
|
|
11257
11550
|
}, "dropped a session its CLI could not resume");
|
|
11551
|
+
} else if (reflected) {
|
|
11552
|
+
if (job.session !== null) {
|
|
11553
|
+
await this.board.dropSession(job.agent, job.scope, job.cli, job.session, part);
|
|
11554
|
+
}
|
|
11555
|
+
this.log.info({ agent: job.agent, project: job.scope, session: outcome.session }, "dropped a home session after its reflection");
|
|
11258
11556
|
} else if (outcome.session !== null && outcome.session !== job.session) {
|
|
11259
11557
|
await this.board.writeSession(job.agent, job.scope, job.cli, outcome.session, turn.runner, part);
|
|
11260
11558
|
}
|
|
@@ -11550,6 +11848,8 @@ var RunnerProtocolError = class extends Error {
|
|
|
11550
11848
|
};
|
|
11551
11849
|
var RunnerAwayError = class extends Error {
|
|
11552
11850
|
};
|
|
11851
|
+
var MoveSourceAwayError = class extends Error {
|
|
11852
|
+
};
|
|
11553
11853
|
var SILENT2 = { info() {
|
|
11554
11854
|
}, warn() {
|
|
11555
11855
|
}, error() {
|
|
@@ -11582,6 +11882,12 @@ var RunnerHub = class {
|
|
|
11582
11882
|
/** Disconnects being recorded, which `close` waits for. */
|
|
11583
11883
|
detaching = /* @__PURE__ */ new Set();
|
|
11584
11884
|
runnerListeners = /* @__PURE__ */ new Set();
|
|
11885
|
+
/** The project being moved, by slug: none of its turns is placed and nothing lands there until it ends. */
|
|
11886
|
+
moving = /* @__PURE__ */ new Map();
|
|
11887
|
+
/** Turns of each project placed on a runner and not yet opened, which the board cannot see yet. */
|
|
11888
|
+
starting = /* @__PURE__ */ new Map();
|
|
11889
|
+
/** Moves started since the hub did, so a placement that a move overtook gives its turn back. */
|
|
11890
|
+
moveStarts = 0;
|
|
11585
11891
|
constructor(options) {
|
|
11586
11892
|
this.board = options.board;
|
|
11587
11893
|
this.host = options.host;
|
|
@@ -11853,6 +12159,7 @@ var RunnerHub = class {
|
|
|
11853
12159
|
return accepted ? "sent" : "refused";
|
|
11854
12160
|
}
|
|
11855
12161
|
async assign(dispatch) {
|
|
12162
|
+
const starts = this.moveStarts;
|
|
11856
12163
|
const agent = await this.board.readAgent(dispatch.agent).catch(() => null);
|
|
11857
12164
|
if (agent === null) {
|
|
11858
12165
|
return { refused: `no agent ${dispatch.agent}` };
|
|
@@ -11869,6 +12176,9 @@ var RunnerHub = class {
|
|
|
11869
12176
|
if (dispatch.project === SOCIETY_SCOPE) {
|
|
11870
12177
|
chosen = await this.pinnedSeat(agent.name, agent.homeRunner, cli, free);
|
|
11871
12178
|
} else {
|
|
12179
|
+
if (this.moving.has(dispatch.project)) {
|
|
12180
|
+
return null;
|
|
12181
|
+
}
|
|
11872
12182
|
const project = await this.board.readProject(dispatch.project);
|
|
11873
12183
|
let home = project.runner;
|
|
11874
12184
|
if (home === void 0) {
|
|
@@ -11895,7 +12205,13 @@ var RunnerHub = class {
|
|
|
11895
12205
|
if (chosen === void 0) {
|
|
11896
12206
|
return null;
|
|
11897
12207
|
}
|
|
12208
|
+
if (dispatch.project !== SOCIETY_SCOPE && (this.moving.has(dispatch.project) || this.moveStarts !== starts)) {
|
|
12209
|
+
return null;
|
|
12210
|
+
}
|
|
11898
12211
|
chosen.inUse += 1;
|
|
12212
|
+
if (dispatch.project !== SOCIETY_SCOPE) {
|
|
12213
|
+
this.starting.set(dispatch.project, (this.starting.get(dispatch.project) ?? 0) + 1);
|
|
12214
|
+
}
|
|
11899
12215
|
return { runner: chosen.name };
|
|
11900
12216
|
}
|
|
11901
12217
|
async runTurn(dispatch, assignment) {
|
|
@@ -11904,11 +12220,23 @@ var RunnerHub = class {
|
|
|
11904
12220
|
}
|
|
11905
12221
|
const seat = this.seats.get(assignment.runner);
|
|
11906
12222
|
let turnId = null;
|
|
12223
|
+
let placed = dispatch.project !== SOCIETY_SCOPE;
|
|
12224
|
+
const opening = () => {
|
|
12225
|
+
if (placed) {
|
|
12226
|
+
placed = false;
|
|
12227
|
+
const left = (this.starting.get(dispatch.project) ?? 1) - 1;
|
|
12228
|
+
if (left > 0) {
|
|
12229
|
+
this.starting.set(dispatch.project, left);
|
|
12230
|
+
} else {
|
|
12231
|
+
this.starting.delete(dispatch.project);
|
|
12232
|
+
}
|
|
12233
|
+
}
|
|
12234
|
+
};
|
|
11907
12235
|
try {
|
|
11908
12236
|
if (seat === void 0) {
|
|
11909
12237
|
return await this.host.refuseTurn(dispatch, assignment.runner, "the runner is gone");
|
|
11910
12238
|
}
|
|
11911
|
-
const opened = await this.host.openTurn(dispatch, this.seatView(seat));
|
|
12239
|
+
const opened = await this.host.openTurn(dispatch, this.seatView(seat)).finally(opening);
|
|
11912
12240
|
if ("failed" in opened) {
|
|
11913
12241
|
return opened.failed;
|
|
11914
12242
|
}
|
|
@@ -11920,6 +12248,7 @@ var RunnerHub = class {
|
|
|
11920
12248
|
turnId = opened.job.turnId;
|
|
11921
12249
|
return await opened.ended;
|
|
11922
12250
|
} finally {
|
|
12251
|
+
opening();
|
|
11923
12252
|
if (seat !== void 0) {
|
|
11924
12253
|
seat.inUse = Math.max(0, seat.inUse - 1);
|
|
11925
12254
|
if (turnId !== null) {
|
|
@@ -11952,6 +12281,9 @@ var RunnerHub = class {
|
|
|
11952
12281
|
});
|
|
11953
12282
|
return "done";
|
|
11954
12283
|
}
|
|
12284
|
+
if (this.moving.has(project)) {
|
|
12285
|
+
return "deferred";
|
|
12286
|
+
}
|
|
11955
12287
|
let outcome = { ok: true, detail: "nothing to land, since the task left no branch" };
|
|
11956
12288
|
if (record.runner !== void 0) {
|
|
11957
12289
|
const seat = this.seats.get(record.runner);
|
|
@@ -12004,6 +12336,9 @@ var RunnerHub = class {
|
|
|
12004
12336
|
if (task.status !== "abandoned" || task.onDone !== "ghpr" || pull === void 0 || pull.closed !== void 0) {
|
|
12005
12337
|
return "done";
|
|
12006
12338
|
}
|
|
12339
|
+
if (this.moving.has(project)) {
|
|
12340
|
+
return "deferred";
|
|
12341
|
+
}
|
|
12007
12342
|
const record = await this.board.readProject(project);
|
|
12008
12343
|
let outcome = {
|
|
12009
12344
|
ok: false,
|
|
@@ -12036,16 +12371,171 @@ var RunnerHub = class {
|
|
|
12036
12371
|
});
|
|
12037
12372
|
return "done";
|
|
12038
12373
|
}
|
|
12374
|
+
/**
|
|
12375
|
+
* Moves a project to another runner, which only the user does, carrying every branch and tag of
|
|
12376
|
+
* its repository: the project's turns, landings, and pull-request closes wait while it runs.
|
|
12377
|
+
* The old runner pushes to a repository on the server once no worktree of the project holds work
|
|
12378
|
+
* no branch does, the new runner clones it, and the board records the new runner only once that
|
|
12379
|
+
* runner holds every branch and tag at the commit the old one had; until then the project works
|
|
12380
|
+
* where it was. The server's repository goes as the move ends, and the old runner then removes
|
|
12381
|
+
* its copy. With the old runner away nothing can travel, so the move goes ahead only when the
|
|
12382
|
+
* user says `withoutBranches`, and the branches stay there.
|
|
12383
|
+
*/
|
|
12384
|
+
async moveProject(actor, slug, to, options = {}) {
|
|
12385
|
+
const under = [...this.moving.keys()];
|
|
12386
|
+
if (under.length > 0) {
|
|
12387
|
+
throw new BoardError("INVALID_STATE", `a move of ${under.join(", ")} is under way; try again once it ends`);
|
|
12388
|
+
}
|
|
12389
|
+
const move = { id: null, from: null, to, phase: "checking" };
|
|
12390
|
+
this.moving.set(slug, move);
|
|
12391
|
+
this.moveStarts += 1;
|
|
12392
|
+
let source;
|
|
12393
|
+
let moved;
|
|
12394
|
+
try {
|
|
12395
|
+
if ((this.starting.get(slug) ?? 0) > 0) {
|
|
12396
|
+
throw new BoardError("INVALID_STATE", `a turn of ${slug} is starting; try again once it has started and ended`);
|
|
12397
|
+
}
|
|
12398
|
+
const { project, inPlay: inPlay2 } = await this.board.prepareProjectMove(actor, slug, to);
|
|
12399
|
+
const from = project.runner;
|
|
12400
|
+
move.from = from;
|
|
12401
|
+
const target = this.seats.get(to);
|
|
12402
|
+
if (target === void 0 || target.send === null) {
|
|
12403
|
+
throw new BoardError("INVALID_STATE", `runner ${to} is not connected`);
|
|
12404
|
+
}
|
|
12405
|
+
await this.assertCanHost(target, project, inPlay2);
|
|
12406
|
+
source = this.seats.get(from);
|
|
12407
|
+
if (source === void 0 || source.send === null) {
|
|
12408
|
+
if (options.withoutBranches !== true) {
|
|
12409
|
+
throw new MoveSourceAwayError(sourceAwayMessage(project, from, to, inPlay2));
|
|
12410
|
+
}
|
|
12411
|
+
move.phase = "recording";
|
|
12412
|
+
const recorded2 = await this.board.recordProjectMove(actor, {
|
|
12413
|
+
slug,
|
|
12414
|
+
from,
|
|
12415
|
+
to,
|
|
12416
|
+
carried: null
|
|
12417
|
+
});
|
|
12418
|
+
this.log.info({ project: slug, from, to }, "project moved without its branches");
|
|
12419
|
+
return { project: recorded2, carried: null, released: null };
|
|
12420
|
+
}
|
|
12421
|
+
if (options.withoutBranches === true) {
|
|
12422
|
+
throw new BoardError("INVALID_STATE", `runner ${from}, where ${slug} lives, is connected, so the move carries its branches`);
|
|
12423
|
+
}
|
|
12424
|
+
if (!source.hello.projectMoves) {
|
|
12425
|
+
throw new BoardError("INVALID_STATE", `runner ${from}, where ${slug} lives, cannot carry a project yet; upgrade it`);
|
|
12426
|
+
}
|
|
12427
|
+
const repo = projectRepo(project);
|
|
12428
|
+
move.id = await this.board.openMoveRepo();
|
|
12429
|
+
const id = move.id;
|
|
12430
|
+
move.phase = "export";
|
|
12431
|
+
const exported = ProjectExportedSchema.parse(await this.request(source, (request) => ({
|
|
12432
|
+
type: "export",
|
|
12433
|
+
request,
|
|
12434
|
+
export: { move: id, repo }
|
|
12435
|
+
})).catch((error) => {
|
|
12436
|
+
throw new BoardError("INVALID_STATE", `${from} could not send ${slug}: ${describe(error)}`);
|
|
12437
|
+
}));
|
|
12438
|
+
if (!exported.ok) {
|
|
12439
|
+
throw new BoardError("INVALID_STATE", exported.held.length === 0 ? `${slug}'s repository on ${from} is ${mebibytes(exported.bytes)}, more than a move carries (${mebibytes(MOVE_PACK_LIMIT_BYTES)})` : `${slug} cannot be moved while work no branch holds sits in its worktrees on ${from}: ${exported.held.map(heldLine).join("; ")}. Have each citizen commit it or clear it, then move again`);
|
|
12440
|
+
}
|
|
12441
|
+
move.phase = "import";
|
|
12442
|
+
const imported = ProjectImportedSchema.parse(await this.request(target, (request) => ({
|
|
12443
|
+
type: "import",
|
|
12444
|
+
request,
|
|
12445
|
+
import: { move: id, repo, pullRequests: project.onDone === "ghpr" }
|
|
12446
|
+
})).catch((error) => {
|
|
12447
|
+
throw new BoardError("INVALID_STATE", `${to} could not take ${slug}: ${describe(error)}`);
|
|
12448
|
+
}));
|
|
12449
|
+
move.phase = "recording";
|
|
12450
|
+
const differ = differentRefs(exported.refs, imported.refs);
|
|
12451
|
+
if (differ.length > 0) {
|
|
12452
|
+
await this.releaseOn(target, slug);
|
|
12453
|
+
throw new BoardError("INVALID_STATE", `${slug} arrived on ${to} unlike it left ${from} (${differ.slice(0, 5).join(", ")}${differ.length > 5 ? ", \u2026" : ""}); it stays on ${from}`);
|
|
12454
|
+
}
|
|
12455
|
+
const carried = Object.keys(exported.refs).length;
|
|
12456
|
+
const recorded = await this.board.recordProjectMove(actor, { slug, from, to, carried }).catch(async (error) => {
|
|
12457
|
+
await this.releaseOn(target, slug);
|
|
12458
|
+
throw error;
|
|
12459
|
+
});
|
|
12460
|
+
this.log.info({ project: slug, from, to, carried }, "project moved");
|
|
12461
|
+
moved = { project: recorded, carried };
|
|
12462
|
+
} finally {
|
|
12463
|
+
this.moving.delete(slug);
|
|
12464
|
+
if (move.id !== null) {
|
|
12465
|
+
const id = move.id;
|
|
12466
|
+
await this.board.dropMoveRepo(id).catch((error) => {
|
|
12467
|
+
this.log.warn({ move: id, error: String(error) }, "could not remove a move's repository");
|
|
12468
|
+
});
|
|
12469
|
+
}
|
|
12470
|
+
}
|
|
12471
|
+
return { ...moved, released: await this.releaseOn(source, slug) };
|
|
12472
|
+
}
|
|
12473
|
+
/**
|
|
12474
|
+
* Whether a runner may reach a move's repository on the server: the old runner to push while it
|
|
12475
|
+
* exports, the new one to fetch while it imports. `unknown` for a move that is not under way.
|
|
12476
|
+
*/
|
|
12477
|
+
moveAccess(runner, id, push) {
|
|
12478
|
+
const move = [...this.moving.values()].find((each) => each.id !== null && each.id === id);
|
|
12479
|
+
if (move === void 0) {
|
|
12480
|
+
return "unknown";
|
|
12481
|
+
}
|
|
12482
|
+
const allowed = push ? move.phase === "export" && runner === move.from : move.phase === "import" && runner === move.to;
|
|
12483
|
+
return allowed ? "allowed" : "refused";
|
|
12484
|
+
}
|
|
12485
|
+
/** Refuses a runner that cannot take a project's turns: its members' CLIs, the capabilities, a git identity. */
|
|
12486
|
+
async assertCanHost(seat, project, inPlay2) {
|
|
12487
|
+
if (!seat.hello.projectMoves) {
|
|
12488
|
+
throw new BoardError("INVALID_STATE", `runner ${seat.name} cannot take a project yet; upgrade it`);
|
|
12489
|
+
}
|
|
12490
|
+
const lacking = [];
|
|
12491
|
+
for (const name of project.members) {
|
|
12492
|
+
const agent = await this.board.readAgent(name).catch(() => null);
|
|
12493
|
+
if (agent !== null && agent.status === "active" && agent.cli !== null && !seat.hello.clis.includes(agent.cli)) {
|
|
12494
|
+
lacking.push(`${agent.cli} for ${agent.name}`);
|
|
12495
|
+
}
|
|
12496
|
+
}
|
|
12497
|
+
const needs = /* @__PURE__ */ new Set([
|
|
12498
|
+
...project.requiredCapabilities,
|
|
12499
|
+
...inPlay2.flatMap((task) => task.requiredCapabilities)
|
|
12500
|
+
]);
|
|
12501
|
+
const missing = [...needs].filter((each) => !seat.hello.capabilities.includes(each));
|
|
12502
|
+
if (missing.length > 0) {
|
|
12503
|
+
lacking.push(`the ${missing.length === 1 ? "capability" : "capabilities"} ${missing.join(", ")}`);
|
|
12504
|
+
}
|
|
12505
|
+
const identity = seat.hello.gitIdentityMissing ?? [];
|
|
12506
|
+
if (identity.length > 0) {
|
|
12507
|
+
lacking.push(`a git ${identity.join(" and ")} for citizens' commits`);
|
|
12508
|
+
}
|
|
12509
|
+
if (lacking.length > 0) {
|
|
12510
|
+
throw new BoardError("INVALID_STATE", `runner ${seat.name} cannot take ${project.slug}: it lacks ${lacking.join("; ")}`);
|
|
12511
|
+
}
|
|
12512
|
+
}
|
|
12513
|
+
/** Asks a runner to remove its copy of a project that lives elsewhere now; how that went, never a throw. */
|
|
12514
|
+
async releaseOn(seat, slug) {
|
|
12515
|
+
if (seat === void 0) {
|
|
12516
|
+
return null;
|
|
12517
|
+
}
|
|
12518
|
+
try {
|
|
12519
|
+
const released = ProjectReleasedSchema.parse(await this.request(seat, (request) => ({ type: "release", request, release: { slug } }), SWEEP_TIMEOUT_MS));
|
|
12520
|
+
return {
|
|
12521
|
+
ok: true,
|
|
12522
|
+
detail: `${seat.name} removed ${released.worktrees} worktree(s)${released.clone ? " and its clone" : ""}`
|
|
12523
|
+
};
|
|
12524
|
+
} catch (error) {
|
|
12525
|
+
this.log.warn({ runner: seat.name, project: slug, error: String(error) }, "could not release a project");
|
|
12526
|
+
return { ok: false, detail: `${seat.name} kept its copy of ${slug}: ${describe(error)}` };
|
|
12527
|
+
}
|
|
12528
|
+
}
|
|
12039
12529
|
/**
|
|
12040
12530
|
* One path on a branch of a project's repository, read on the runner the project lives on.
|
|
12041
12531
|
* NOT_FOUND when the project has no runner yet or the branch or the path does not exist, and
|
|
12042
12532
|
* `RunnerAwayError` while its runner is away.
|
|
12043
12533
|
*/
|
|
12044
|
-
async readBranch(project, branch,
|
|
12534
|
+
async readBranch(project, branch, path12) {
|
|
12045
12535
|
const { seat, repo } = await this.projectSeat(project, "branchReads", "read files");
|
|
12046
|
-
const file = BranchFileSchema.nullable().parse(await this.request(seat, (request) => ({ type: "file", request, read: { repo, branch, path:
|
|
12536
|
+
const file = BranchFileSchema.nullable().parse(await this.request(seat, (request) => ({ type: "file", request, read: { repo, branch, path: path12 } }), FILE_TIMEOUT_MS));
|
|
12047
12537
|
if (file === null) {
|
|
12048
|
-
throw new BoardError("NOT_FOUND", `${branch} has no ${
|
|
12538
|
+
throw new BoardError("NOT_FOUND", `${branch} has no ${path12 === "" ? "commit" : path12}`);
|
|
12049
12539
|
}
|
|
12050
12540
|
return file;
|
|
12051
12541
|
}
|
|
@@ -12065,11 +12555,11 @@ var RunnerHub = class {
|
|
|
12065
12555
|
* What a branch of a project's repository did to one file since it left the default branch, read
|
|
12066
12556
|
* on the runner the project lives on, with the same refusals as `branchChanges`.
|
|
12067
12557
|
*/
|
|
12068
|
-
async branchDiff(project, branch,
|
|
12558
|
+
async branchDiff(project, branch, path12) {
|
|
12069
12559
|
const { seat, repo } = await this.projectSeat(project, "branchChanges", "list changes");
|
|
12070
|
-
const diff = BranchFileDiffSchema.nullable().parse(await this.request(seat, (request) => ({ type: "diff", request, read: { repo, branch, path:
|
|
12560
|
+
const diff = BranchFileDiffSchema.nullable().parse(await this.request(seat, (request) => ({ type: "diff", request, read: { repo, branch, path: path12 } }), FILE_TIMEOUT_MS));
|
|
12071
12561
|
if (diff === null) {
|
|
12072
|
-
throw new BoardError("NOT_FOUND", `${branch} did not change ${
|
|
12562
|
+
throw new BoardError("NOT_FOUND", `${branch} did not change ${path12}`);
|
|
12073
12563
|
}
|
|
12074
12564
|
return diff;
|
|
12075
12565
|
}
|
|
@@ -12306,6 +12796,32 @@ var RunnerHub = class {
|
|
|
12306
12796
|
});
|
|
12307
12797
|
}
|
|
12308
12798
|
};
|
|
12799
|
+
function describe(error) {
|
|
12800
|
+
return error instanceof Error ? error.message : String(error);
|
|
12801
|
+
}
|
|
12802
|
+
function mebibytes(bytes) {
|
|
12803
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
12804
|
+
}
|
|
12805
|
+
function heldLine(held) {
|
|
12806
|
+
const where = held.conversation === null ? "its own worktree" : held.conversation.kind === "thread" ? `thread ${held.conversation.id}` : held.conversation.kind === "channel" ? `channel ${held.conversation.name}` : `the channels of ${held.conversation.project}`;
|
|
12807
|
+
const parts = [
|
|
12808
|
+
held.uncommitted > 0 ? `${held.uncommitted} uncommitted file(s)` : "",
|
|
12809
|
+
held.onNoBranch > 0 ? `${held.onNoBranch} commit(s) on no branch` : ""
|
|
12810
|
+
].filter((part) => part !== "");
|
|
12811
|
+
return `${held.agent}, ${where}: ${parts.join(" and ")}`;
|
|
12812
|
+
}
|
|
12813
|
+
function differentRefs(left, right) {
|
|
12814
|
+
const names = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
12815
|
+
return [...names].filter((name) => left[name] !== right[name]).toSorted();
|
|
12816
|
+
}
|
|
12817
|
+
function sourceAwayMessage(project, from, to, inPlay2) {
|
|
12818
|
+
const stays = [
|
|
12819
|
+
"its local branches and worktrees",
|
|
12820
|
+
project.repo === null ? `its whole history, since ${project.slug} has no remote (${to} would start an empty repository)` : "",
|
|
12821
|
+
inPlay2.length === 0 ? "" : `the work of ${inPlay2.length} task(s) in play (${inPlay2.map((task) => task.id).join(", ")})`
|
|
12822
|
+
].filter((part) => part !== "");
|
|
12823
|
+
return `runner ${from}, where ${project.slug} lives, is not connected, so nothing can travel. Staying on ${from}: ${stays.join("; ")}. Move it without them to go ahead`;
|
|
12824
|
+
}
|
|
12309
12825
|
|
|
12310
12826
|
// apps/server/dist/index.js
|
|
12311
12827
|
import pino from "pino";
|
|
@@ -12381,6 +12897,7 @@ import { z as z19, ZodError } from "zod";
|
|
|
12381
12897
|
// apps/server/dist/body-limit.js
|
|
12382
12898
|
import { bodyLimit } from "hono/body-limit";
|
|
12383
12899
|
var ENROLL_BODY_LIMIT_BYTES = 16 * 1024;
|
|
12900
|
+
var APP_TOKEN_BODY_LIMIT_BYTES = 16 * 1024;
|
|
12384
12901
|
var API_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
12385
12902
|
var RUNNER_BODY_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
12386
12903
|
var RUNNER_ANSWER_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
@@ -12398,13 +12915,19 @@ function limitBody(maxSize) {
|
|
|
12398
12915
|
}
|
|
12399
12916
|
|
|
12400
12917
|
// apps/server/dist/github-auth.js
|
|
12401
|
-
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
12918
|
+
import { createHash as createHash3, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
|
|
12402
12919
|
import { Hono } from "hono";
|
|
12403
12920
|
import { z as z18 } from "zod";
|
|
12404
12921
|
var SIGN_IN_TTL_MS = 30 * 24 * 36e5;
|
|
12405
12922
|
var STATE_TTL_MS = 10 * 6e4;
|
|
12406
12923
|
var MAX_STATES = 1e3;
|
|
12924
|
+
var CODE_TTL_MS = 2 * 6e4;
|
|
12407
12925
|
var TokenAnswerSchema = z18.object({ access_token: z18.string().min(1) });
|
|
12926
|
+
var CodeChallengeSchema = z18.string().regex(/^[A-Za-z0-9_-]{43}$/);
|
|
12927
|
+
var CodeExchangeSchema = z18.object({
|
|
12928
|
+
code: z18.string().min(1).max(64),
|
|
12929
|
+
code_verifier: z18.string().regex(/^[A-Za-z0-9._~-]{43,128}$/)
|
|
12930
|
+
});
|
|
12408
12931
|
var GithubUserSchema = z18.object({
|
|
12409
12932
|
login: z18.string().min(1),
|
|
12410
12933
|
id: z18.number().int(),
|
|
@@ -12429,32 +12952,56 @@ function safeNext(next) {
|
|
|
12429
12952
|
} catch {
|
|
12430
12953
|
return "/";
|
|
12431
12954
|
}
|
|
12432
|
-
const
|
|
12433
|
-
return url.origin === BOARD_ORIGIN && /^\/(?![/\\])/.test(
|
|
12955
|
+
const path12 = `${url.pathname}${url.search}`;
|
|
12956
|
+
return url.origin === BOARD_ORIGIN && /^\/(?![/\\])/.test(path12) ? path12 : "/";
|
|
12434
12957
|
}
|
|
12435
|
-
var APP_LANDING = "
|
|
12958
|
+
var APP_LANDING = "/auth/app/signin";
|
|
12436
12959
|
function land(c, next, fragment) {
|
|
12960
|
+
c.header("cache-control", "no-store");
|
|
12437
12961
|
return c.redirect(`${next}#${fragment}`, 302);
|
|
12438
12962
|
}
|
|
12963
|
+
function prune(map, now) {
|
|
12964
|
+
for (const [key, entry] of map) {
|
|
12965
|
+
if (entry.expiresAt <= now || map.size >= MAX_STATES) {
|
|
12966
|
+
map.delete(key);
|
|
12967
|
+
}
|
|
12968
|
+
}
|
|
12969
|
+
}
|
|
12970
|
+
function provesChallenge(verifier, challenge) {
|
|
12971
|
+
const answer = Buffer.from(createHash3("sha256").update(verifier).digest("base64url"));
|
|
12972
|
+
const expected = Buffer.from(challenge);
|
|
12973
|
+
return answer.length === expected.length && timingSafeEqual(answer, expected);
|
|
12974
|
+
}
|
|
12439
12975
|
function githubAuthRoutes(options) {
|
|
12440
12976
|
const { board: board2, github } = options;
|
|
12441
12977
|
const fetchImpl = options.fetch ?? fetch;
|
|
12442
12978
|
const now = options.now ?? Date.now;
|
|
12443
12979
|
const states = /* @__PURE__ */ new Map();
|
|
12980
|
+
const codes = /* @__PURE__ */ new Map();
|
|
12444
12981
|
const routes = new Hono();
|
|
12445
|
-
routes.get("/config", (c) => c.json({ github: github !== null }));
|
|
12982
|
+
routes.get("/config", (c) => c.json({ github: github !== null, appLanding: (github?.appleAppId ?? null) !== null }));
|
|
12983
|
+
routes.get("/app/signin", (c) => c.text("not found", 404, { "cache-control": "no-store" }));
|
|
12446
12984
|
if (github === null) {
|
|
12447
12985
|
return routes;
|
|
12448
12986
|
}
|
|
12449
|
-
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
|
|
12987
|
+
const limit2 = options.limit ?? ((_c, next) => next());
|
|
12988
|
+
routes.get("/github", limit2, (c) => {
|
|
12989
|
+
const app2 = c.req.query("landing") === "app";
|
|
12990
|
+
let challenge = null;
|
|
12991
|
+
if (app2) {
|
|
12992
|
+
if (github.appleAppId === null) {
|
|
12993
|
+
return c.text("Signing in to the app is off on this board", 403);
|
|
12994
|
+
}
|
|
12995
|
+
const parsed = CodeChallengeSchema.safeParse(c.req.query("code_challenge"));
|
|
12996
|
+
if (!parsed.success || c.req.query("code_challenge_method") !== "S256") {
|
|
12997
|
+
return c.text("Signing in to the app needs a code_challenge with method S256", 400);
|
|
12453
12998
|
}
|
|
12999
|
+
challenge = parsed.data;
|
|
12454
13000
|
}
|
|
13001
|
+
prune(states, now());
|
|
12455
13002
|
const state = randomBytes2(24).toString("base64url");
|
|
12456
|
-
const next =
|
|
12457
|
-
states.set(state, { next, expiresAt: now() + STATE_TTL_MS });
|
|
13003
|
+
const next = app2 ? APP_LANDING : safeNext(c.req.query("next"));
|
|
13004
|
+
states.set(state, { next, challenge, expiresAt: now() + STATE_TTL_MS });
|
|
12458
13005
|
const authorize = new URL("https://github.com/login/oauth/authorize");
|
|
12459
13006
|
authorize.searchParams.set("client_id", github.clientId);
|
|
12460
13007
|
authorize.searchParams.set("state", state);
|
|
@@ -12507,9 +13054,43 @@ function githubAuthRoutes(options) {
|
|
|
12507
13054
|
options.log?.warn({ login }, "GitHub sign-in refused a login that is not on the list");
|
|
12508
13055
|
return land(c, pending.next, "signin-error=not-allowed");
|
|
12509
13056
|
}
|
|
13057
|
+
if (pending.challenge !== null) {
|
|
13058
|
+
prune(codes, now());
|
|
13059
|
+
const appCode = randomBytes2(24).toString("base64url");
|
|
13060
|
+
codes.set(appCode, {
|
|
13061
|
+
challenge: pending.challenge,
|
|
13062
|
+
login,
|
|
13063
|
+
avatar,
|
|
13064
|
+
expiresAt: now() + CODE_TTL_MS
|
|
13065
|
+
});
|
|
13066
|
+
return land(c, pending.next, `code=${appCode}`);
|
|
13067
|
+
}
|
|
12510
13068
|
const token = await board2.signIn(login, SIGN_IN_TTL_MS, avatar);
|
|
12511
13069
|
return land(c, pending.next, `session=${encodeURIComponent(token)}`);
|
|
12512
13070
|
});
|
|
13071
|
+
if (github.appleAppId !== null) {
|
|
13072
|
+
routes.post("/app/token", limit2, limitBody(APP_TOKEN_BODY_LIMIT_BYTES), async (c) => {
|
|
13073
|
+
const body = CodeExchangeSchema.safeParse(await c.req.json().catch(() => null));
|
|
13074
|
+
if (!body.success) {
|
|
13075
|
+
return c.json({ error: "invalid_request" }, 400);
|
|
13076
|
+
}
|
|
13077
|
+
const pending = codes.get(body.data.code);
|
|
13078
|
+
codes.delete(body.data.code);
|
|
13079
|
+
if (pending === void 0 || pending.expiresAt <= now() || !provesChallenge(body.data.code_verifier, pending.challenge)) {
|
|
13080
|
+
return c.json({ error: "invalid_grant" }, 400);
|
|
13081
|
+
}
|
|
13082
|
+
const token = await board2.signIn(pending.login, SIGN_IN_TTL_MS, pending.avatar);
|
|
13083
|
+
return c.json({ session: token }, 200, { "cache-control": "no-store" });
|
|
13084
|
+
});
|
|
13085
|
+
}
|
|
13086
|
+
return routes;
|
|
13087
|
+
}
|
|
13088
|
+
function appleAppSiteAssociation(github) {
|
|
13089
|
+
const routes = new Hono();
|
|
13090
|
+
routes.get("/apple-app-site-association", (c) => {
|
|
13091
|
+
const appId = github?.appleAppId ?? null;
|
|
13092
|
+
return appId === null ? c.text("not found", 404) : c.json({ webcredentials: { apps: [appId] } });
|
|
13093
|
+
});
|
|
12513
13094
|
return routes;
|
|
12514
13095
|
}
|
|
12515
13096
|
|
|
@@ -12617,14 +13198,14 @@ function rateLimit(options) {
|
|
|
12617
13198
|
}
|
|
12618
13199
|
|
|
12619
13200
|
// apps/server/dist/runner-routes.js
|
|
12620
|
-
import
|
|
13201
|
+
import path9 from "node:path";
|
|
12621
13202
|
import { Hono as Hono2 } from "hono";
|
|
12622
13203
|
import { streamSSE } from "hono/streaming";
|
|
12623
13204
|
|
|
12624
13205
|
// apps/server/dist/home-git.js
|
|
12625
|
-
import { execa as
|
|
13206
|
+
import { execa as execa3 } from "execa";
|
|
12626
13207
|
var SMART_PATHS = /* @__PURE__ */ new Set(["/info/refs", "/git-upload-pack", "/git-receive-pack"]);
|
|
12627
|
-
var MAX_BYTES =
|
|
13208
|
+
var MAX_BYTES = MOVE_PACK_LIMIT_BYTES;
|
|
12628
13209
|
function parseCgi(output) {
|
|
12629
13210
|
const crlf = output.indexOf("\r\n\r\n");
|
|
12630
13211
|
const lf = output.indexOf("\n\n");
|
|
@@ -12655,9 +13236,9 @@ var HomeGit = class {
|
|
|
12655
13236
|
if (input.rest !== "/git-receive-pack") {
|
|
12656
13237
|
return serveHomeGit(input);
|
|
12657
13238
|
}
|
|
12658
|
-
const previous = this.pushes.get(input.
|
|
13239
|
+
const previous = this.pushes.get(input.repository) ?? Promise.resolve();
|
|
12659
13240
|
const run = previous.then(() => serveHomeGit(input), () => serveHomeGit(input));
|
|
12660
|
-
this.pushes.set(input.
|
|
13241
|
+
this.pushes.set(input.repository, run.catch(() => void 0));
|
|
12661
13242
|
return run;
|
|
12662
13243
|
}
|
|
12663
13244
|
};
|
|
@@ -12668,7 +13249,7 @@ async function serveHomeGit(input) {
|
|
|
12668
13249
|
const { request } = input;
|
|
12669
13250
|
const body = Buffer.from(await request.arrayBuffer());
|
|
12670
13251
|
if (body.length > MAX_BYTES) {
|
|
12671
|
-
return new Response("pack too large for
|
|
13252
|
+
return new Response("pack too large for the board to take", { status: 413 });
|
|
12672
13253
|
}
|
|
12673
13254
|
const url = new URL(request.url);
|
|
12674
13255
|
const env = {
|
|
@@ -12677,7 +13258,7 @@ async function serveHomeGit(input) {
|
|
|
12677
13258
|
GIT_PROJECT_ROOT: input.root,
|
|
12678
13259
|
GIT_HTTP_EXPORT_ALL: "1",
|
|
12679
13260
|
REQUEST_METHOD: request.method,
|
|
12680
|
-
PATH_INFO: `/${input.
|
|
13261
|
+
PATH_INFO: `/${input.repository}${input.rest}`,
|
|
12681
13262
|
QUERY_STRING: url.search.replace(/^\?/, ""),
|
|
12682
13263
|
CONTENT_TYPE: request.headers.get("content-type") ?? "",
|
|
12683
13264
|
CONTENT_LENGTH: String(body.length),
|
|
@@ -12692,11 +13273,14 @@ async function serveHomeGit(input) {
|
|
|
12692
13273
|
if (protocol !== null) {
|
|
12693
13274
|
env["HTTP_GIT_PROTOCOL"] = protocol;
|
|
12694
13275
|
}
|
|
12695
|
-
const result = await
|
|
13276
|
+
const result = await execa3("git", ["http-backend"], {
|
|
12696
13277
|
env,
|
|
12697
13278
|
extendEnv: false,
|
|
12698
13279
|
input: body,
|
|
12699
13280
|
encoding: "buffer",
|
|
13281
|
+
// The blank line that ends the headers ends the whole answer when the body is empty, as for
|
|
13282
|
+
// the 4-byte probe git sends before a pack too large for one post; stripped, it reads as none.
|
|
13283
|
+
stripFinalNewline: false,
|
|
12700
13284
|
reject: false,
|
|
12701
13285
|
maxBuffer: MAX_BYTES
|
|
12702
13286
|
});
|
|
@@ -12812,20 +13396,47 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
12812
13396
|
const marker = `/homes/${agent}/git`;
|
|
12813
13397
|
const at = c.req.path.indexOf(marker);
|
|
12814
13398
|
return homeGit.serve({
|
|
12815
|
-
root:
|
|
12816
|
-
agent,
|
|
13399
|
+
root: path9.dirname(repository),
|
|
13400
|
+
repository: agent,
|
|
12817
13401
|
rest: at === -1 ? "" : c.req.path.slice(at + marker.length),
|
|
12818
13402
|
request: c.req.raw,
|
|
12819
13403
|
runner: c.get("runner")
|
|
12820
13404
|
});
|
|
12821
13405
|
});
|
|
12822
13406
|
routes.route("/homes/:agent", home);
|
|
13407
|
+
routes.all("/moves/:id/git/*", async (c) => {
|
|
13408
|
+
const id = UlidSchema.safeParse(c.req.param("id"));
|
|
13409
|
+
const marker = `/moves/${c.req.param("id")}/git`;
|
|
13410
|
+
const at = c.req.path.indexOf(marker);
|
|
13411
|
+
const rest = at === -1 ? "" : c.req.path.slice(at + marker.length);
|
|
13412
|
+
const service = rest === "/info/refs" ? c.req.query("service") ?? "" : rest.slice(1);
|
|
13413
|
+
if (service !== "git-upload-pack" && service !== "git-receive-pack") {
|
|
13414
|
+
return c.json({ error: "NOT_FOUND", message: "not a smart HTTP request" }, 404);
|
|
13415
|
+
}
|
|
13416
|
+
const access3 = id.success ? hub.moveAccess(c.get("runner"), id.data, service === "git-receive-pack") : "unknown";
|
|
13417
|
+
if (access3 === "unknown") {
|
|
13418
|
+
return c.json({ error: "NOT_FOUND", message: "no such move under way" }, 404);
|
|
13419
|
+
}
|
|
13420
|
+
if (access3 === "refused" || !id.success) {
|
|
13421
|
+
return c.json({
|
|
13422
|
+
error: "FORBIDDEN",
|
|
13423
|
+
message: `this runner may not ${service === "git-receive-pack" ? "push to" : "fetch from"} this move now`
|
|
13424
|
+
}, 403);
|
|
13425
|
+
}
|
|
13426
|
+
return homeGit.serve({
|
|
13427
|
+
root: board2.moveRepos,
|
|
13428
|
+
repository: `${id.data}.git`,
|
|
13429
|
+
rest,
|
|
13430
|
+
request: c.req.raw,
|
|
13431
|
+
runner: c.get("runner")
|
|
13432
|
+
});
|
|
13433
|
+
});
|
|
12823
13434
|
return routes;
|
|
12824
13435
|
}
|
|
12825
13436
|
|
|
12826
13437
|
// apps/server/dist/static.js
|
|
12827
13438
|
import { readFile as readFile5, stat as stat4 } from "node:fs/promises";
|
|
12828
|
-
import
|
|
13439
|
+
import path10 from "node:path";
|
|
12829
13440
|
var CONTENT_TYPES = {
|
|
12830
13441
|
".html": "text/html; charset=utf-8",
|
|
12831
13442
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -12853,11 +13464,11 @@ function within2(root, pathname) {
|
|
|
12853
13464
|
} catch {
|
|
12854
13465
|
return null;
|
|
12855
13466
|
}
|
|
12856
|
-
const candidate =
|
|
12857
|
-
return candidate === root || candidate.startsWith(root +
|
|
13467
|
+
const candidate = path10.resolve(root, `.${decoded}`);
|
|
13468
|
+
return candidate === root || candidate.startsWith(root + path10.sep) ? candidate : null;
|
|
12858
13469
|
}
|
|
12859
13470
|
function spaHandler(staticDir) {
|
|
12860
|
-
const root =
|
|
13471
|
+
const root = path10.resolve(staticDir);
|
|
12861
13472
|
return async (c) => {
|
|
12862
13473
|
const { pathname } = new URL(c.req.url);
|
|
12863
13474
|
if (pathname.startsWith("/api/")) {
|
|
@@ -12868,14 +13479,14 @@ function spaHandler(staticDir) {
|
|
|
12868
13479
|
if (file !== null && await isFile(file)) {
|
|
12869
13480
|
return c.body(await readFile5(file), 200, {
|
|
12870
13481
|
...headers,
|
|
12871
|
-
"content-type": CONTENT_TYPES[
|
|
13482
|
+
"content-type": CONTENT_TYPES[path10.extname(file)] ?? "application/octet-stream",
|
|
12872
13483
|
"cache-control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache"
|
|
12873
13484
|
});
|
|
12874
13485
|
}
|
|
12875
13486
|
if (pathname.startsWith("/assets/")) {
|
|
12876
13487
|
return c.text("not found", 404, headers);
|
|
12877
13488
|
}
|
|
12878
|
-
const index =
|
|
13489
|
+
const index = path10.join(root, "index.html");
|
|
12879
13490
|
if (!await isFile(index)) {
|
|
12880
13491
|
return c.text("The interface is not built: run pnpm build:web.", 404);
|
|
12881
13492
|
}
|
|
@@ -12902,6 +13513,10 @@ var RetireBodySchema = z19.object({ reason: z19.string().min(1) });
|
|
|
12902
13513
|
var RunnerBodySchema = z19.object({ name: NameSchema });
|
|
12903
13514
|
var EnrollmentApprovalSchema = z19.object({ name: NameSchema });
|
|
12904
13515
|
var HomeRunnerBodySchema = z19.object({ runner: NameSchema.nullable() });
|
|
13516
|
+
var ProjectRunnerBodySchema = z19.object({
|
|
13517
|
+
runner: NameSchema,
|
|
13518
|
+
withoutBranches: z19.boolean().optional()
|
|
13519
|
+
});
|
|
12905
13520
|
var ModelBodySchema = z19.object({ model: ModelNameSchema.nullable() });
|
|
12906
13521
|
var EffortBodySchema = z19.object({ effort: EffortSchema.nullable() });
|
|
12907
13522
|
var HistoryLimitSchema = z19.coerce.number().int().min(1).max(500);
|
|
@@ -12935,6 +13550,9 @@ function createApp(deps) {
|
|
|
12935
13550
|
if (error instanceof RunnerAwayError) {
|
|
12936
13551
|
return c.json({ error: "RUNNER_AWAY", message: error.message }, 503);
|
|
12937
13552
|
}
|
|
13553
|
+
if (error instanceof MoveSourceAwayError) {
|
|
13554
|
+
return c.json({ error: "SOURCE_AWAY", message: error.message }, 409);
|
|
13555
|
+
}
|
|
12938
13556
|
return c.json({ error: "INTERNAL", message: error.message }, 500);
|
|
12939
13557
|
});
|
|
12940
13558
|
const authenticate = async (c, next) => {
|
|
@@ -13109,6 +13727,13 @@ function createApp(deps) {
|
|
|
13109
13727
|
return branchFile(c, project.slug, project.defaultBranch, file);
|
|
13110
13728
|
};
|
|
13111
13729
|
api.get("/projects/:slug/files", (c) => projectFile(c, ""));
|
|
13730
|
+
api.put("/projects/:slug/runner", async (c) => {
|
|
13731
|
+
const body = ProjectRunnerBodySchema.parse(await c.req.json());
|
|
13732
|
+
if (deps.runners === void 0) {
|
|
13733
|
+
throw new BoardError("NOT_FOUND", "this board has no runners to move a project between");
|
|
13734
|
+
}
|
|
13735
|
+
return c.json(await deps.runners.moveProject(c.get("actor"), NameSchema.parse(c.req.param("slug")), body.runner, { withoutBranches: body.withoutBranches }));
|
|
13736
|
+
});
|
|
13112
13737
|
api.get("/projects/:slug/files/:path{.+}", (c) => projectFile(c, c.req.param("path")));
|
|
13113
13738
|
api.get("/tasks/:id", async (c) => c.json((await board2.findTask(c.req.param("id"))).task));
|
|
13114
13739
|
api.get("/tasks/:id/thread", async (c) => c.json(await board2.listThread(c.req.param("id"))));
|
|
@@ -13286,6 +13911,7 @@ function createApp(deps) {
|
|
|
13286
13911
|
...deps.signIn,
|
|
13287
13912
|
limit: limited(SIGN_IN_LIMIT, "sign-ins")
|
|
13288
13913
|
}));
|
|
13914
|
+
app2.route("/.well-known", appleAppSiteAssociation(deps.signIn?.github ?? null));
|
|
13289
13915
|
if (deps.runners !== void 0) {
|
|
13290
13916
|
app2.route("/runner", runnerRoutes(board2, deps.runners, deps.enrollments, limited(ENROLL_LIMIT, "enrollment requests")));
|
|
13291
13917
|
}
|
|
@@ -13585,8 +14211,8 @@ var view = {
|
|
|
13585
14211
|
return scheduler.activeSignals;
|
|
13586
14212
|
}
|
|
13587
14213
|
};
|
|
13588
|
-
var webDir = process.env["STELLARIS_WEB_DIR"] ??
|
|
13589
|
-
var webBuilt = await access2(
|
|
14214
|
+
var webDir = process.env["STELLARIS_WEB_DIR"] ?? path11.resolve(import.meta.dirname, "../../web/dist");
|
|
14215
|
+
var webBuilt = await access2(path11.join(webDir, "index.html")).then(() => true, () => false);
|
|
13590
14216
|
var app = createApp({
|
|
13591
14217
|
board,
|
|
13592
14218
|
version: VERSION,
|
|
@@ -13610,6 +14236,7 @@ var server = serve({ fetch: app.fetch, port: config.port, hostname: config.host
|
|
|
13610
14236
|
webDir,
|
|
13611
14237
|
webBuilt,
|
|
13612
14238
|
signIn: config.github === null ? "token" : `github (${config.github.users.join(", ")})`,
|
|
14239
|
+
appleAppId: config.github?.appleAppId ?? null,
|
|
13613
14240
|
trustedProxies: config.trustedProxies
|
|
13614
14241
|
}, "board server listening");
|
|
13615
14242
|
});
|