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