runcastle 1.0.25 → 1.0.26
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/bin/runcastle.js +737 -98
- package/drizzle/0011_steep_ted_forrester.sql +2 -0
- package/drizzle/0012_greedy_lionheart.sql +1 -0
- package/drizzle/0013_sudden_naoko.sql +21 -0
- package/drizzle/meta/0011_snapshot.json +771 -0
- package/drizzle/meta/0012_snapshot.json +778 -0
- package/drizzle/meta/0013_snapshot.json +785 -0
- package/drizzle/meta/_journal.json +21 -0
- package/index.js +735 -96
- package/package.json +1 -1
- package/skills/burner/prepare-project.md +13 -2
- package/web/assets/index-BLrA7l4N.css +1 -0
- package/web/assets/index-BcwSVMee.js +138 -0
- package/web/index.html +2 -2
- package/web/assets/index-B4LHcg8h.css +0 -1
- package/web/assets/index-DrjQZmZ4.js +0 -110
package/bin/runcastle.js
CHANGED
|
@@ -22,19 +22,38 @@ __export(exports_paths, {
|
|
|
22
22
|
worktreeDir: () => worktreeDir,
|
|
23
23
|
sessionDir: () => sessionDir,
|
|
24
24
|
sandboxBuildDir: () => sandboxBuildDir,
|
|
25
|
+
sameDataDir: () => sameDataDir,
|
|
26
|
+
projectWorktreesDir: () => projectWorktreesDir,
|
|
27
|
+
prodDataDir: () => prodDataDir,
|
|
25
28
|
logsDir: () => logsDir,
|
|
26
29
|
featureDocsRel: () => featureDocsRel,
|
|
27
30
|
envPath: () => envPath,
|
|
31
|
+
devDataDir: () => devDataDir,
|
|
28
32
|
dbPath: () => dbPath,
|
|
29
33
|
dataDir: () => dataDir,
|
|
30
34
|
configPath: () => configPath,
|
|
31
35
|
burnCacheDir: () => burnCacheDir
|
|
32
36
|
});
|
|
33
37
|
import { homedir } from "os";
|
|
34
|
-
import { join } from "path";
|
|
35
|
-
function
|
|
38
|
+
import { join, resolve } from "path";
|
|
39
|
+
function prodDataDir() {
|
|
36
40
|
return join(homedir(), ".runcastle");
|
|
37
41
|
}
|
|
42
|
+
function devDataDir() {
|
|
43
|
+
const override = process.env.RUNCASTLE_DEV_DATA_DIR;
|
|
44
|
+
return override ? resolve(override) : join(homedir(), ".runcastle-dev");
|
|
45
|
+
}
|
|
46
|
+
function dataDir() {
|
|
47
|
+
const override = process.env.RUNCASTLE_DATA_DIR;
|
|
48
|
+
return override ? resolve(override) : prodDataDir();
|
|
49
|
+
}
|
|
50
|
+
function sameDataDir(a, b) {
|
|
51
|
+
const canon = (p) => {
|
|
52
|
+
const abs = resolve(p).replace(/[\\/]+$/, "");
|
|
53
|
+
return process.platform === "win32" ? abs.toLowerCase().replace(/\//g, "\\") : abs;
|
|
54
|
+
};
|
|
55
|
+
return canon(a) === canon(b);
|
|
56
|
+
}
|
|
38
57
|
function dbPath() {
|
|
39
58
|
return join(dataDir(), "runcastle.db");
|
|
40
59
|
}
|
|
@@ -50,8 +69,11 @@ function logsDir() {
|
|
|
50
69
|
function sessionDir(id) {
|
|
51
70
|
return join(dataDir(), "sessions", id);
|
|
52
71
|
}
|
|
72
|
+
function projectWorktreesDir(projectId) {
|
|
73
|
+
return join(dataDir(), "worktrees", projectId);
|
|
74
|
+
}
|
|
53
75
|
function worktreeDir(projectId, slug) {
|
|
54
|
-
return join(
|
|
76
|
+
return join(projectWorktreesDir(projectId), slug);
|
|
55
77
|
}
|
|
56
78
|
function burnCacheDir(pm) {
|
|
57
79
|
return join(dataDir(), "cache", pm);
|
|
@@ -117,6 +139,7 @@ var init_config = __esm(() => {
|
|
|
117
139
|
model: z.string().default("claude-opus-5"),
|
|
118
140
|
stepModels: z.partialRecord(ModelStep, z.string()).default({ smoke: DEFAULT_SMOKE_MODEL }),
|
|
119
141
|
sandbox: z.enum(["docker", "podman", "noSandbox"]).default("docker"),
|
|
142
|
+
sessionMcp: z.enum(["inherit", "runcastleOnly"]).default("inherit"),
|
|
120
143
|
mainBranch: z.string().default("main"),
|
|
121
144
|
sandboxImage: z.string().optional(),
|
|
122
145
|
burnConcurrency: z.number().int().min(1).max(8).default(3),
|
|
@@ -158,6 +181,8 @@ function loadConfig(env = process.env) {
|
|
|
158
181
|
overrides.smokeModel = env.RUNCASTLE_SMOKE_MODEL;
|
|
159
182
|
if (env.RUNCASTLE_SANDBOX)
|
|
160
183
|
overrides.sandbox = env.RUNCASTLE_SANDBOX;
|
|
184
|
+
if (env.RUNCASTLE_SESSION_MCP)
|
|
185
|
+
overrides.sessionMcp = env.RUNCASTLE_SESSION_MCP;
|
|
161
186
|
if (env.RUNCASTLE_MAIN_BRANCH)
|
|
162
187
|
overrides.mainBranch = env.RUNCASTLE_MAIN_BRANCH;
|
|
163
188
|
if (env.RUNCASTLE_SANDBOX_IMAGE)
|
|
@@ -230,7 +255,7 @@ var init_schemas = __esm(() => {
|
|
|
230
255
|
"shipped"
|
|
231
256
|
]);
|
|
232
257
|
TicketStatus = z2.enum(["pending", "burning", "done", "failed", "cancelled"]);
|
|
233
|
-
SessionKind = z2.enum(["ideation", "qa", "waypoint", "converge", "revisit"]);
|
|
258
|
+
SessionKind = z2.enum(["ideation", "qa", "waypoint", "converge", "revisit", "prepare"]);
|
|
234
259
|
RunStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
|
|
235
260
|
FeatureStatus = z2.enum(["active", "shipped", "archived"]);
|
|
236
261
|
SessionStatus = z2.enum(["launching", "live", "ended"]);
|
|
@@ -277,10 +302,13 @@ var init_schemas = __esm(() => {
|
|
|
277
302
|
"verifyCommands",
|
|
278
303
|
"knownFailures",
|
|
279
304
|
"devCommand",
|
|
305
|
+
"driveSetupCommand",
|
|
306
|
+
"driveStopCommand",
|
|
307
|
+
"driveEnv",
|
|
280
308
|
"dbResetCommand"
|
|
281
309
|
];
|
|
282
310
|
PreparedKey = z2.enum(PREPARED_KEYS);
|
|
283
|
-
FindingSource = z2.enum(["prep", "human"]);
|
|
311
|
+
FindingSource = z2.enum(["prep", "human", "session"]);
|
|
284
312
|
Project = z2.object({
|
|
285
313
|
id: z2.string(),
|
|
286
314
|
name: z2.string(),
|
|
@@ -291,7 +319,10 @@ var init_schemas = __esm(() => {
|
|
|
291
319
|
setupCommand: z2.string().optional(),
|
|
292
320
|
verifyCommands: z2.string().optional(),
|
|
293
321
|
knownFailures: z2.string().optional(),
|
|
294
|
-
dbResetCommand: z2.string().optional()
|
|
322
|
+
dbResetCommand: z2.string().optional(),
|
|
323
|
+
driveSetupCommand: z2.string().optional(),
|
|
324
|
+
driveStopCommand: z2.string().optional(),
|
|
325
|
+
driveEnv: z2.string().optional()
|
|
295
326
|
});
|
|
296
327
|
ProjectFinding = z2.object({
|
|
297
328
|
key: PreparedKey,
|
|
@@ -326,7 +357,8 @@ var init_schemas = __esm(() => {
|
|
|
326
357
|
});
|
|
327
358
|
SessionRow = z2.object({
|
|
328
359
|
id: z2.string(),
|
|
329
|
-
featureId: z2.string(),
|
|
360
|
+
featureId: z2.string().optional(),
|
|
361
|
+
projectId: z2.string().optional(),
|
|
330
362
|
kind: SessionKind,
|
|
331
363
|
ccSessionId: z2.string().optional(),
|
|
332
364
|
transcriptPath: z2.string().optional(),
|
|
@@ -523,7 +555,10 @@ var init_db_schema = __esm(() => {
|
|
|
523
555
|
setupCommand: text("setup_command"),
|
|
524
556
|
verifyCommands: text("verify_commands"),
|
|
525
557
|
knownFailures: text("known_failures"),
|
|
526
|
-
dbResetCommand: text("db_reset_command")
|
|
558
|
+
dbResetCommand: text("db_reset_command"),
|
|
559
|
+
driveSetupCommand: text("drive_setup_command"),
|
|
560
|
+
driveStopCommand: text("drive_stop_command"),
|
|
561
|
+
driveEnv: text("drive_env")
|
|
527
562
|
});
|
|
528
563
|
projectFindings = sqliteTable("project_findings", {
|
|
529
564
|
projectId: text("project_id").notNull(),
|
|
@@ -557,7 +592,8 @@ var init_db_schema = __esm(() => {
|
|
|
557
592
|
});
|
|
558
593
|
sessions = sqliteTable("sessions", {
|
|
559
594
|
id: text("id").primaryKey(),
|
|
560
|
-
featureId: text("feature_id")
|
|
595
|
+
featureId: text("feature_id"),
|
|
596
|
+
projectId: text("project_id"),
|
|
561
597
|
kind: text("kind").notNull().$type(),
|
|
562
598
|
ccSessionId: text("cc_session_id"),
|
|
563
599
|
transcriptPath: text("transcript_path"),
|
|
@@ -669,11 +705,11 @@ var init_client = __esm(() => {
|
|
|
669
705
|
|
|
670
706
|
// src/launcher/asset-paths.ts
|
|
671
707
|
import { existsSync as existsSync2 } from "fs";
|
|
672
|
-
import { join as join3, resolve } from "path";
|
|
708
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
673
709
|
function resolveAsset(envVar, fallback) {
|
|
674
710
|
const override = process.env[envVar];
|
|
675
711
|
if (override) {
|
|
676
|
-
const abs =
|
|
712
|
+
const abs = resolve2(override);
|
|
677
713
|
if (existsSync2(abs))
|
|
678
714
|
return abs;
|
|
679
715
|
throw new Error(`${envVar}=${override} does not exist (resolved ${abs})`);
|
|
@@ -1353,6 +1389,13 @@ function emit(ctx, featureId, e) {
|
|
|
1353
1389
|
function emitProject(ctx, projectId, e) {
|
|
1354
1390
|
return insertEvent(ctx, projectId, null, e);
|
|
1355
1391
|
}
|
|
1392
|
+
function emitForSession(ctx, session, e) {
|
|
1393
|
+
if (session.featureId)
|
|
1394
|
+
return emit(ctx, session.featureId, e);
|
|
1395
|
+
if (session.projectId)
|
|
1396
|
+
return emitProject(ctx, session.projectId, e);
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1356
1399
|
function insertEvent(ctx, projectId, featureId, e) {
|
|
1357
1400
|
const row = ctx.db.insert(events).values({
|
|
1358
1401
|
projectId,
|
|
@@ -1415,7 +1458,10 @@ function rowToProject(row) {
|
|
|
1415
1458
|
setupCommand: row.setupCommand ?? undefined,
|
|
1416
1459
|
verifyCommands: row.verifyCommands ?? undefined,
|
|
1417
1460
|
knownFailures: row.knownFailures ?? undefined,
|
|
1418
|
-
dbResetCommand: row.dbResetCommand ?? undefined
|
|
1461
|
+
dbResetCommand: row.dbResetCommand ?? undefined,
|
|
1462
|
+
driveSetupCommand: row.driveSetupCommand ?? undefined,
|
|
1463
|
+
driveStopCommand: row.driveStopCommand ?? undefined,
|
|
1464
|
+
driveEnv: row.driveEnv ?? undefined
|
|
1419
1465
|
};
|
|
1420
1466
|
}
|
|
1421
1467
|
function rowToRun(row) {
|
|
@@ -1432,7 +1478,8 @@ function rowToRun(row) {
|
|
|
1432
1478
|
function rowToSession(row) {
|
|
1433
1479
|
return {
|
|
1434
1480
|
id: row.id,
|
|
1435
|
-
featureId: row.featureId,
|
|
1481
|
+
featureId: row.featureId ?? undefined,
|
|
1482
|
+
projectId: row.projectId ?? undefined,
|
|
1436
1483
|
kind: row.kind,
|
|
1437
1484
|
ccSessionId: row.ccSessionId ?? undefined,
|
|
1438
1485
|
transcriptPath: row.transcriptPath ?? undefined,
|
|
@@ -1649,7 +1696,7 @@ function releaseForSession(ctx, claimant) {
|
|
|
1649
1696
|
const held = ctx.db.select().from(waypoints).where(and3(eq3(waypoints.claimedBy, claimant), eq3(waypoints.status, "claimed"))).all().map(rowToWaypoint);
|
|
1650
1697
|
return held.map((w) => release(ctx, w.id));
|
|
1651
1698
|
}
|
|
1652
|
-
function
|
|
1699
|
+
function resolve3(ctx, id, disposition, summary) {
|
|
1653
1700
|
const wp = getWaypoint(ctx, id);
|
|
1654
1701
|
const before = listByFeature(ctx, wp.featureId);
|
|
1655
1702
|
const bySeqBefore = new Map(before.map((w) => [w.seq, w]));
|
|
@@ -1692,7 +1739,8 @@ import { and as and4, desc as desc2, eq as eq4, inArray, isNotNull, sql as sql2
|
|
|
1692
1739
|
function createSessionRow(ctx, input) {
|
|
1693
1740
|
const row = ctx.db.insert(sessions).values({
|
|
1694
1741
|
id: newId("sess"),
|
|
1695
|
-
featureId: input.featureId,
|
|
1742
|
+
featureId: input.featureId ?? null,
|
|
1743
|
+
projectId: input.projectId ?? null,
|
|
1696
1744
|
kind: input.kind,
|
|
1697
1745
|
ccSessionId: null,
|
|
1698
1746
|
transcriptPath: null,
|
|
@@ -1789,7 +1837,7 @@ function attemptKickoff(ctx, session, d, delayMs, clearFirst = false) {
|
|
|
1789
1837
|
writeKickoffSequence(d.line, {
|
|
1790
1838
|
write: io.write,
|
|
1791
1839
|
alive: io.alive,
|
|
1792
|
-
onSubmitted: () =>
|
|
1840
|
+
onSubmitted: () => emitForSession(ctx, session, {
|
|
1793
1841
|
type: "session.kickoff",
|
|
1794
1842
|
message: attempt === 1 ? `${session.kind} session kicked off automatically` : `${session.kind} kickoff re-sent (attempt ${attempt}) \u2014 the first was never acknowledged`,
|
|
1795
1843
|
data: { sessionId: session.id, kind: session.kind, attempt }
|
|
@@ -1813,7 +1861,7 @@ function armConfirmation(ctx, session, d) {
|
|
|
1813
1861
|
function settleUndelivered(ctx, session, d, reason) {
|
|
1814
1862
|
d.settled = true;
|
|
1815
1863
|
stopTimers(d);
|
|
1816
|
-
|
|
1864
|
+
emitForSession(ctx, session, {
|
|
1817
1865
|
type: "session.kickoff_undelivered",
|
|
1818
1866
|
message: reason === "superseded" ? `the ${session.kind} briefing was never delivered \u2014 you typed first, so runcastle stopped injecting it` : `the ${session.kind} briefing was typed ${d.attempts}\xD7 but Claude Code never acknowledged it \u2014 send it again from the session strip`,
|
|
1819
1867
|
data: { sessionId: session.id, kind: session.kind, reason, attempts: d.attempts, line: d.line }
|
|
@@ -1877,7 +1925,7 @@ function armSessionReadyWatchdog(ctx, session) {
|
|
|
1877
1925
|
return;
|
|
1878
1926
|
if (!ptyIo(session.id).alive())
|
|
1879
1927
|
return;
|
|
1880
|
-
|
|
1928
|
+
emitForSession(ctx, session, {
|
|
1881
1929
|
type: "session.not_ready",
|
|
1882
1930
|
message: `the ${session.kind} terminal is open but Claude Code has not reported ready \u2014 check it for a prompt or dialog waiting on you, then send the briefing`,
|
|
1883
1931
|
data: { sessionId: session.id, kind: session.kind }
|
|
@@ -1907,6 +1955,14 @@ function mostRecentResumableSession(ctx, featureId, kind) {
|
|
|
1907
1955
|
const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.featureId, featureId), eq4(sessions.status, "ended"), isNotNull(sessions.ccSessionId), ...kind ? [eq4(sessions.kind, kind)] : [])).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1908
1956
|
return row ? rowToSession(row) : null;
|
|
1909
1957
|
}
|
|
1958
|
+
function activeProjectSession(ctx, projectId, kind) {
|
|
1959
|
+
const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.projectId, projectId), eq4(sessions.kind, kind), inArray(sessions.status, ["launching", "live"]))).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1960
|
+
return row ? rowToSession(row) : null;
|
|
1961
|
+
}
|
|
1962
|
+
function mostRecentResumableProjectSession(ctx, projectId, kind) {
|
|
1963
|
+
const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.projectId, projectId), eq4(sessions.kind, kind), eq4(sessions.status, "ended"), isNotNull(sessions.ccSessionId))).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1964
|
+
return row ? rowToSession(row) : null;
|
|
1965
|
+
}
|
|
1910
1966
|
function mostRecentLiveSession(ctx) {
|
|
1911
1967
|
const row = ctx.db.select().from(sessions).where(eq4(sessions.status, "live")).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1912
1968
|
return row ? rowToSession(row) : null;
|
|
@@ -1925,7 +1981,8 @@ var init_sessions = __esm(() => {
|
|
|
1925
1981
|
qa: "Proceed with your task: invoke the /runcastle:qa skill and answer questions from the " + "docs and code \u2014 do not advance phases or emit tickets.",
|
|
1926
1982
|
waypoint: "Proceed with your task: invoke the /runcastle:waypoint skill and work your assigned " + "waypoint to a resolution.",
|
|
1927
1983
|
converge: CONVERGE_KICKOFF_LINE,
|
|
1928
|
-
revisit: "Proceed with your task: invoke the /runcastle:revisit skill and work through what the " + "human brings up."
|
|
1984
|
+
revisit: "Proceed with your task: invoke the /runcastle:revisit skill and work through what the " + "human brings up.",
|
|
1985
|
+
prepare: "Proceed with your task: work through the unestablished preparation fields with the human. " + "Start by telling them which fields are still open and what you need from them for each; " + "ask before running anything that touches their database or services."
|
|
1929
1986
|
};
|
|
1930
1987
|
RESUME_KICKOFF_PREFIX = "We are picking this conversation back up \u2014 runcastle restarted and closed the terminal, " + "but this conversation is intact. Do NOT start over: tell me in one or two lines where we " + "left off and what is next, then carry on. Your original instruction was: ";
|
|
1931
1988
|
pendingKickoffOverrides = new Map;
|
|
@@ -1943,7 +2000,7 @@ function reconcileStaleSessions(ctx) {
|
|
|
1943
2000
|
continue;
|
|
1944
2001
|
markSessionEnded(ctx, session.id);
|
|
1945
2002
|
const released = releaseForSession(ctx, session.id);
|
|
1946
|
-
|
|
2003
|
+
emitForSession(ctx, session, {
|
|
1947
2004
|
type: "session.reconciled",
|
|
1948
2005
|
message: `session marked ended at boot \u2014 the server restarted while it was ${session.status}`,
|
|
1949
2006
|
data: {
|
|
@@ -1967,11 +2024,11 @@ var init_reconcile = __esm(() => {
|
|
|
1967
2024
|
|
|
1968
2025
|
// src/launcher/skills-root.ts
|
|
1969
2026
|
import { existsSync as existsSync5 } from "fs";
|
|
1970
|
-
import { dirname, join as join5, resolve as
|
|
2027
|
+
import { dirname, join as join5, resolve as resolve4 } from "path";
|
|
1971
2028
|
function resolveSkillsRoot(fromDir) {
|
|
1972
2029
|
const override = process.env[SKILLS_DIR_ENV];
|
|
1973
2030
|
if (override) {
|
|
1974
|
-
const root =
|
|
2031
|
+
const root = resolve4(override);
|
|
1975
2032
|
if (existsSync5(join5(root, SKILLS_MARKER)))
|
|
1976
2033
|
return root;
|
|
1977
2034
|
throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
|
|
@@ -2012,7 +2069,7 @@ function devSpawnTarget(devCommand) {
|
|
|
2012
2069
|
return { file: "/bin/sh", args: ["-c", devCommand] };
|
|
2013
2070
|
}
|
|
2014
2071
|
function startDevPane(input) {
|
|
2015
|
-
const { ctx, featureId, repoPath, devCommand, onUrl } = input;
|
|
2072
|
+
const { ctx, featureId, repoPath, devCommand, env, onUrl } = input;
|
|
2016
2073
|
const paneId = drivePaneId(featureId);
|
|
2017
2074
|
const { file, args } = devSpawnTarget(devCommand);
|
|
2018
2075
|
try {
|
|
@@ -2020,7 +2077,7 @@ function startDevPane(input) {
|
|
|
2020
2077
|
sessionId: paneId,
|
|
2021
2078
|
cmd: file,
|
|
2022
2079
|
args,
|
|
2023
|
-
opts: { cwd: repoPath, env: process.env, cols: 80, rows: 24, useConpty: true }
|
|
2080
|
+
opts: { cwd: repoPath, env: env ?? process.env, cols: 80, rows: 24, useConpty: true }
|
|
2024
2081
|
});
|
|
2025
2082
|
} catch (err) {
|
|
2026
2083
|
emit(ctx, featureId, {
|
|
@@ -2070,10 +2127,179 @@ var init_dev_pane = __esm(() => {
|
|
|
2070
2127
|
init_registry();
|
|
2071
2128
|
});
|
|
2072
2129
|
|
|
2130
|
+
// src/services/drive-hooks.ts
|
|
2131
|
+
import { spawn as spawn2 } from "child_process";
|
|
2132
|
+
function tailLines(text2, max = OUTPUT_TAIL_LINES) {
|
|
2133
|
+
const lines = text2.replace(/\r\n/g, `
|
|
2134
|
+
`).split(`
|
|
2135
|
+
`);
|
|
2136
|
+
while (lines.length > 0 && lines[lines.length - 1]?.trim() === "")
|
|
2137
|
+
lines.pop();
|
|
2138
|
+
return lines.slice(-max).join(`
|
|
2139
|
+
`).trim();
|
|
2140
|
+
}
|
|
2141
|
+
function hookSpawnTarget(command) {
|
|
2142
|
+
if (process.platform === "win32") {
|
|
2143
|
+
return {
|
|
2144
|
+
file: process.env.ComSpec ?? "cmd.exe",
|
|
2145
|
+
args: ["/d", "/s", "/c", `"${command}"`],
|
|
2146
|
+
verbatim: true
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
return { file: "/bin/sh", args: ["-c", command], verbatim: false };
|
|
2150
|
+
}
|
|
2151
|
+
function runDriveHook(command, opts) {
|
|
2152
|
+
const spawnFn = opts.spawnFn ?? spawn2;
|
|
2153
|
+
const now = opts.now ?? Date.now;
|
|
2154
|
+
const timeoutMs = opts.timeoutMs ?? DRIVE_HOOK_TIMEOUT_MS;
|
|
2155
|
+
const startedAt = now();
|
|
2156
|
+
const { file, args, verbatim } = hookSpawnTarget(command);
|
|
2157
|
+
return new Promise((resolve5) => {
|
|
2158
|
+
let output = "";
|
|
2159
|
+
let timedOut = false;
|
|
2160
|
+
let settled = false;
|
|
2161
|
+
let timer;
|
|
2162
|
+
const finish = (exitCode) => {
|
|
2163
|
+
if (settled)
|
|
2164
|
+
return;
|
|
2165
|
+
settled = true;
|
|
2166
|
+
if (timer)
|
|
2167
|
+
clearTimeout(timer);
|
|
2168
|
+
resolve5({
|
|
2169
|
+
ok: !timedOut && exitCode === 0,
|
|
2170
|
+
exitCode,
|
|
2171
|
+
timedOut,
|
|
2172
|
+
output: tailLines(output),
|
|
2173
|
+
durationMs: now() - startedAt
|
|
2174
|
+
});
|
|
2175
|
+
};
|
|
2176
|
+
let child;
|
|
2177
|
+
try {
|
|
2178
|
+
child = spawnFn(file, args, {
|
|
2179
|
+
cwd: opts.cwd,
|
|
2180
|
+
env: opts.env ?? process.env,
|
|
2181
|
+
windowsHide: true,
|
|
2182
|
+
windowsVerbatimArguments: verbatim
|
|
2183
|
+
});
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
output = err instanceof Error ? err.message : String(err);
|
|
2186
|
+
finish(null);
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
timer = setTimeout(() => {
|
|
2190
|
+
timedOut = true;
|
|
2191
|
+
child.kill();
|
|
2192
|
+
const grace = setTimeout(() => finish(null), KILL_GRACE_MS);
|
|
2193
|
+
grace.unref?.();
|
|
2194
|
+
}, timeoutMs);
|
|
2195
|
+
timer.unref?.();
|
|
2196
|
+
child.stdout?.on("data", (d) => {
|
|
2197
|
+
output += d.toString();
|
|
2198
|
+
});
|
|
2199
|
+
child.stderr?.on("data", (d) => {
|
|
2200
|
+
output += d.toString();
|
|
2201
|
+
});
|
|
2202
|
+
child.on("error", (err) => {
|
|
2203
|
+
output += `
|
|
2204
|
+
${err instanceof Error ? err.message : String(err)}`;
|
|
2205
|
+
finish(null);
|
|
2206
|
+
});
|
|
2207
|
+
child.on("close", (code) => {
|
|
2208
|
+
finish(code);
|
|
2209
|
+
});
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
function describeHookResult(command, result) {
|
|
2213
|
+
const secs = Math.max(1, Math.round(result.durationMs / 1000));
|
|
2214
|
+
if (result.timedOut)
|
|
2215
|
+
return `\`${command}\` timed out after ${secs}s`;
|
|
2216
|
+
if (result.ok)
|
|
2217
|
+
return `\`${command}\` finished in ${secs}s`;
|
|
2218
|
+
return `\`${command}\` exited ${result.exitCode ?? "without a code"} after ${secs}s`;
|
|
2219
|
+
}
|
|
2220
|
+
var DRIVE_HOOK_TIMEOUT_MS, OUTPUT_TAIL_LINES = 40, KILL_GRACE_MS = 2000;
|
|
2221
|
+
var init_drive_hooks = __esm(() => {
|
|
2222
|
+
DRIVE_HOOK_TIMEOUT_MS = 10 * 60000;
|
|
2223
|
+
});
|
|
2224
|
+
|
|
2225
|
+
// src/services/drive-env.ts
|
|
2226
|
+
import { createHash } from "crypto";
|
|
2227
|
+
function identifierSafe(slug, max = MAX_ID_LENGTH) {
|
|
2228
|
+
const cleaned = slug.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2229
|
+
const safe = cleaned === "" || /^[0-9]/.test(cleaned) ? `x_${cleaned}` : cleaned;
|
|
2230
|
+
if (safe.length <= max)
|
|
2231
|
+
return safe;
|
|
2232
|
+
const digest = createHash("sha1").update(slug).digest("hex").slice(0, 6);
|
|
2233
|
+
return `${safe.slice(0, max - digest.length - 1)}_${digest}`;
|
|
2234
|
+
}
|
|
2235
|
+
function driveVars(identity) {
|
|
2236
|
+
return {
|
|
2237
|
+
slug: identity.slug,
|
|
2238
|
+
branch: identity.branch,
|
|
2239
|
+
id: identifierSafe(identity.slug)
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
function renderTemplate(value, vars) {
|
|
2243
|
+
const unknown2 = [];
|
|
2244
|
+
const out = value.replace(PLACEHOLDER_RE, (match, name) => {
|
|
2245
|
+
const replacement = vars[name];
|
|
2246
|
+
if (replacement === undefined) {
|
|
2247
|
+
if (!unknown2.includes(name))
|
|
2248
|
+
unknown2.push(name);
|
|
2249
|
+
return match;
|
|
2250
|
+
}
|
|
2251
|
+
return replacement;
|
|
2252
|
+
});
|
|
2253
|
+
return { value: out, unknown: unknown2 };
|
|
2254
|
+
}
|
|
2255
|
+
function parseDriveEnv(raw, identity) {
|
|
2256
|
+
const vars = {};
|
|
2257
|
+
const unknown2 = [];
|
|
2258
|
+
if (!raw?.trim())
|
|
2259
|
+
return { vars, unknown: unknown2 };
|
|
2260
|
+
const substitutions = driveVars(identity);
|
|
2261
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
2262
|
+
const trimmed = line.trim();
|
|
2263
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
2264
|
+
continue;
|
|
2265
|
+
const eq5 = trimmed.indexOf("=");
|
|
2266
|
+
if (eq5 <= 0)
|
|
2267
|
+
continue;
|
|
2268
|
+
const key = trimmed.slice(0, eq5).trim().replace(/^export\s+/, "");
|
|
2269
|
+
if (key === "")
|
|
2270
|
+
continue;
|
|
2271
|
+
const rendered = renderTemplate(unquote(trimmed.slice(eq5 + 1).trim()), substitutions);
|
|
2272
|
+
vars[key] = rendered.value;
|
|
2273
|
+
for (const name of rendered.unknown)
|
|
2274
|
+
if (!unknown2.includes(name))
|
|
2275
|
+
unknown2.push(name);
|
|
2276
|
+
}
|
|
2277
|
+
return { vars, unknown: unknown2 };
|
|
2278
|
+
}
|
|
2279
|
+
function unquote(value) {
|
|
2280
|
+
if (value.length < 2)
|
|
2281
|
+
return value;
|
|
2282
|
+
const first = value[0];
|
|
2283
|
+
if ((first === '"' || first === "'") && value.endsWith(first))
|
|
2284
|
+
return value.slice(1, -1);
|
|
2285
|
+
return value;
|
|
2286
|
+
}
|
|
2287
|
+
function driveProcessEnv(overrides, base = process.env) {
|
|
2288
|
+
return { ...base, ...overrides };
|
|
2289
|
+
}
|
|
2290
|
+
function describeDriveEnv(vars) {
|
|
2291
|
+
const keys = Object.keys(vars);
|
|
2292
|
+
return `drive environment: ${keys.join(", ")}`;
|
|
2293
|
+
}
|
|
2294
|
+
var MAX_ID_LENGTH = 40, PLACEHOLDER_RE;
|
|
2295
|
+
var init_drive_env = __esm(() => {
|
|
2296
|
+
PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
|
|
2297
|
+
});
|
|
2298
|
+
|
|
2073
2299
|
// src/services/git.ts
|
|
2074
2300
|
import { existsSync as existsSync6, mkdirSync as mkdirSync2, mkdtempSync, realpathSync, rmSync } from "fs";
|
|
2075
2301
|
import { tmpdir } from "os";
|
|
2076
|
-
import { dirname as dirname2, join as join6, resolve as
|
|
2302
|
+
import { dirname as dirname2, join as join6, resolve as resolve5 } from "path";
|
|
2077
2303
|
import { simpleGit } from "simple-git";
|
|
2078
2304
|
function migrationPaths(paths) {
|
|
2079
2305
|
return paths.filter((p) => MIGRATION_DIR_RE.test(p) || DRIZZLE_SQL_RE.test(p));
|
|
@@ -2088,7 +2314,7 @@ function errMsg(e) {
|
|
|
2088
2314
|
return e instanceof Error ? e.message : String(e);
|
|
2089
2315
|
}
|
|
2090
2316
|
function canon(p) {
|
|
2091
|
-
let abs =
|
|
2317
|
+
let abs = resolve5(p);
|
|
2092
2318
|
try {
|
|
2093
2319
|
abs = realpathSync.native(abs);
|
|
2094
2320
|
} catch {}
|
|
@@ -2583,7 +2809,7 @@ async function deleteFeatureBranches(repoPath, slug) {
|
|
|
2583
2809
|
return { deleted, kept };
|
|
2584
2810
|
}
|
|
2585
2811
|
async function commitDocs(worktreePath, message) {
|
|
2586
|
-
if (!existsSync6(
|
|
2812
|
+
if (!existsSync6(resolve5(worktreePath, DOCS_PATHSPEC)))
|
|
2587
2813
|
return;
|
|
2588
2814
|
const g = git(worktreePath);
|
|
2589
2815
|
await g.add([DOCS_PATHSPEC]);
|
|
@@ -2628,6 +2854,7 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2628
2854
|
const devPaneId = testDriveState.devPaneId;
|
|
2629
2855
|
if (devPaneId)
|
|
2630
2856
|
stopDevPane(devPaneId);
|
|
2857
|
+
const teardownFailure = await runDriveHookStep(ctx, feature.id, project.repoPath, "teardown", project.driveStopCommand, driveEnvFor(ctx, project, feature, branch));
|
|
2631
2858
|
const carriedChanges = await dirtyPaths(g);
|
|
2632
2859
|
await g.checkout(previousBranch2);
|
|
2633
2860
|
if (detachedWorktree2)
|
|
@@ -2650,7 +2877,8 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2650
2877
|
ok: true,
|
|
2651
2878
|
branch: previousBranch2,
|
|
2652
2879
|
...carriedChanges.length > 0 ? { carriedChanges } : {},
|
|
2653
|
-
...dbDrift ? { dbDrift } : {}
|
|
2880
|
+
...dbDrift ? { dbDrift } : {},
|
|
2881
|
+
...teardownFailure ? { hookFailure: teardownFailure } : {}
|
|
2654
2882
|
};
|
|
2655
2883
|
}
|
|
2656
2884
|
const porcelain = (await g.raw(["status", "--porcelain"])).trim();
|
|
@@ -2675,18 +2903,76 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2675
2903
|
message: `test driving ${branch} (was on ${previousBranch})`,
|
|
2676
2904
|
data: { branch, previousBranch }
|
|
2677
2905
|
});
|
|
2906
|
+
const driveEnv = driveEnvFor(ctx, project, feature, branch);
|
|
2907
|
+
const setupFailure = await runDriveHookStep(ctx, feature.id, project.repoPath, "setup", project.driveSetupCommand, driveEnv);
|
|
2678
2908
|
if (project.devCommand) {
|
|
2679
2909
|
const devPaneId = startDevPane({
|
|
2680
2910
|
ctx,
|
|
2681
2911
|
featureId: feature.id,
|
|
2682
2912
|
repoPath: project.repoPath,
|
|
2683
2913
|
devCommand: project.devCommand,
|
|
2914
|
+
env: driveEnv,
|
|
2684
2915
|
onUrl: (url) => recordDriveUrl(ctx, feature.id, url)
|
|
2685
2916
|
});
|
|
2686
2917
|
if (devPaneId)
|
|
2687
2918
|
testDriveState.devPaneId = devPaneId;
|
|
2688
2919
|
}
|
|
2689
|
-
return { ok: true, branch };
|
|
2920
|
+
return { ok: true, branch, ...setupFailure ? { hookFailure: setupFailure } : {} };
|
|
2921
|
+
}
|
|
2922
|
+
function driveEnvFor(ctx, project, feature, branch) {
|
|
2923
|
+
const { vars, unknown: unknown2 } = parseDriveEnv(project.driveEnv, { slug: feature.slug, branch });
|
|
2924
|
+
if (unknown2.length > 0) {
|
|
2925
|
+
emit(ctx, feature.id, {
|
|
2926
|
+
type: "testdrive.env_unknown_placeholder",
|
|
2927
|
+
message: `drive environment left ${unknown2.map((u) => `{{${u}}}`).join(", ")} unsubstituted \u2014 known variables are {{slug}}, {{branch}}, {{id}}`,
|
|
2928
|
+
data: { unknown: unknown2 }
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
if (Object.keys(vars).length > 0) {
|
|
2932
|
+
emit(ctx, feature.id, {
|
|
2933
|
+
type: "testdrive.env",
|
|
2934
|
+
message: describeDriveEnv(vars),
|
|
2935
|
+
data: { keys: Object.keys(vars) }
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
return driveProcessEnv(vars);
|
|
2939
|
+
}
|
|
2940
|
+
async function runDriveHookStep(ctx, featureId, repoPath, phase, command, env) {
|
|
2941
|
+
const cmd = command?.trim();
|
|
2942
|
+
if (!cmd)
|
|
2943
|
+
return;
|
|
2944
|
+
emit(ctx, featureId, {
|
|
2945
|
+
type: `testdrive.${phase}_started`,
|
|
2946
|
+
message: phase === "setup" ? `preparing environment: \`${cmd}\`` : `tearing down environment: \`${cmd}\``,
|
|
2947
|
+
data: { command: cmd }
|
|
2948
|
+
});
|
|
2949
|
+
const result = await runDriveHook(cmd, { cwd: repoPath, env });
|
|
2950
|
+
if (result.ok) {
|
|
2951
|
+
emit(ctx, featureId, {
|
|
2952
|
+
type: `testdrive.${phase}_ok`,
|
|
2953
|
+
message: describeHookResult(cmd, result),
|
|
2954
|
+
data: { command: cmd, durationMs: result.durationMs }
|
|
2955
|
+
});
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
emit(ctx, featureId, {
|
|
2959
|
+
type: `testdrive.${phase}_failed`,
|
|
2960
|
+
message: `${describeHookResult(cmd, result)}${result.output ? `
|
|
2961
|
+
${result.output}` : ""}`,
|
|
2962
|
+
data: {
|
|
2963
|
+
command: cmd,
|
|
2964
|
+
exitCode: result.exitCode,
|
|
2965
|
+
timedOut: result.timedOut,
|
|
2966
|
+
output: result.output
|
|
2967
|
+
}
|
|
2968
|
+
});
|
|
2969
|
+
return {
|
|
2970
|
+
phase,
|
|
2971
|
+
command: cmd,
|
|
2972
|
+
exitCode: result.exitCode,
|
|
2973
|
+
timedOut: result.timedOut,
|
|
2974
|
+
output: result.output
|
|
2975
|
+
};
|
|
2690
2976
|
}
|
|
2691
2977
|
async function dirtyPaths(g) {
|
|
2692
2978
|
try {
|
|
@@ -2777,6 +3063,8 @@ var init_git = __esm(() => {
|
|
|
2777
3063
|
init_paths();
|
|
2778
3064
|
init_errors();
|
|
2779
3065
|
init_dev_pane();
|
|
3066
|
+
init_drive_hooks();
|
|
3067
|
+
init_drive_env();
|
|
2780
3068
|
init_events();
|
|
2781
3069
|
init_repo();
|
|
2782
3070
|
MIGRATION_DIR_RE = /(^|\/)(migrations|migrate)\//i;
|
|
@@ -2962,7 +3250,7 @@ function detectCycle(tickets2) {
|
|
|
2962
3250
|
}
|
|
2963
3251
|
return null;
|
|
2964
3252
|
}
|
|
2965
|
-
function
|
|
3253
|
+
function renderTemplate2(template, values) {
|
|
2966
3254
|
let out = template;
|
|
2967
3255
|
for (const [key, value] of Object.entries(values)) {
|
|
2968
3256
|
out = out.split(`{{${key}}}`).join(value);
|
|
@@ -2970,7 +3258,7 @@ function renderTemplate(template, values) {
|
|
|
2970
3258
|
return out;
|
|
2971
3259
|
}
|
|
2972
3260
|
function renderTicketPrompt(template, values) {
|
|
2973
|
-
return
|
|
3261
|
+
return renderTemplate2(template, values);
|
|
2974
3262
|
}
|
|
2975
3263
|
function buildTicketJson(ticket) {
|
|
2976
3264
|
return JSON.stringify({
|
|
@@ -3058,6 +3346,9 @@ function detectPackageManager(tc) {
|
|
|
3058
3346
|
return "npm";
|
|
3059
3347
|
return tc.hasPackageJson ? "npm" : undefined;
|
|
3060
3348
|
}
|
|
3349
|
+
function orFallback(strict, permissive) {
|
|
3350
|
+
return `( ${strict} || ${permissive} )`;
|
|
3351
|
+
}
|
|
3061
3352
|
function resolveSetupCommand(tc, override) {
|
|
3062
3353
|
const trimmed = override?.trim();
|
|
3063
3354
|
if (trimmed)
|
|
@@ -3067,13 +3358,13 @@ function resolveSetupCommand(tc, override) {
|
|
|
3067
3358
|
return;
|
|
3068
3359
|
switch (pm) {
|
|
3069
3360
|
case "bun":
|
|
3070
|
-
return tc.lockfiles.bun ? "bun install --frozen-lockfile" : "bun install";
|
|
3361
|
+
return tc.lockfiles.bun ? orFallback("bun install --frozen-lockfile", "bun install") : "bun install";
|
|
3071
3362
|
case "pnpm":
|
|
3072
|
-
return tc.lockfiles.pnpm ? "corepack pnpm install --frozen-lockfile" : "corepack pnpm install";
|
|
3363
|
+
return tc.lockfiles.pnpm ? orFallback("corepack pnpm install --frozen-lockfile", "corepack pnpm install") : "corepack pnpm install";
|
|
3073
3364
|
case "yarn":
|
|
3074
|
-
return tc.lockfiles.yarn ? "corepack yarn install --frozen-lockfile" : "corepack yarn install";
|
|
3365
|
+
return tc.lockfiles.yarn ? orFallback("corepack yarn install --frozen-lockfile", "corepack yarn install") : "corepack yarn install";
|
|
3075
3366
|
case "npm":
|
|
3076
|
-
return tc.lockfiles.npm ? "npm ci" : "npm install";
|
|
3367
|
+
return tc.lockfiles.npm ? orFallback("npm ci", "npm install") : "npm install";
|
|
3077
3368
|
}
|
|
3078
3369
|
}
|
|
3079
3370
|
function cacheMountFor(pm, hostPath) {
|
|
@@ -3157,7 +3448,7 @@ function readRepoToolchain(repoPath) {
|
|
|
3157
3448
|
}
|
|
3158
3449
|
};
|
|
3159
3450
|
}
|
|
3160
|
-
function createStreamThrottle(
|
|
3451
|
+
function createStreamThrottle(emit3, opts = {}) {
|
|
3161
3452
|
const intervalMs = opts.intervalMs ?? 2000;
|
|
3162
3453
|
const maxChars = opts.maxChars ?? 500;
|
|
3163
3454
|
const now = opts.now ?? Date.now;
|
|
@@ -3167,7 +3458,7 @@ function createStreamThrottle(emit2, opts = {}) {
|
|
|
3167
3458
|
const flushText = () => {
|
|
3168
3459
|
if (buffer.length === 0)
|
|
3169
3460
|
return;
|
|
3170
|
-
|
|
3461
|
+
emit3({ type: "burn.text", message: buffer, data: { iteration: lastIteration } });
|
|
3171
3462
|
buffer = "";
|
|
3172
3463
|
lastFlush = now();
|
|
3173
3464
|
};
|
|
@@ -3182,7 +3473,7 @@ function createStreamThrottle(emit2, opts = {}) {
|
|
|
3182
3473
|
flushText();
|
|
3183
3474
|
const rawArgs = event.formattedArgs ?? "";
|
|
3184
3475
|
const args = rawArgs.length > 200 ? `${rawArgs.slice(0, 200)}\u2026` : rawArgs;
|
|
3185
|
-
|
|
3476
|
+
emit3({
|
|
3186
3477
|
type: "burn.tool",
|
|
3187
3478
|
message: args.length > 0 ? `${event.name} ${args}` : event.name,
|
|
3188
3479
|
data: { name: event.name, args: rawArgs, iteration: event.iteration }
|
|
@@ -3294,18 +3585,18 @@ function buildRetryNotes(input) {
|
|
|
3294
3585
|
`);
|
|
3295
3586
|
}
|
|
3296
3587
|
function delayUnlessAborted(ms, signal) {
|
|
3297
|
-
return new Promise((
|
|
3588
|
+
return new Promise((resolve6) => {
|
|
3298
3589
|
if (signal.aborted) {
|
|
3299
|
-
|
|
3590
|
+
resolve6();
|
|
3300
3591
|
return;
|
|
3301
3592
|
}
|
|
3302
3593
|
const onAbort = () => {
|
|
3303
3594
|
clearTimeout(t);
|
|
3304
|
-
|
|
3595
|
+
resolve6();
|
|
3305
3596
|
};
|
|
3306
3597
|
const t = setTimeout(() => {
|
|
3307
3598
|
signal.removeEventListener("abort", onAbort);
|
|
3308
|
-
|
|
3599
|
+
resolve6();
|
|
3309
3600
|
}, ms);
|
|
3310
3601
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
3311
3602
|
});
|
|
@@ -3659,7 +3950,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3659
3950
|
const runResolver = async (input) => {
|
|
3660
3951
|
const resolveBranch = ticketBranchName(feature.slug, ticket.seq, newId("r").slice(2, 10));
|
|
3661
3952
|
const otherSide = await commitSummaries(project.repoPath, input.branch, feature.branch);
|
|
3662
|
-
const prompt =
|
|
3953
|
+
const prompt = renderTemplate2(readFileSync3(resolverTemplatePath(), "utf8"), {
|
|
3663
3954
|
TICKET_JSON: ticketJson,
|
|
3664
3955
|
FEATURE_BRIEF: featureBrief,
|
|
3665
3956
|
DOCS_DIGEST: docsDigest,
|
|
@@ -4139,8 +4430,11 @@ function errorHeadline2(s) {
|
|
|
4139
4430
|
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
4140
4431
|
return causes.at(-1) ?? lines[0] ?? "";
|
|
4141
4432
|
}
|
|
4142
|
-
function createPrepStreamThrottle(
|
|
4143
|
-
return createStreamThrottle((e) =>
|
|
4433
|
+
function createPrepStreamThrottle(emit3, opts = {}) {
|
|
4434
|
+
return createStreamThrottle((e) => emit3({ ...e, type: e.type.replace(/^burn\./, "prep.") }), opts);
|
|
4435
|
+
}
|
|
4436
|
+
function nonFatalSetup(setupCommand) {
|
|
4437
|
+
return `( ${setupCommand} || true )`;
|
|
4144
4438
|
}
|
|
4145
4439
|
function prepTemplatePath() {
|
|
4146
4440
|
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
@@ -4173,10 +4467,11 @@ async function realExecutePrepRun(ctx, config2, token, model) {
|
|
|
4173
4467
|
if (setupCommand) {
|
|
4174
4468
|
ctx.emitEvent({
|
|
4175
4469
|
type: "prep.setup",
|
|
4176
|
-
message: `
|
|
4470
|
+
message: `attempting a dependency install before the agent starts (best-effort): ${setupCommand}`
|
|
4177
4471
|
});
|
|
4178
4472
|
}
|
|
4179
|
-
const
|
|
4473
|
+
const prepSetupCommand = setupCommand ? nonFatalSetup(setupCommand) : undefined;
|
|
4474
|
+
const hookCommand = workspaceMode === "isolated" ? buildIsolatedSetupCommand(branch, prepSetupCommand, pm) : prepSetupCommand;
|
|
4180
4475
|
const runOptions = {
|
|
4181
4476
|
agent: buildBurnAgent(config2, token, model),
|
|
4182
4477
|
sandbox: selectSandbox(config2, mounts),
|
|
@@ -4276,6 +4571,9 @@ var init_project_prep = __esm(() => {
|
|
|
4276
4571
|
verifyCommands: "**`verifyCommands`** \u2014 the exact typecheck / test / lint commands for this repo, one per line. In a monorepo these must include the correct workspace filter names. Run each one and confirm it executes the thing it claims to.",
|
|
4277
4572
|
knownFailures: '**`knownFailures`** \u2014 which tests are ALREADY failing on this checkout, before anything is changed. Run the full suite once and report the count plus the failing suite names. "0 known failures" is a real, useful answer.',
|
|
4278
4573
|
devCommand: "**`devCommand`** \u2014 the command that starts this project's dev server on a developer's own machine. Read it from config; do not run it here.",
|
|
4574
|
+
driveSetupCommand: "**`driveSetupCommand`** \u2014 a single shell command that takes this repo from a fresh checkout to \"the dev server would actually work\": backing services up, schema applied, whatever this project needs. Chain steps with `&&`. Read it from the repo's own config (compose file, Makefile, package scripts, README); do not run it here. Omit it if the dev command alone is enough \u2014 an empty answer is better than a guessed one, because this runs on a developer's real machine.",
|
|
4575
|
+
driveStopCommand: "**`driveStopCommand`** \u2014 the matching teardown for `driveSetupCommand`, if this project has one (`docker compose down`, a stop script). Read it from config; do not run it here. Omit it if setup leaves nothing that needs stopping.",
|
|
4576
|
+
driveEnv: "**`driveEnv`** \u2014 `KEY=VALUE` lines (one per line) overlaid on the dev server's environment during a test drive, with `{{slug}}`, `{{branch}}` and `{{id}}` (an identifier-safe slug) rendered per drive. Its purpose is giving a branch its own database, e.g. `DATABASE_URL=postgres://localhost:5432/myapp_{{id}}` paired with a `driveSetupCommand` that creates it. Report it ONLY if you can read the real connection variable's name and shape out of the repo's own config or `.env.example`; a connection string you assembled from defaults points at the wrong database convincingly, which is worse than omitting the key. Omit it if the project has no database or you had to guess the host, port or credentials.",
|
|
4279
4577
|
dbResetCommand: "**`dbResetCommand`** \u2014 the command that rebuilds the dev database from the migrations currently in the working tree. Read it from config; do not run it here. Omit it entirely if this repo has no database."
|
|
4280
4578
|
};
|
|
4281
4579
|
});
|
|
@@ -4382,6 +4680,9 @@ var init_findings = __esm(() => {
|
|
|
4382
4680
|
verifyCommands: projects.verifyCommands,
|
|
4383
4681
|
knownFailures: projects.knownFailures,
|
|
4384
4682
|
devCommand: projects.devCommand,
|
|
4683
|
+
driveSetupCommand: projects.driveSetupCommand,
|
|
4684
|
+
driveStopCommand: projects.driveStopCommand,
|
|
4685
|
+
driveEnv: projects.driveEnv,
|
|
4385
4686
|
dbResetCommand: projects.dbResetCommand
|
|
4386
4687
|
};
|
|
4387
4688
|
COLUMN_NAME = {
|
|
@@ -4389,13 +4690,16 @@ var init_findings = __esm(() => {
|
|
|
4389
4690
|
verifyCommands: "verifyCommands",
|
|
4390
4691
|
knownFailures: "knownFailures",
|
|
4391
4692
|
devCommand: "devCommand",
|
|
4693
|
+
driveSetupCommand: "driveSetupCommand",
|
|
4694
|
+
driveStopCommand: "driveStopCommand",
|
|
4695
|
+
driveEnv: "driveEnv",
|
|
4392
4696
|
dbResetCommand: "dbResetCommand"
|
|
4393
4697
|
};
|
|
4394
4698
|
PREPARED_SET = new Set(PREPARED_KEYS);
|
|
4395
4699
|
});
|
|
4396
4700
|
|
|
4397
4701
|
// src/services/prep.ts
|
|
4398
|
-
import { desc as desc3, eq as eq6 } from "drizzle-orm";
|
|
4702
|
+
import { and as and6, desc as desc3, eq as eq6 } from "drizzle-orm";
|
|
4399
4703
|
function isPreparing(projectId) {
|
|
4400
4704
|
return controllers.has(projectId);
|
|
4401
4705
|
}
|
|
@@ -4404,6 +4708,13 @@ function keysToPrepare(ctx, project, opts = {}) {
|
|
|
4404
4708
|
const candidates = opts.keys ?? (opts.refresh ? PREPARED_KEYS : [...unset]);
|
|
4405
4709
|
return candidates.filter((key) => (opts.refresh || unset.has(key)) && isOverwritable(ctx, project.id, key));
|
|
4406
4710
|
}
|
|
4711
|
+
function shouldAutoPrepare(ctx, project) {
|
|
4712
|
+
if (!ctx.config.autoPrepare)
|
|
4713
|
+
return false;
|
|
4714
|
+
if (latestPrep(ctx, project.id))
|
|
4715
|
+
return false;
|
|
4716
|
+
return keysToPrepare(ctx, project).length > 0;
|
|
4717
|
+
}
|
|
4407
4718
|
async function startPrep(ctx, projectId, opts = {}) {
|
|
4408
4719
|
const project = requireProjectById(ctx, projectId);
|
|
4409
4720
|
if (controllers.has(projectId)) {
|
|
@@ -4523,6 +4834,11 @@ function latestPrep(ctx, projectId) {
|
|
|
4523
4834
|
...row.headSha !== null ? { headSha: row.headSha } : {}
|
|
4524
4835
|
};
|
|
4525
4836
|
}
|
|
4837
|
+
function latestPrepNotes(ctx, projectId) {
|
|
4838
|
+
const row = ctx.db.select({ data: events.data }).from(events).where(and6(eq6(events.projectId, projectId), eq6(events.type, "prep.notes"))).orderBy(desc3(events.id)).get();
|
|
4839
|
+
const notes = row?.data?.notes;
|
|
4840
|
+
return typeof notes === "string" && notes.trim() !== "" ? notes : undefined;
|
|
4841
|
+
}
|
|
4526
4842
|
function reconcilePreps(ctx) {
|
|
4527
4843
|
const stale = ctx.db.select({ id: projectPreps.id, projectId: projectPreps.projectId }).from(projectPreps).where(eq6(projectPreps.status, "running")).all().filter((row) => !controllers.has(row.projectId));
|
|
4528
4844
|
for (const row of stale) {
|
|
@@ -4577,7 +4893,7 @@ var init_feature_docs = __esm(() => {
|
|
|
4577
4893
|
});
|
|
4578
4894
|
|
|
4579
4895
|
// src/services/tickets.ts
|
|
4580
|
-
import { and as
|
|
4896
|
+
import { and as and7, asc as asc3, eq as eq7 } from "drizzle-orm";
|
|
4581
4897
|
function rowToTicket(row) {
|
|
4582
4898
|
return {
|
|
4583
4899
|
id: row.id,
|
|
@@ -4688,7 +5004,7 @@ function cancelTicket(ctx, id, reason) {
|
|
|
4688
5004
|
return getTicket(ctx, id);
|
|
4689
5005
|
}
|
|
4690
5006
|
function sweepOrphanedBurning(ctx, featureId, reason) {
|
|
4691
|
-
const orphaned = ctx.db.select().from(tickets).where(
|
|
5007
|
+
const orphaned = ctx.db.select().from(tickets).where(and7(eq7(tickets.featureId, featureId), eq7(tickets.status, "burning"))).all().map(rowToTicket);
|
|
4692
5008
|
for (const t of orphaned) {
|
|
4693
5009
|
updateTicket(ctx, t.id, { status: "failed", error: reason });
|
|
4694
5010
|
emit(ctx, featureId, {
|
|
@@ -4811,7 +5127,7 @@ var init_gates = __esm(() => {
|
|
|
4811
5127
|
|
|
4812
5128
|
// src/services/knowledge.ts
|
|
4813
5129
|
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, writeFileSync } from "fs";
|
|
4814
|
-
import { join as join10, relative, resolve as
|
|
5130
|
+
import { join as join10, relative, resolve as resolve6, sep } from "path";
|
|
4815
5131
|
function scaffoldDocs(ctx, feature) {
|
|
4816
5132
|
const project = projectForFeature(ctx, feature);
|
|
4817
5133
|
const dir = featureDocsDir(project, feature);
|
|
@@ -4871,8 +5187,8 @@ function listDocs(ctx, feature) {
|
|
|
4871
5187
|
function readDoc(ctx, feature, relPath) {
|
|
4872
5188
|
const project = projectForFeature(ctx, feature);
|
|
4873
5189
|
const dir = featureDocsDir(project, feature);
|
|
4874
|
-
const root =
|
|
4875
|
-
const target =
|
|
5190
|
+
const root = resolve6(dir);
|
|
5191
|
+
const target = resolve6(dir, relPath);
|
|
4876
5192
|
const rel = relative(root, target);
|
|
4877
5193
|
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
4878
5194
|
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
@@ -4913,7 +5229,7 @@ function endSession(ctx, sessionId) {
|
|
|
4913
5229
|
const session = markSessionEnded(ctx, sessionId);
|
|
4914
5230
|
if (session) {
|
|
4915
5231
|
releaseForSession(ctx, sessionId);
|
|
4916
|
-
|
|
5232
|
+
emitForSession(ctx, session, {
|
|
4917
5233
|
type: "session.ended",
|
|
4918
5234
|
message: "session ended by user",
|
|
4919
5235
|
data: { sessionId, killed }
|
|
@@ -4985,8 +5301,8 @@ function errorHeadline3(s) {
|
|
|
4985
5301
|
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
4986
5302
|
return causes.at(-1) ?? lines[0] ?? "";
|
|
4987
5303
|
}
|
|
4988
|
-
function createResearchStreamThrottle(
|
|
4989
|
-
return createStreamThrottle((e) =>
|
|
5304
|
+
function createResearchStreamThrottle(emit3, opts = {}) {
|
|
5305
|
+
return createStreamThrottle((e) => emit3({ ...e, type: e.type.replace(/^burn\./, "research.") }), opts);
|
|
4990
5306
|
}
|
|
4991
5307
|
function renderResearchPrompt(template, values) {
|
|
4992
5308
|
let out = template;
|
|
@@ -5194,7 +5510,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
5194
5510
|
input: opts.input,
|
|
5195
5511
|
modelOverride: opts.modelOverride,
|
|
5196
5512
|
resolveWaypoint: (id, disposition, summary) => {
|
|
5197
|
-
|
|
5513
|
+
resolve3(ctx, id, disposition, summary);
|
|
5198
5514
|
},
|
|
5199
5515
|
signal: controller.signal
|
|
5200
5516
|
};
|
|
@@ -5619,8 +5935,14 @@ function resolveSession(ctx, sessionId) {
|
|
|
5619
5935
|
}
|
|
5620
5936
|
return mostRecentLiveSession(ctx);
|
|
5621
5937
|
}
|
|
5938
|
+
function requireFeatureId(session) {
|
|
5939
|
+
if (!session.featureId) {
|
|
5940
|
+
throw new GateError(`this tool needs a feature, and a ${session.kind} session is project-scoped. ` + "Use record_finding to store what you establish about the project.");
|
|
5941
|
+
}
|
|
5942
|
+
return session.featureId;
|
|
5943
|
+
}
|
|
5622
5944
|
function toolGetFeatureContext(ctx, session) {
|
|
5623
|
-
const feature = getFeatureRow(ctx, session
|
|
5945
|
+
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
5624
5946
|
const docs = listDocs(ctx, feature).map((d) => {
|
|
5625
5947
|
try {
|
|
5626
5948
|
return { relPath: d.relPath, content: readDoc(ctx, feature, d.relPath).content };
|
|
@@ -5642,17 +5964,17 @@ function toolGetFeatureContext(ctx, session) {
|
|
|
5642
5964
|
return context;
|
|
5643
5965
|
}
|
|
5644
5966
|
function toolResolveWaypoint(ctx, _session, input) {
|
|
5645
|
-
|
|
5967
|
+
resolve3(ctx, input.id, input.disposition, input.summary);
|
|
5646
5968
|
return { ok: true };
|
|
5647
5969
|
}
|
|
5648
5970
|
function toolEmitTickets(ctx, session, input) {
|
|
5649
|
-
const feature = getFeatureRow(ctx, session
|
|
5971
|
+
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
5650
5972
|
const stored = storeTickets(ctx, feature.id, input.tickets);
|
|
5651
5973
|
return { stored: stored.length, ids: stored.map((t) => t.id) };
|
|
5652
5974
|
}
|
|
5653
5975
|
function requireOwnTicket(ctx, session, ticketId) {
|
|
5654
5976
|
const ticket = getTicket(ctx, ticketId);
|
|
5655
|
-
if (ticket.featureId !== session
|
|
5977
|
+
if (ticket.featureId !== requireFeatureId(session)) {
|
|
5656
5978
|
throw new GateError(`ticket ${ticketId} does not belong to this session's feature`);
|
|
5657
5979
|
}
|
|
5658
5980
|
return ticket;
|
|
@@ -5667,10 +5989,10 @@ function toolCancelTicket(ctx, session, input) {
|
|
|
5667
5989
|
return { ok: true, ticket: cancelTicket(ctx, input.id, input.reason) };
|
|
5668
5990
|
}
|
|
5669
5991
|
function toolEscalateToMap(ctx, session, input) {
|
|
5670
|
-
return escalateToMap(ctx, session
|
|
5992
|
+
return escalateToMap(ctx, requireFeatureId(session), input);
|
|
5671
5993
|
}
|
|
5672
5994
|
function toolEmitWaypoints(ctx, session, input) {
|
|
5673
|
-
const feature = getFeatureRow(ctx, session
|
|
5995
|
+
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
5674
5996
|
if (!feature.mapped) {
|
|
5675
5997
|
throw new GateError("feature is not mapped \u2014 call escalate_to_map before emitting waypoints");
|
|
5676
5998
|
}
|
|
@@ -5678,7 +6000,7 @@ function toolEmitWaypoints(ctx, session, input) {
|
|
|
5678
6000
|
return { stored: stored.length, ids: stored.map((w) => w.id) };
|
|
5679
6001
|
}
|
|
5680
6002
|
function toolRecordEvent(ctx, session, input) {
|
|
5681
|
-
emit(ctx, session
|
|
6003
|
+
emit(ctx, requireFeatureId(session), {
|
|
5682
6004
|
type: input.type,
|
|
5683
6005
|
message: input.message,
|
|
5684
6006
|
data: { source: "mcp" }
|
|
@@ -5686,7 +6008,7 @@ function toolRecordEvent(ctx, session, input) {
|
|
|
5686
6008
|
return { ok: true };
|
|
5687
6009
|
}
|
|
5688
6010
|
function toolCompletePhase(ctx, session, input) {
|
|
5689
|
-
const feature = getFeatureRow(ctx, session
|
|
6011
|
+
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
5690
6012
|
emit(ctx, feature.id, {
|
|
5691
6013
|
type: "phase.complete_requested",
|
|
5692
6014
|
message: `session marked phase '${input.phase}' complete`,
|
|
@@ -5715,13 +6037,46 @@ async function commitDocsCheckpoint(ctx, session, message) {
|
|
|
5715
6037
|
try {
|
|
5716
6038
|
await commitDocs(session.worktreePath, message);
|
|
5717
6039
|
} catch (e) {
|
|
5718
|
-
emit(ctx, session
|
|
6040
|
+
emit(ctx, requireFeatureId(session), {
|
|
5719
6041
|
type: "git.commit_pending",
|
|
5720
6042
|
message: isNotImplemented(e) ? "docs checkpoint skipped (git service pending)" : `docs checkpoint failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
5721
6043
|
data: { message }
|
|
5722
6044
|
});
|
|
5723
6045
|
}
|
|
5724
6046
|
}
|
|
6047
|
+
function requireProject(ctx, session) {
|
|
6048
|
+
if (!session.projectId) {
|
|
6049
|
+
throw new GateError(`record_finding is for a project-scoped preparation session; this is a ${session.kind} session.`);
|
|
6050
|
+
}
|
|
6051
|
+
const project = getProjectById(ctx, session.projectId);
|
|
6052
|
+
if (!project)
|
|
6053
|
+
throw new GateError(`project ${session.projectId} not found`);
|
|
6054
|
+
return project;
|
|
6055
|
+
}
|
|
6056
|
+
function toolRecordFinding(ctx, session, input) {
|
|
6057
|
+
const project = requireProject(ctx, session);
|
|
6058
|
+
const source = input.userSupplied ? "human" : "session";
|
|
6059
|
+
if (!isOverwritable(ctx, project.id, input.key) && !input.userSupplied) {
|
|
6060
|
+
return {
|
|
6061
|
+
ok: true,
|
|
6062
|
+
key: input.key,
|
|
6063
|
+
source,
|
|
6064
|
+
skipped: `${input.key} was set by a human and is not auto-overwritable \u2014 ask them to clear it first if it should change`
|
|
6065
|
+
};
|
|
6066
|
+
}
|
|
6067
|
+
recordFinding(ctx, project.id, {
|
|
6068
|
+
key: input.key,
|
|
6069
|
+
value: input.value,
|
|
6070
|
+
source,
|
|
6071
|
+
...input.evidence ? { evidence: input.evidence } : {}
|
|
6072
|
+
});
|
|
6073
|
+
emitProject(ctx, project.id, {
|
|
6074
|
+
type: "prep.finding_recorded",
|
|
6075
|
+
message: `preparation session established ${input.key} (${source})`,
|
|
6076
|
+
data: { key: input.key, source, sessionId: session.id }
|
|
6077
|
+
});
|
|
6078
|
+
return { ok: true, key: input.key, source };
|
|
6079
|
+
}
|
|
5725
6080
|
function ok(result) {
|
|
5726
6081
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
5727
6082
|
}
|
|
@@ -5743,6 +6098,21 @@ async function resolveCtxSession(extra) {
|
|
|
5743
6098
|
}
|
|
5744
6099
|
function buildMcpServer() {
|
|
5745
6100
|
const server = new McpServer({ name: "runcastle", version: "0.1.0" });
|
|
6101
|
+
server.registerTool("record_finding", {
|
|
6102
|
+
title: "Record a preparation finding",
|
|
6103
|
+
description: "Store one established project setting from a preparation conversation. " + "Set userSupplied: true ONLY when the human gave you this value or confirmed it verbatim \u2014 " + "that marks it as theirs and stops any future automatic run from overwriting it. " + "Anything you worked out yourself must leave it false, even if they were watching. " + "Always include evidence: what you ran, or what the human told you.",
|
|
6104
|
+
inputSchema: {
|
|
6105
|
+
key: PreparedKey,
|
|
6106
|
+
value: z4.string(),
|
|
6107
|
+
evidence: z4.string().optional(),
|
|
6108
|
+
userSupplied: z4.boolean().optional()
|
|
6109
|
+
}
|
|
6110
|
+
}, async (args, extra) => {
|
|
6111
|
+
const rs = await resolveCtxSession(extra);
|
|
6112
|
+
if (!rs)
|
|
6113
|
+
return noSession();
|
|
6114
|
+
return ok(toolRecordFinding(rs.ctx, rs.session, args));
|
|
6115
|
+
});
|
|
5746
6116
|
server.registerTool("get_feature_context", {
|
|
5747
6117
|
title: "Get feature context",
|
|
5748
6118
|
description: "Full context for the current feature: the feature row, its phase, all docs/features/<slug>/*.md contents, and its tickets.",
|
|
@@ -5865,6 +6235,7 @@ var init_server = __esm(() => {
|
|
|
5865
6235
|
init_sessions();
|
|
5866
6236
|
init_features();
|
|
5867
6237
|
init_events();
|
|
6238
|
+
init_findings();
|
|
5868
6239
|
init_git();
|
|
5869
6240
|
init_knowledge();
|
|
5870
6241
|
init_repo();
|
|
@@ -5975,6 +6346,57 @@ function handleSessionStart(ctx, sessionId, feature, payload) {
|
|
|
5975
6346
|
}
|
|
5976
6347
|
};
|
|
5977
6348
|
}
|
|
6349
|
+
function handlePrepareSessionStart(ctx, session, payload) {
|
|
6350
|
+
const ccSessionId = typeof payload?.session_id === "string" ? payload.session_id : undefined;
|
|
6351
|
+
const transcriptPath = typeof payload?.transcript_path === "string" ? payload.transcript_path : undefined;
|
|
6352
|
+
const source = typeof payload?.source === "string" ? payload.source : undefined;
|
|
6353
|
+
const wasLive = getSessionRow(ctx, session.id)?.status === "live";
|
|
6354
|
+
markSessionLive(ctx, session.id, { ccSessionId, transcriptPath });
|
|
6355
|
+
if (!wasLive) {
|
|
6356
|
+
emitForSession(ctx, session, {
|
|
6357
|
+
type: "session.started",
|
|
6358
|
+
message: source === "resume" ? "preparation session live (resumed)" : "preparation session live",
|
|
6359
|
+
data: { sessionId: session.id, ccSessionId, transcriptPath, source: source ?? null }
|
|
6360
|
+
});
|
|
6361
|
+
}
|
|
6362
|
+
return {
|
|
6363
|
+
hookSpecificOutput: {
|
|
6364
|
+
hookEventName: "SessionStart",
|
|
6365
|
+
additionalContext: prepareStartContext(ctx, session)
|
|
6366
|
+
}
|
|
6367
|
+
};
|
|
6368
|
+
}
|
|
6369
|
+
function handlePrepareUserPrompt(ctx, session, payload) {
|
|
6370
|
+
const prompt = typeof payload?.prompt === "string" ? payload.prompt : undefined;
|
|
6371
|
+
noteKickoffPrompt(ctx, session.id, prompt);
|
|
6372
|
+
return {
|
|
6373
|
+
hookSpecificOutput: {
|
|
6374
|
+
hookEventName: "UserPromptSubmit",
|
|
6375
|
+
additionalContext: `[runcastle] preparation session (project-scoped, no feature)`
|
|
6376
|
+
}
|
|
6377
|
+
};
|
|
6378
|
+
}
|
|
6379
|
+
function handlePrepareSessionEnd(ctx, session) {
|
|
6380
|
+
markSessionEnded(ctx, session.id);
|
|
6381
|
+
emitForSession(ctx, session, {
|
|
6382
|
+
type: "session.ended",
|
|
6383
|
+
message: "preparation session ended",
|
|
6384
|
+
data: { sessionId: session.id }
|
|
6385
|
+
});
|
|
6386
|
+
return {};
|
|
6387
|
+
}
|
|
6388
|
+
function prepareStartContext(ctx, session) {
|
|
6389
|
+
const project = session.projectId ? getProjectById(ctx, session.projectId) : null;
|
|
6390
|
+
if (!project)
|
|
6391
|
+
return "[runcastle] preparation session \u2014 project not found";
|
|
6392
|
+
const remaining = keysToPrepare(ctx, project);
|
|
6393
|
+
return [
|
|
6394
|
+
`[runcastle] preparation session for ${project.name} (${project.repoPath}).`,
|
|
6395
|
+
remaining.length > 0 ? `Still unestablished: ${remaining.join(", ")}.` : "Every prepared field currently has a value.",
|
|
6396
|
+
"Record what you establish with the `record_finding` MCP tool \u2014 a value the user",
|
|
6397
|
+
"gives you or confirms verbatim is theirs (userSupplied: true), anything you measured is not."
|
|
6398
|
+
].join(" ");
|
|
6399
|
+
}
|
|
5978
6400
|
function handleUserPrompt(ctx, sessionId, feature, payload) {
|
|
5979
6401
|
const prompt = typeof payload?.prompt === "string" ? payload.prompt : undefined;
|
|
5980
6402
|
noteKickoffPrompt(ctx, sessionId, prompt);
|
|
@@ -6016,6 +6438,7 @@ var hooks, hooks_default;
|
|
|
6016
6438
|
var init_hooks = __esm(() => {
|
|
6017
6439
|
init_sessions();
|
|
6018
6440
|
init_events();
|
|
6441
|
+
init_prep();
|
|
6019
6442
|
init_repo();
|
|
6020
6443
|
init_tickets();
|
|
6021
6444
|
init_waypoints();
|
|
@@ -6037,6 +6460,18 @@ var init_hooks = __esm(() => {
|
|
|
6037
6460
|
const session = getSessionRow(ctx, sessionId);
|
|
6038
6461
|
if (!session)
|
|
6039
6462
|
return c.json({});
|
|
6463
|
+
if (!session.featureId) {
|
|
6464
|
+
switch (event) {
|
|
6465
|
+
case "session-start":
|
|
6466
|
+
return c.json(handlePrepareSessionStart(ctx, session, body.payload));
|
|
6467
|
+
case "user-prompt":
|
|
6468
|
+
return c.json(handlePrepareUserPrompt(ctx, session, body.payload));
|
|
6469
|
+
case "session-end":
|
|
6470
|
+
return c.json(handlePrepareSessionEnd(ctx, session));
|
|
6471
|
+
default:
|
|
6472
|
+
return c.json({});
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6040
6475
|
const feature = tryGetFeature(ctx, session.featureId);
|
|
6041
6476
|
if (!feature)
|
|
6042
6477
|
return c.json({});
|
|
@@ -6111,7 +6546,7 @@ var init_stream = __esm(() => {
|
|
|
6111
6546
|
// src/routes/web.ts
|
|
6112
6547
|
import { existsSync as existsSync13, statSync } from "fs";
|
|
6113
6548
|
import { readFile } from "fs/promises";
|
|
6114
|
-
import { extname, join as join12, normalize, resolve as
|
|
6549
|
+
import { extname, join as join12, normalize, resolve as resolve7, sep as sep2 } from "path";
|
|
6115
6550
|
function contentType(path) {
|
|
6116
6551
|
return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
6117
6552
|
}
|
|
@@ -6137,11 +6572,11 @@ function safeJoin(root, pathname) {
|
|
|
6137
6572
|
function resolveWebDist() {
|
|
6138
6573
|
const override = process.env.RUNCASTLE_WEB_DIST;
|
|
6139
6574
|
if (override)
|
|
6140
|
-
return
|
|
6141
|
-
return
|
|
6575
|
+
return resolve7(override);
|
|
6576
|
+
return resolve7(import.meta.dirname, "..", "..", "..", "..", "apps", "web", "dist");
|
|
6142
6577
|
}
|
|
6143
6578
|
function mountWebApp(app, distDir) {
|
|
6144
|
-
const root =
|
|
6579
|
+
const root = resolve7(distDir);
|
|
6145
6580
|
const indexHtml = join12(root, "index.html");
|
|
6146
6581
|
app.get("*", async (c, next) => {
|
|
6147
6582
|
const { pathname } = new URL(c.req.url);
|
|
@@ -6432,6 +6867,69 @@ function renderRevisitPrompt(feature) {
|
|
|
6432
6867
|
].join(`
|
|
6433
6868
|
`);
|
|
6434
6869
|
}
|
|
6870
|
+
function renderPreparePrompt(brief) {
|
|
6871
|
+
const { project, remainingKeys, established, notes } = brief;
|
|
6872
|
+
return [
|
|
6873
|
+
`# runcastle \u2014 preparing ${project.name}`,
|
|
6874
|
+
"",
|
|
6875
|
+
"This is a **preparation session**: a project-scoped conversation whose job is to",
|
|
6876
|
+
"establish the settings every later agent depends on, and to record them with evidence.",
|
|
6877
|
+
"There is no feature here and no pipeline to advance.",
|
|
6878
|
+
"",
|
|
6879
|
+
"## Where this runs \u2014 and why that matters",
|
|
6880
|
+
`You are on the developer's own machine, in \`${project.repoPath}\`, NOT in a sandbox.`,
|
|
6881
|
+
"A headless preparation run measures the repo in a throwaway container, which means it",
|
|
6882
|
+
"can never verify anything about *this* machine: the dev server, the local database,",
|
|
6883
|
+
"docker, credentials. Those keys \u2014 `devCommand`, `driveSetupCommand`, `driveStopCommand`,",
|
|
6884
|
+
"`driveEnv`, `dbResetCommand` \u2014 are exactly what it has to leave as proposals.",
|
|
6885
|
+
"You can actually run them. That is the point of this session.",
|
|
6886
|
+
"",
|
|
6887
|
+
"The same access is why you must **ask before you act**. Anything that starts or stops a",
|
|
6888
|
+
"service, creates or migrates a database, or writes outside the repo needs the human to",
|
|
6889
|
+
"agree first \u2014 say what you are about to run and why, then wait.",
|
|
6890
|
+
"",
|
|
6891
|
+
...established.length > 0 ? [
|
|
6892
|
+
"## Already established \u2014 do not re-derive",
|
|
6893
|
+
"These are recorded findings. Treat them as true unless the human says otherwise;",
|
|
6894
|
+
"replacing a measured value with a fresh guess makes preparation worse, not better.",
|
|
6895
|
+
"",
|
|
6896
|
+
...established.map((f) => `- \`${f.key}\` (${f.source})${f.evidence ? ` \u2014 ${f.evidence}` : ""}`),
|
|
6897
|
+
""
|
|
6898
|
+
] : [],
|
|
6899
|
+
...notes ? [
|
|
6900
|
+
"## What the last run could not settle",
|
|
6901
|
+
"Its own words. Where it says it declined to guess, it was right to \u2014 that is the",
|
|
6902
|
+
"gap you are here to close, and closing it usually means asking one direct question.",
|
|
6903
|
+
"",
|
|
6904
|
+
notes,
|
|
6905
|
+
""
|
|
6906
|
+
] : [],
|
|
6907
|
+
"## Still open",
|
|
6908
|
+
remainingKeys.length > 0 ? remainingKeys.map((k) => `- \`${k}\``).join(`
|
|
6909
|
+
`) : "_Nothing is unset. Confirm the existing values still hold, then say so and stop._",
|
|
6910
|
+
"",
|
|
6911
|
+
"## Recording what you establish",
|
|
6912
|
+
"- `record_finding({ key, value, evidence, userSupplied })` \u2014 one call per key.",
|
|
6913
|
+
"- `userSupplied: true` means the human GAVE you this value or confirmed it verbatim.",
|
|
6914
|
+
" That marks it as theirs and permanently stops automatic runs from overwriting it.",
|
|
6915
|
+
"- Leave it false for anything you worked out yourself, even with them watching \u2014",
|
|
6916
|
+
" that stays improvable by a later run. Getting this backwards silently retires a",
|
|
6917
|
+
" field from preparation forever, and the only way back is the human clearing it.",
|
|
6918
|
+
"- `evidence` is not optional in spirit: record what you ran and what it printed, or",
|
|
6919
|
+
" what the human told you. A value with no account of itself is a guess with a source field.",
|
|
6920
|
+
"",
|
|
6921
|
+
"## Secrets",
|
|
6922
|
+
"This is a development environment and the human has agreed to supply real connection",
|
|
6923
|
+
"strings and credentials here. Store them as given. Do not paste a secret into a timeline",
|
|
6924
|
+
"note or a commit message \u2014 `record_finding` is the only place a value belongs.",
|
|
6925
|
+
"",
|
|
6926
|
+
"## Your task",
|
|
6927
|
+
"Open by telling the human which fields are still open and what you need from them for",
|
|
6928
|
+
"each. Work them one at a time: propose, ask, run it if they agree, then record it.",
|
|
6929
|
+
""
|
|
6930
|
+
].join(`
|
|
6931
|
+
`);
|
|
6932
|
+
}
|
|
6435
6933
|
function renderSettings(hookClient) {
|
|
6436
6934
|
const cmd = (event) => ({
|
|
6437
6935
|
type: "command",
|
|
@@ -6462,13 +6960,16 @@ function renderMcpConfig(session, config2) {
|
|
|
6462
6960
|
};
|
|
6463
6961
|
}
|
|
6464
6962
|
async function writeSessionArtifacts(input) {
|
|
6465
|
-
const { session, feature, config: config2, waypoint } = input;
|
|
6963
|
+
const { session, feature, config: config2, waypoint, prepare } = input;
|
|
6466
6964
|
const dir = sessionDir(session.id);
|
|
6467
6965
|
mkdirSync7(dir, { recursive: true });
|
|
6468
6966
|
const systemPromptPath = join13(dir, "system-prompt.md");
|
|
6469
6967
|
const settingsPath = join13(dir, "settings.json");
|
|
6470
6968
|
const mcpConfigPath = join13(dir, "mcp.json");
|
|
6471
|
-
|
|
6969
|
+
const systemPrompt = feature ? renderSystemPrompt(feature, session.kind, waypoint) : prepare ? renderPreparePrompt(prepare) : (() => {
|
|
6970
|
+
throw new Error(`session ${session.id} has neither a feature nor a preparation brief`);
|
|
6971
|
+
})();
|
|
6972
|
+
writeFileSync2(systemPromptPath, systemPrompt, "utf8");
|
|
6472
6973
|
writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath()), null, 2), "utf8");
|
|
6473
6974
|
writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
|
|
6474
6975
|
return { systemPromptPath, settingsPath, mcpConfigPath };
|
|
@@ -6486,7 +6987,8 @@ var init_artifacts = __esm(() => {
|
|
|
6486
6987
|
"mcp__runcastle__complete_phase",
|
|
6487
6988
|
"mcp__runcastle__escalate_to_map",
|
|
6488
6989
|
"mcp__runcastle__emit_waypoints",
|
|
6489
|
-
"mcp__runcastle__resolve_waypoint"
|
|
6990
|
+
"mcp__runcastle__resolve_waypoint",
|
|
6991
|
+
"mcp__runcastle__record_finding"
|
|
6490
6992
|
];
|
|
6491
6993
|
SESSION_BASH_ALLOW_RULES = [
|
|
6492
6994
|
"Bash(git status:*)",
|
|
@@ -6503,9 +7005,9 @@ var init_artifacts = __esm(() => {
|
|
|
6503
7005
|
|
|
6504
7006
|
// src/launcher/launcher.ts
|
|
6505
7007
|
import { existsSync as existsSync14 } from "fs";
|
|
6506
|
-
import { dirname as dirname6, join as join14, resolve as
|
|
7008
|
+
import { dirname as dirname6, join as join14, resolve as resolve8 } from "path";
|
|
6507
7009
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
6508
|
-
import { and as
|
|
7010
|
+
import { and as and8, eq as eq10 } from "drizzle-orm";
|
|
6509
7011
|
function ptyExitMessage(exitCode) {
|
|
6510
7012
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
6511
7013
|
return `terminal exited (code ${label})`;
|
|
@@ -6519,7 +7021,7 @@ function buildClaudeArgs(input) {
|
|
|
6519
7021
|
input.settingsPath,
|
|
6520
7022
|
"--mcp-config",
|
|
6521
7023
|
input.mcpConfigPath,
|
|
6522
|
-
"--strict-mcp-config",
|
|
7024
|
+
...input.strictMcp ? ["--strict-mcp-config"] : [],
|
|
6523
7025
|
"--plugin-dir",
|
|
6524
7026
|
input.pluginDir,
|
|
6525
7027
|
"--append-system-prompt-file",
|
|
@@ -6540,7 +7042,7 @@ function resolvePluginDir(fromDir = dirname6(fileURLToPath6(import.meta.url))) {
|
|
|
6540
7042
|
const rel = join14("packages", "skills", "packs", "runcastle");
|
|
6541
7043
|
const override = process.env[SKILLS_DIR_ENV];
|
|
6542
7044
|
if (override) {
|
|
6543
|
-
const dir2 = join14(
|
|
7045
|
+
const dir2 = join14(resolve8(override), "packs", "runcastle");
|
|
6544
7046
|
if (existsSync14(dir2))
|
|
6545
7047
|
return dir2;
|
|
6546
7048
|
throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
|
|
@@ -6562,7 +7064,7 @@ function resolvePluginDir(fromDir = dirname6(fileURLToPath6(import.meta.url))) {
|
|
|
6562
7064
|
`)}`);
|
|
6563
7065
|
}
|
|
6564
7066
|
function activeRunFor(ctx, featureId) {
|
|
6565
|
-
const row = ctx.db.select().from(runs).where(
|
|
7067
|
+
const row = ctx.db.select().from(runs).where(and8(eq10(runs.featureId, featureId), eq10(runs.status, "running"))).limit(1).get();
|
|
6566
7068
|
return row ? rowToRun(row) : null;
|
|
6567
7069
|
}
|
|
6568
7070
|
function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
@@ -6575,6 +7077,29 @@ function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
|
6575
7077
|
throw new GateError(`a ${running.workflow} run is in progress on ${feature.slug} \u2014 it holds the feature branch; terminals are available when it finishes`);
|
|
6576
7078
|
}
|
|
6577
7079
|
}
|
|
7080
|
+
function sessionFinished(ctx, feature, session) {
|
|
7081
|
+
if (session.kind !== "waypoint")
|
|
7082
|
+
return feature.mapped;
|
|
7083
|
+
const own = listByFeature(ctx, feature.id).find((w) => w.lastSessionId === session.id);
|
|
7084
|
+
return !!own && (own.status === "resolved" || own.status === "dropped");
|
|
7085
|
+
}
|
|
7086
|
+
function sweepActiveSessions(ctx, feature, endLive) {
|
|
7087
|
+
for (const session of activeSessionsForFeature(ctx, feature.id)) {
|
|
7088
|
+
const finished = sessionFinished(ctx, feature, session);
|
|
7089
|
+
if (!finished && !endLive)
|
|
7090
|
+
continue;
|
|
7091
|
+
endSession(ctx, session.id);
|
|
7092
|
+
emit(ctx, feature.id, {
|
|
7093
|
+
type: "session.auto_ended",
|
|
7094
|
+
message: finished ? `ended the finished ${session.kind} session to work the next waypoint` : `abandoned the in-flight ${session.kind} session to work the next waypoint`,
|
|
7095
|
+
data: {
|
|
7096
|
+
sessionId: session.id,
|
|
7097
|
+
kind: session.kind,
|
|
7098
|
+
reason: finished ? "finished" : "abandoned"
|
|
7099
|
+
}
|
|
7100
|
+
});
|
|
7101
|
+
}
|
|
7102
|
+
}
|
|
6578
7103
|
async function ensureWorktree(ctx, project, feature) {
|
|
6579
7104
|
try {
|
|
6580
7105
|
return await ensureTalkWorktree(project, feature);
|
|
@@ -6682,7 +7207,8 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
6682
7207
|
mcpConfigPath: artifacts.mcpConfigPath,
|
|
6683
7208
|
systemPromptPath: artifacts.systemPromptPath,
|
|
6684
7209
|
model: resolveModel(input.kind, ctx.config, project),
|
|
6685
|
-
resumeSessionId
|
|
7210
|
+
resumeSessionId,
|
|
7211
|
+
strictMcp: ctx.config.sessionMcp === "runcastleOnly"
|
|
6686
7212
|
};
|
|
6687
7213
|
if (opts.spawn === false) {
|
|
6688
7214
|
emit(ctx, feature.id, {
|
|
@@ -6702,6 +7228,75 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
6702
7228
|
});
|
|
6703
7229
|
return { sessionId: session.id };
|
|
6704
7230
|
}
|
|
7231
|
+
async function launchPrepareSession(ctx, input, opts = {}) {
|
|
7232
|
+
const project = requireProjectById(ctx, input.projectId);
|
|
7233
|
+
const session = createSessionRow(ctx, {
|
|
7234
|
+
projectId: project.id,
|
|
7235
|
+
kind: "prepare",
|
|
7236
|
+
worktreePath: project.repoPath
|
|
7237
|
+
});
|
|
7238
|
+
const resumeSessionId = mostRecentResumableProjectSession(ctx, project.id, "prepare")?.ccSessionId;
|
|
7239
|
+
emitProject(ctx, project.id, {
|
|
7240
|
+
type: "session.launching",
|
|
7241
|
+
message: "launching preparation session",
|
|
7242
|
+
data: { sessionId: session.id, kind: "prepare", worktreePath: project.repoPath }
|
|
7243
|
+
});
|
|
7244
|
+
if (resumeSessionId) {
|
|
7245
|
+
emitProject(ctx, project.id, {
|
|
7246
|
+
type: "session.resumed",
|
|
7247
|
+
message: "resuming the previous preparation conversation",
|
|
7248
|
+
data: { sessionId: session.id, resumeSessionId }
|
|
7249
|
+
});
|
|
7250
|
+
}
|
|
7251
|
+
const artifacts = await writeSessionArtifacts({
|
|
7252
|
+
session,
|
|
7253
|
+
project,
|
|
7254
|
+
config: ctx.config,
|
|
7255
|
+
prepare: await buildPrepareBrief(ctx, project)
|
|
7256
|
+
});
|
|
7257
|
+
const serverUrl = serverUrlFor(ctx.config);
|
|
7258
|
+
const buildInput = {
|
|
7259
|
+
sessionId: session.id,
|
|
7260
|
+
serverUrl,
|
|
7261
|
+
featureTitle: `preparing ${project.name}`,
|
|
7262
|
+
worktreePath: project.repoPath,
|
|
7263
|
+
pluginDir: resolvePluginDir(),
|
|
7264
|
+
settingsPath: artifacts.settingsPath,
|
|
7265
|
+
mcpConfigPath: artifacts.mcpConfigPath,
|
|
7266
|
+
systemPromptPath: artifacts.systemPromptPath,
|
|
7267
|
+
model: resolveModel("prepare", ctx.config, project),
|
|
7268
|
+
resumeSessionId,
|
|
7269
|
+
strictMcp: ctx.config.sessionMcp === "runcastleOnly"
|
|
7270
|
+
};
|
|
7271
|
+
if (opts.spawn === false) {
|
|
7272
|
+
emitProject(ctx, project.id, {
|
|
7273
|
+
type: "session.launched",
|
|
7274
|
+
message: "preparation session prepared (terminal spawn skipped)",
|
|
7275
|
+
data: {
|
|
7276
|
+
sessionId: session.id,
|
|
7277
|
+
command: ["claude", ...buildClaudeArgs(buildInput)].join(" "),
|
|
7278
|
+
spawned: false
|
|
7279
|
+
}
|
|
7280
|
+
});
|
|
7281
|
+
return { sessionId: session.id };
|
|
7282
|
+
}
|
|
7283
|
+
spawnEmbeddedPty(ctx, undefined, session, project.repoPath, serverUrl, buildClaudeArgs(buildInput), { resumeSessionId });
|
|
7284
|
+
return { sessionId: session.id };
|
|
7285
|
+
}
|
|
7286
|
+
async function buildPrepareBrief(ctx, project) {
|
|
7287
|
+
const findings = await listFindings(ctx, project);
|
|
7288
|
+
const notes = latestPrepNotes(ctx, project.id);
|
|
7289
|
+
return {
|
|
7290
|
+
project,
|
|
7291
|
+
remainingKeys: keysToPrepare(ctx, project),
|
|
7292
|
+
established: findings.map((f) => ({
|
|
7293
|
+
key: f.key,
|
|
7294
|
+
source: f.source,
|
|
7295
|
+
...f.evidence ? { evidence: f.evidence } : {}
|
|
7296
|
+
})),
|
|
7297
|
+
...notes ? { notes } : {}
|
|
7298
|
+
};
|
|
7299
|
+
}
|
|
6705
7300
|
async function workWaypoint(ctx, input, opts = {}) {
|
|
6706
7301
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
6707
7302
|
if (!feature.mapped) {
|
|
@@ -6718,6 +7313,7 @@ async function workWaypoint(ctx, input, opts = {}) {
|
|
|
6718
7313
|
});
|
|
6719
7314
|
return { runId };
|
|
6720
7315
|
}
|
|
7316
|
+
sweepActiveSessions(ctx, feature, input.endLive === true);
|
|
6721
7317
|
assertSpawnable(ctx, feature);
|
|
6722
7318
|
return launchSession(ctx, { featureId: feature.id, kind: "waypoint", waypointId: wp.id }, opts);
|
|
6723
7319
|
}
|
|
@@ -6774,7 +7370,7 @@ function handlePtyExit(ctx, feature, session, meta, exitCode) {
|
|
|
6774
7370
|
releaseForSession(ctx, session.id);
|
|
6775
7371
|
if (diedBeforeLive && meta.resumeSessionId) {
|
|
6776
7372
|
const label = meta.waypoint ? `waypoint ${meta.waypoint.seq} (${meta.waypoint.title})` : session.kind;
|
|
6777
|
-
|
|
7373
|
+
emitForSession(ctx, session, {
|
|
6778
7374
|
type: "session.resume_failed",
|
|
6779
7375
|
message: `resume failed for ${label} \u2014 the session exited before starting (code ${exitCode ?? "unknown"}); the previous conversation is still resumable`,
|
|
6780
7376
|
data: {
|
|
@@ -6785,7 +7381,7 @@ function handlePtyExit(ctx, feature, session, meta, exitCode) {
|
|
|
6785
7381
|
}
|
|
6786
7382
|
});
|
|
6787
7383
|
}
|
|
6788
|
-
|
|
7384
|
+
emitForSession(ctx, session, {
|
|
6789
7385
|
type: "session.pty_exited",
|
|
6790
7386
|
message: ptyExitMessage(exitCode),
|
|
6791
7387
|
data: { sessionId: session.id, exitCode: exitCode ?? null }
|
|
@@ -6808,7 +7404,7 @@ function spawnEmbeddedPty(ctx, feature, session, worktreePath, serverUrl, claude
|
|
|
6808
7404
|
opts: { cwd: worktreePath, env, cols: 80, rows: 24, useConpty: true },
|
|
6809
7405
|
onExit: ({ exitCode }) => handlePtyExit(ctx, feature, session, meta, exitCode)
|
|
6810
7406
|
});
|
|
6811
|
-
|
|
7407
|
+
emitForSession(ctx, session, {
|
|
6812
7408
|
type: "session.launched",
|
|
6813
7409
|
message: "embedded terminal spawned",
|
|
6814
7410
|
data: { sessionId: session.id, mode: "embedded", pid: entry.pty.pid }
|
|
@@ -6817,7 +7413,7 @@ function spawnEmbeddedPty(ctx, feature, session, worktreePath, serverUrl, claude
|
|
|
6817
7413
|
} catch (err) {
|
|
6818
7414
|
markSessionEnded(ctx, session.id);
|
|
6819
7415
|
releaseForSession(ctx, session.id);
|
|
6820
|
-
|
|
7416
|
+
emitForSession(ctx, session, {
|
|
6821
7417
|
type: "session.spawn_failed",
|
|
6822
7418
|
message: `failed to spawn embedded terminal: ${err instanceof Error ? err.message : String(err)}`,
|
|
6823
7419
|
data: { sessionId: session.id, mode: "embedded" }
|
|
@@ -6832,6 +7428,7 @@ var init_launcher = __esm(() => {
|
|
|
6832
7428
|
init_skills_root();
|
|
6833
7429
|
init_schema();
|
|
6834
7430
|
init_errors();
|
|
7431
|
+
init_end_session();
|
|
6835
7432
|
init_registry();
|
|
6836
7433
|
init_events();
|
|
6837
7434
|
init_gates();
|
|
@@ -6840,6 +7437,8 @@ var init_launcher = __esm(() => {
|
|
|
6840
7437
|
init_tickets();
|
|
6841
7438
|
init_waypoints();
|
|
6842
7439
|
init_runner();
|
|
7440
|
+
init_findings();
|
|
7441
|
+
init_prep();
|
|
6843
7442
|
init_artifacts();
|
|
6844
7443
|
init_sessions();
|
|
6845
7444
|
init_sessions();
|
|
@@ -6883,7 +7482,7 @@ var init_feature = __esm(() => {
|
|
|
6883
7482
|
kind: SessionKind,
|
|
6884
7483
|
kickoffLine: z7.string().min(1).optional()
|
|
6885
7484
|
})).mutation(({ ctx, input }) => launchSession(ctx, input)),
|
|
6886
|
-
workWaypoint: publicProcedure.input(z7.object({ featureId: z7.string(), waypointId: z7.string() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
|
|
7485
|
+
workWaypoint: publicProcedure.input(z7.object({ featureId: z7.string(), waypointId: z7.string(), endLive: z7.boolean().optional() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
|
|
6887
7486
|
converge: publicProcedure.input(z7.object({ featureId: z7.string(), overrideReason: z7.string().min(1).optional() })).mutation(({ ctx, input }) => converge(ctx, { featureId: input.featureId, overrideReason: input.overrideReason })),
|
|
6888
7487
|
resendKickoff: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => resendKickoff(ctx, input.sessionId)),
|
|
6889
7488
|
endSession: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => endSession(ctx, input.sessionId)),
|
|
@@ -6924,7 +7523,7 @@ var init_feature = __esm(() => {
|
|
|
6924
7523
|
// src/services/fsbrowse.ts
|
|
6925
7524
|
import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
|
|
6926
7525
|
import { homedir as homedir2 } from "os";
|
|
6927
|
-
import { basename, dirname as dirname7, isAbsolute, join as join15, resolve as
|
|
7526
|
+
import { basename, dirname as dirname7, isAbsolute, join as join15, resolve as resolve9, sep as sep3 } from "path";
|
|
6928
7527
|
function expandPath(input, home = homedir2()) {
|
|
6929
7528
|
const trimmed = input.trim();
|
|
6930
7529
|
if (trimmed === "")
|
|
@@ -6934,7 +7533,7 @@ function expandPath(input, home = homedir2()) {
|
|
|
6934
7533
|
out = home;
|
|
6935
7534
|
else if (out.startsWith("~/") || out.startsWith("~\\"))
|
|
6936
7535
|
out = join15(home, out.slice(2));
|
|
6937
|
-
return
|
|
7536
|
+
return resolve9(out);
|
|
6938
7537
|
}
|
|
6939
7538
|
function looksLikeRepo(dir) {
|
|
6940
7539
|
try {
|
|
@@ -7087,6 +7686,7 @@ async function openProject(ctx, rawPath) {
|
|
|
7087
7686
|
message: `project ${existing.name} re-opened at ${repoPath} (${mainBranch})`
|
|
7088
7687
|
});
|
|
7089
7688
|
warnIfTranslatedMount(ctx, existing.id, repoPath);
|
|
7689
|
+
maybeAutoPrepare(ctx, reopened);
|
|
7090
7690
|
return reopened;
|
|
7091
7691
|
}
|
|
7092
7692
|
const row = {
|
|
@@ -7108,7 +7708,7 @@ async function openProject(ctx, rawPath) {
|
|
|
7108
7708
|
return project;
|
|
7109
7709
|
}
|
|
7110
7710
|
function maybeAutoPrepare(ctx, project) {
|
|
7111
|
-
if (!ctx
|
|
7711
|
+
if (!shouldAutoPrepare(ctx, project))
|
|
7112
7712
|
return;
|
|
7113
7713
|
startPrep(ctx, project.id).catch((e) => {
|
|
7114
7714
|
emitProject(ctx, project.id, {
|
|
@@ -7183,6 +7783,8 @@ var init_project = __esm(() => {
|
|
|
7183
7783
|
init_fsbrowse();
|
|
7184
7784
|
init_git();
|
|
7185
7785
|
init_prep();
|
|
7786
|
+
init_launcher();
|
|
7787
|
+
init_sessions();
|
|
7186
7788
|
init_projects();
|
|
7187
7789
|
init_repo();
|
|
7188
7790
|
init_context();
|
|
@@ -7205,6 +7807,8 @@ var init_project = __esm(() => {
|
|
|
7205
7807
|
});
|
|
7206
7808
|
return { prepId, keys };
|
|
7207
7809
|
}),
|
|
7810
|
+
talkToPrep: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ ctx, input }) => launchPrepareSession(ctx, { projectId: input.projectId })),
|
|
7811
|
+
prepSession: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => activeProjectSession(ctx, input.projectId, "prepare")),
|
|
7208
7812
|
cancelPrepare: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ input }) => {
|
|
7209
7813
|
cancelPrep(input.projectId);
|
|
7210
7814
|
return { ok: true };
|
|
@@ -7279,7 +7883,10 @@ function projectOverrides(ctx, projectId) {
|
|
|
7279
7883
|
setupCommand: projects.setupCommand,
|
|
7280
7884
|
verifyCommands: projects.verifyCommands,
|
|
7281
7885
|
knownFailures: projects.knownFailures,
|
|
7282
|
-
dbResetCommand: projects.dbResetCommand
|
|
7886
|
+
dbResetCommand: projects.dbResetCommand,
|
|
7887
|
+
driveSetupCommand: projects.driveSetupCommand,
|
|
7888
|
+
driveStopCommand: projects.driveStopCommand,
|
|
7889
|
+
driveEnv: projects.driveEnv
|
|
7283
7890
|
}).from(projects).where(eq12(projects.id, projectId)).get();
|
|
7284
7891
|
return {
|
|
7285
7892
|
model: row?.model ?? null,
|
|
@@ -7288,7 +7895,10 @@ function projectOverrides(ctx, projectId) {
|
|
|
7288
7895
|
setupCommand: row?.setupCommand ?? null,
|
|
7289
7896
|
verifyCommands: row?.verifyCommands ?? null,
|
|
7290
7897
|
knownFailures: row?.knownFailures ?? null,
|
|
7291
|
-
dbResetCommand: row?.dbResetCommand ?? null
|
|
7898
|
+
dbResetCommand: row?.dbResetCommand ?? null,
|
|
7899
|
+
driveSetupCommand: row?.driveSetupCommand ?? null,
|
|
7900
|
+
driveStopCommand: row?.driveStopCommand ?? null,
|
|
7901
|
+
driveEnv: row?.driveEnv ?? null
|
|
7292
7902
|
};
|
|
7293
7903
|
}
|
|
7294
7904
|
function resolveField(desc5, layers) {
|
|
@@ -7474,6 +8084,14 @@ var init_settings = __esm(() => {
|
|
|
7474
8084
|
valueSchema: z10.string().min(1),
|
|
7475
8085
|
parseEnv: idEnv
|
|
7476
8086
|
},
|
|
8087
|
+
{
|
|
8088
|
+
key: "sessionMcp",
|
|
8089
|
+
configKey: "sessionMcp",
|
|
8090
|
+
envVar: "RUNCASTLE_SESSION_MCP",
|
|
8091
|
+
restartRequired: false,
|
|
8092
|
+
valueSchema: z10.enum(["inherit", "runcastleOnly"]),
|
|
8093
|
+
parseEnv: idEnv
|
|
8094
|
+
},
|
|
7477
8095
|
{
|
|
7478
8096
|
key: "burnConcurrency",
|
|
7479
8097
|
configKey: "burnConcurrency",
|
|
@@ -7570,6 +8188,27 @@ var init_settings = __esm(() => {
|
|
|
7570
8188
|
restartRequired: false,
|
|
7571
8189
|
valueSchema: z10.string().min(1),
|
|
7572
8190
|
parseEnv: idEnv
|
|
8191
|
+
},
|
|
8192
|
+
{
|
|
8193
|
+
key: "driveSetupCommand",
|
|
8194
|
+
projectColumn: "driveSetupCommand",
|
|
8195
|
+
restartRequired: false,
|
|
8196
|
+
valueSchema: z10.string().min(1),
|
|
8197
|
+
parseEnv: idEnv
|
|
8198
|
+
},
|
|
8199
|
+
{
|
|
8200
|
+
key: "driveStopCommand",
|
|
8201
|
+
projectColumn: "driveStopCommand",
|
|
8202
|
+
restartRequired: false,
|
|
8203
|
+
valueSchema: z10.string().min(1),
|
|
8204
|
+
parseEnv: idEnv
|
|
8205
|
+
},
|
|
8206
|
+
{
|
|
8207
|
+
key: "driveEnv",
|
|
8208
|
+
projectColumn: "driveEnv",
|
|
8209
|
+
restartRequired: false,
|
|
8210
|
+
valueSchema: z10.string().min(1),
|
|
8211
|
+
parseEnv: idEnv
|
|
7573
8212
|
}
|
|
7574
8213
|
];
|
|
7575
8214
|
DEFAULTS = RuncastleConfig.parse({});
|
|
@@ -7751,13 +8390,13 @@ var init_doctor = __esm(() => {
|
|
|
7751
8390
|
});
|
|
7752
8391
|
|
|
7753
8392
|
// src/doctor/system-exec.ts
|
|
7754
|
-
import { spawn as
|
|
8393
|
+
import { spawn as spawn3 } from "child_process";
|
|
7755
8394
|
function createSystemExec(opts = {}) {
|
|
7756
|
-
return (command, args) => new Promise((
|
|
8395
|
+
return (command, args) => new Promise((resolve10) => {
|
|
7757
8396
|
const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
|
|
7758
8397
|
let stdout = "";
|
|
7759
8398
|
let stderr = "";
|
|
7760
|
-
const child =
|
|
8399
|
+
const child = spawn3(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
|
|
7761
8400
|
child.stdout?.on("data", (d) => {
|
|
7762
8401
|
stdout += d.toString();
|
|
7763
8402
|
});
|
|
@@ -7765,10 +8404,10 @@ function createSystemExec(opts = {}) {
|
|
|
7765
8404
|
stderr += d.toString();
|
|
7766
8405
|
});
|
|
7767
8406
|
child.on("error", (err) => {
|
|
7768
|
-
|
|
8407
|
+
resolve10({ ok: false, code: null, stdout, stderr: stderr || String(err) });
|
|
7769
8408
|
});
|
|
7770
8409
|
child.on("close", (code) => {
|
|
7771
|
-
|
|
8410
|
+
resolve10({ ok: true, code: code ?? 0, stdout, stderr });
|
|
7772
8411
|
});
|
|
7773
8412
|
});
|
|
7774
8413
|
}
|
|
@@ -8217,7 +8856,7 @@ import { Hono as Hono4 } from "hono";
|
|
|
8217
8856
|
function buildApp(ctx) {
|
|
8218
8857
|
const app = new Hono4;
|
|
8219
8858
|
setRuntimeCtx(ctx);
|
|
8220
|
-
app.get("/health", (c) => c.json({ ok: true }));
|
|
8859
|
+
app.get("/health", (c) => c.json({ ok: true, dataDir: dataDir() }));
|
|
8221
8860
|
app.use("/api/trpc/*", async (c, next) => {
|
|
8222
8861
|
await next();
|
|
8223
8862
|
if (c.res.headers.get("content-type") === "application/json") {
|
|
@@ -8262,7 +8901,7 @@ async function startServer() {
|
|
|
8262
8901
|
},
|
|
8263
8902
|
websocket: terminalWebSocket
|
|
8264
8903
|
});
|
|
8265
|
-
console.log(`runcastle server listening on http://localhost:${config2.serverPort}`);
|
|
8904
|
+
console.log(`runcastle${process.env.RUNCASTLE_DEV ? " [dev]" : ""} server listening on ` + `http://localhost:${config2.serverPort} \u2014 data dir: ${dataDir()}`);
|
|
8266
8905
|
const SHUTDOWN_KEY = Symbol.for("runcastle.shutdown.wired");
|
|
8267
8906
|
const g = globalThis;
|
|
8268
8907
|
if (!g[SHUTDOWN_KEY]) {
|
|
@@ -8398,7 +9037,7 @@ var init_cli = __esm(() => {
|
|
|
8398
9037
|
// src/bin/runcastle.ts
|
|
8399
9038
|
init_asset_paths();
|
|
8400
9039
|
init_version();
|
|
8401
|
-
import { dirname as dirname11, resolve as
|
|
9040
|
+
import { dirname as dirname11, resolve as resolve10 } from "path";
|
|
8402
9041
|
import { fileURLToPath as fileURLToPath9 } from "url";
|
|
8403
9042
|
function parseCommand(argv) {
|
|
8404
9043
|
const [first, ...rest] = argv;
|
|
@@ -8423,7 +9062,7 @@ Usage:
|
|
|
8423
9062
|
runcastle --help Show this help
|
|
8424
9063
|
`;
|
|
8425
9064
|
async function main(argv) {
|
|
8426
|
-
applyInstalledAssetEnv(
|
|
9065
|
+
applyInstalledAssetEnv(resolve10(dirname11(fileURLToPath9(import.meta.url)), ".."));
|
|
8427
9066
|
const { command, args } = parseCommand(argv);
|
|
8428
9067
|
switch (command) {
|
|
8429
9068
|
case "version":
|