runcastle 1.0.25 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/runcastle.js +792 -103
- 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/0014_ordinary_mystique.sql +4 -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/0014_snapshot.json +817 -0
- package/drizzle/meta/_journal.json +28 -0
- package/index.js +790 -101
- package/package.json +1 -1
- package/skills/burner/prepare-project.md +13 -2
- package/skills/packs/runcastle/skills/ideate/SKILL.md +2 -0
- package/skills/packs/runcastle/skills/revisit/SKILL.md +18 -8
- package/web/assets/index-CBycxXwk.js +138 -0
- package/web/assets/index-yePFYqdd.css +1 -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"]);
|
|
@@ -246,6 +271,7 @@ var init_schemas = __esm(() => {
|
|
|
246
271
|
featureId: z2.string(),
|
|
247
272
|
seq: z2.number(),
|
|
248
273
|
status: TicketStatus,
|
|
274
|
+
lap: z2.number(),
|
|
249
275
|
commits: z2.array(z2.string()),
|
|
250
276
|
error: z2.string().optional(),
|
|
251
277
|
attemptBranch: z2.string().optional(),
|
|
@@ -276,10 +302,13 @@ var init_schemas = __esm(() => {
|
|
|
276
302
|
"verifyCommands",
|
|
277
303
|
"knownFailures",
|
|
278
304
|
"devCommand",
|
|
305
|
+
"driveSetupCommand",
|
|
306
|
+
"driveStopCommand",
|
|
307
|
+
"driveEnv",
|
|
279
308
|
"dbResetCommand"
|
|
280
309
|
];
|
|
281
310
|
PreparedKey = z2.enum(PREPARED_KEYS);
|
|
282
|
-
FindingSource = z2.enum(["prep", "human"]);
|
|
311
|
+
FindingSource = z2.enum(["prep", "human", "session"]);
|
|
283
312
|
Project = z2.object({
|
|
284
313
|
id: z2.string(),
|
|
285
314
|
name: z2.string(),
|
|
@@ -290,7 +319,10 @@ var init_schemas = __esm(() => {
|
|
|
290
319
|
setupCommand: z2.string().optional(),
|
|
291
320
|
verifyCommands: z2.string().optional(),
|
|
292
321
|
knownFailures: z2.string().optional(),
|
|
293
|
-
dbResetCommand: z2.string().optional()
|
|
322
|
+
dbResetCommand: z2.string().optional(),
|
|
323
|
+
driveSetupCommand: z2.string().optional(),
|
|
324
|
+
driveStopCommand: z2.string().optional(),
|
|
325
|
+
driveEnv: z2.string().optional()
|
|
294
326
|
});
|
|
295
327
|
ProjectFinding = z2.object({
|
|
296
328
|
key: PreparedKey,
|
|
@@ -317,6 +349,7 @@ var init_schemas = __esm(() => {
|
|
|
317
349
|
title: z2.string(),
|
|
318
350
|
oneLiner: z2.string(),
|
|
319
351
|
mapped: z2.boolean(),
|
|
352
|
+
lap: z2.number(),
|
|
320
353
|
phase: Phase,
|
|
321
354
|
branch: z2.string(),
|
|
322
355
|
baseBranch: z2.string().optional(),
|
|
@@ -325,7 +358,8 @@ var init_schemas = __esm(() => {
|
|
|
325
358
|
});
|
|
326
359
|
SessionRow = z2.object({
|
|
327
360
|
id: z2.string(),
|
|
328
|
-
featureId: z2.string(),
|
|
361
|
+
featureId: z2.string().optional(),
|
|
362
|
+
projectId: z2.string().optional(),
|
|
329
363
|
kind: SessionKind,
|
|
330
364
|
ccSessionId: z2.string().optional(),
|
|
331
365
|
transcriptPath: z2.string().optional(),
|
|
@@ -452,7 +486,7 @@ function nextGate(feature) {
|
|
|
452
486
|
return MAPPED_G1;
|
|
453
487
|
return gate;
|
|
454
488
|
}
|
|
455
|
-
var PIPELINE, ORDER, REVIEW_LOOP_BACK, MAPPED_G1;
|
|
489
|
+
var PIPELINE, ORDER, REVIEW_LOOP_BACK, RETHINK_LOOP_BACK, MAPPED_G1;
|
|
456
490
|
var init_pipeline = __esm(() => {
|
|
457
491
|
PIPELINE = [
|
|
458
492
|
{ phase: "ideation" },
|
|
@@ -499,6 +533,7 @@ var init_pipeline = __esm(() => {
|
|
|
499
533
|
];
|
|
500
534
|
ORDER = PIPELINE.map((p) => p.phase);
|
|
501
535
|
REVIEW_LOOP_BACK = { from: "review", to: "implementation" };
|
|
536
|
+
RETHINK_LOOP_BACK = { from: "review", to: "ideation" };
|
|
502
537
|
MAPPED_G1 = {
|
|
503
538
|
id: "G1",
|
|
504
539
|
description: "Every waypoint resolved or dropped before converging",
|
|
@@ -522,7 +557,10 @@ var init_db_schema = __esm(() => {
|
|
|
522
557
|
setupCommand: text("setup_command"),
|
|
523
558
|
verifyCommands: text("verify_commands"),
|
|
524
559
|
knownFailures: text("known_failures"),
|
|
525
|
-
dbResetCommand: text("db_reset_command")
|
|
560
|
+
dbResetCommand: text("db_reset_command"),
|
|
561
|
+
driveSetupCommand: text("drive_setup_command"),
|
|
562
|
+
driveStopCommand: text("drive_stop_command"),
|
|
563
|
+
driveEnv: text("drive_env")
|
|
526
564
|
});
|
|
527
565
|
projectFindings = sqliteTable("project_findings", {
|
|
528
566
|
projectId: text("project_id").notNull(),
|
|
@@ -548,6 +586,7 @@ var init_db_schema = __esm(() => {
|
|
|
548
586
|
title: text("title").notNull(),
|
|
549
587
|
oneLiner: text("one_liner").notNull(),
|
|
550
588
|
mapped: integer("mapped", { mode: "boolean" }).notNull().default(false),
|
|
589
|
+
lap: integer("lap").notNull().default(1),
|
|
551
590
|
phase: text("phase").notNull().$type(),
|
|
552
591
|
branch: text("branch").notNull(),
|
|
553
592
|
baseBranch: text("base_branch"),
|
|
@@ -556,7 +595,9 @@ var init_db_schema = __esm(() => {
|
|
|
556
595
|
});
|
|
557
596
|
sessions = sqliteTable("sessions", {
|
|
558
597
|
id: text("id").primaryKey(),
|
|
559
|
-
featureId: text("feature_id")
|
|
598
|
+
featureId: text("feature_id"),
|
|
599
|
+
projectId: text("project_id"),
|
|
600
|
+
lap: integer("lap").notNull().default(1),
|
|
560
601
|
kind: text("kind").notNull().$type(),
|
|
561
602
|
ccSessionId: text("cc_session_id"),
|
|
562
603
|
transcriptPath: text("transcript_path"),
|
|
@@ -573,6 +614,7 @@ var init_db_schema = __esm(() => {
|
|
|
573
614
|
acceptanceCriteria: text("acceptance_criteria", { mode: "json" }).notNull().$type(),
|
|
574
615
|
seams: text("seams", { mode: "json" }).notNull().$type(),
|
|
575
616
|
blockedBy: text("blocked_by", { mode: "json" }).notNull().$type(),
|
|
617
|
+
lap: integer("lap").notNull().default(1),
|
|
576
618
|
status: text("status").notNull().$type(),
|
|
577
619
|
commits: text("commits", { mode: "json" }).notNull().$type(),
|
|
578
620
|
error: text("error"),
|
|
@@ -608,6 +650,7 @@ var init_db_schema = __esm(() => {
|
|
|
608
650
|
featureId: text("feature_id"),
|
|
609
651
|
runId: text("run_id"),
|
|
610
652
|
ticketId: text("ticket_id"),
|
|
653
|
+
lap: integer("lap").notNull().default(1),
|
|
611
654
|
ts: integer("ts").notNull(),
|
|
612
655
|
type: text("type").notNull(),
|
|
613
656
|
message: text("message").notNull(),
|
|
@@ -668,11 +711,11 @@ var init_client = __esm(() => {
|
|
|
668
711
|
|
|
669
712
|
// src/launcher/asset-paths.ts
|
|
670
713
|
import { existsSync as existsSync2 } from "fs";
|
|
671
|
-
import { join as join3, resolve } from "path";
|
|
714
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
672
715
|
function resolveAsset(envVar, fallback) {
|
|
673
716
|
const override = process.env[envVar];
|
|
674
717
|
if (override) {
|
|
675
|
-
const abs =
|
|
718
|
+
const abs = resolve2(override);
|
|
676
719
|
if (existsSync2(abs))
|
|
677
720
|
return abs;
|
|
678
721
|
throw new Error(`${envVar}=${override} does not exist (resolved ${abs})`);
|
|
@@ -1346,16 +1389,28 @@ function projectIdForFeature(ctx, featureId) {
|
|
|
1346
1389
|
throw new NotFoundError(`feature ${featureId} not found`);
|
|
1347
1390
|
return row.projectId;
|
|
1348
1391
|
}
|
|
1392
|
+
function lapForFeature(ctx, featureId) {
|
|
1393
|
+
const row = ctx.db.select({ lap: features.lap }).from(features).where(eq(features.id, featureId)).get();
|
|
1394
|
+
return row?.lap ?? 1;
|
|
1395
|
+
}
|
|
1349
1396
|
function emit(ctx, featureId, e) {
|
|
1350
1397
|
return insertEvent(ctx, projectIdForFeature(ctx, featureId), featureId, e);
|
|
1351
1398
|
}
|
|
1352
1399
|
function emitProject(ctx, projectId, e) {
|
|
1353
1400
|
return insertEvent(ctx, projectId, null, e);
|
|
1354
1401
|
}
|
|
1402
|
+
function emitForSession(ctx, session, e) {
|
|
1403
|
+
if (session.featureId)
|
|
1404
|
+
return emit(ctx, session.featureId, e);
|
|
1405
|
+
if (session.projectId)
|
|
1406
|
+
return emitProject(ctx, session.projectId, e);
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1355
1409
|
function insertEvent(ctx, projectId, featureId, e) {
|
|
1356
1410
|
const row = ctx.db.insert(events).values({
|
|
1357
1411
|
projectId,
|
|
1358
1412
|
featureId,
|
|
1413
|
+
lap: featureId ? lapForFeature(ctx, featureId) : 1,
|
|
1359
1414
|
runId: e.runId ?? null,
|
|
1360
1415
|
ticketId: e.ticketId ?? null,
|
|
1361
1416
|
ts: Date.now(),
|
|
@@ -1396,6 +1451,7 @@ function rowToFeature(row) {
|
|
|
1396
1451
|
title: row.title,
|
|
1397
1452
|
oneLiner: row.oneLiner,
|
|
1398
1453
|
mapped: row.mapped,
|
|
1454
|
+
lap: row.lap,
|
|
1399
1455
|
phase: row.phase,
|
|
1400
1456
|
branch: row.branch,
|
|
1401
1457
|
baseBranch: row.baseBranch ?? undefined,
|
|
@@ -1414,7 +1470,10 @@ function rowToProject(row) {
|
|
|
1414
1470
|
setupCommand: row.setupCommand ?? undefined,
|
|
1415
1471
|
verifyCommands: row.verifyCommands ?? undefined,
|
|
1416
1472
|
knownFailures: row.knownFailures ?? undefined,
|
|
1417
|
-
dbResetCommand: row.dbResetCommand ?? undefined
|
|
1473
|
+
dbResetCommand: row.dbResetCommand ?? undefined,
|
|
1474
|
+
driveSetupCommand: row.driveSetupCommand ?? undefined,
|
|
1475
|
+
driveStopCommand: row.driveStopCommand ?? undefined,
|
|
1476
|
+
driveEnv: row.driveEnv ?? undefined
|
|
1418
1477
|
};
|
|
1419
1478
|
}
|
|
1420
1479
|
function rowToRun(row) {
|
|
@@ -1431,7 +1490,8 @@ function rowToRun(row) {
|
|
|
1431
1490
|
function rowToSession(row) {
|
|
1432
1491
|
return {
|
|
1433
1492
|
id: row.id,
|
|
1434
|
-
featureId: row.featureId,
|
|
1493
|
+
featureId: row.featureId ?? undefined,
|
|
1494
|
+
projectId: row.projectId ?? undefined,
|
|
1435
1495
|
kind: row.kind,
|
|
1436
1496
|
ccSessionId: row.ccSessionId ?? undefined,
|
|
1437
1497
|
transcriptPath: row.transcriptPath ?? undefined,
|
|
@@ -1648,7 +1708,7 @@ function releaseForSession(ctx, claimant) {
|
|
|
1648
1708
|
const held = ctx.db.select().from(waypoints).where(and3(eq3(waypoints.claimedBy, claimant), eq3(waypoints.status, "claimed"))).all().map(rowToWaypoint);
|
|
1649
1709
|
return held.map((w) => release(ctx, w.id));
|
|
1650
1710
|
}
|
|
1651
|
-
function
|
|
1711
|
+
function resolve3(ctx, id, disposition, summary) {
|
|
1652
1712
|
const wp = getWaypoint(ctx, id);
|
|
1653
1713
|
const before = listByFeature(ctx, wp.featureId);
|
|
1654
1714
|
const bySeqBefore = new Map(before.map((w) => [w.seq, w]));
|
|
@@ -1691,7 +1751,9 @@ import { and as and4, desc as desc2, eq as eq4, inArray, isNotNull, sql as sql2
|
|
|
1691
1751
|
function createSessionRow(ctx, input) {
|
|
1692
1752
|
const row = ctx.db.insert(sessions).values({
|
|
1693
1753
|
id: newId("sess"),
|
|
1694
|
-
featureId: input.featureId,
|
|
1754
|
+
featureId: input.featureId ?? null,
|
|
1755
|
+
projectId: input.projectId ?? null,
|
|
1756
|
+
lap: input.featureId ? getFeatureRow(ctx, input.featureId).lap : 1,
|
|
1695
1757
|
kind: input.kind,
|
|
1696
1758
|
ccSessionId: null,
|
|
1697
1759
|
transcriptPath: null,
|
|
@@ -1723,6 +1785,9 @@ function markSessionLive(ctx, id, input = {}) {
|
|
|
1723
1785
|
}
|
|
1724
1786
|
return getSessionRow(ctx, id);
|
|
1725
1787
|
}
|
|
1788
|
+
function lapKickoff(lap) {
|
|
1789
|
+
return `Proceed with your task: invoke the /runcastle:revisit skill for LAP ${lap} REVIEW ITERATION. ` + `Call get_feature_context, then read this feature's test-notes.md (the "## Lap ${lap - 1}" ` + 'section \u2014 what the last drive surfaced) and the "## Later laps" section of its spec.md. ' + "EITHER MAY NOT EXIST YET; that is normal, not an error \u2014 say so and carry on from what I " + "tell you. Interview me about what the test drive taught: what was wrong, what was missing, " + "what I want next. Write what we settle on into decisions.md and amend spec.md for this lap " + '(pruning anything you promote out of "## Later laps"), then call emit_tickets for this ' + `lap's work. Finish in THIS session: complete_phase through ideation \u2192 spec \u2192 tickets, then ` + "tell me to review the cards and click Burn.";
|
|
1790
|
+
}
|
|
1726
1791
|
function kickoffLineFor(kind, override) {
|
|
1727
1792
|
return override ?? KICKOFF_LINES[kind];
|
|
1728
1793
|
}
|
|
@@ -1788,7 +1853,7 @@ function attemptKickoff(ctx, session, d, delayMs, clearFirst = false) {
|
|
|
1788
1853
|
writeKickoffSequence(d.line, {
|
|
1789
1854
|
write: io.write,
|
|
1790
1855
|
alive: io.alive,
|
|
1791
|
-
onSubmitted: () =>
|
|
1856
|
+
onSubmitted: () => emitForSession(ctx, session, {
|
|
1792
1857
|
type: "session.kickoff",
|
|
1793
1858
|
message: attempt === 1 ? `${session.kind} session kicked off automatically` : `${session.kind} kickoff re-sent (attempt ${attempt}) \u2014 the first was never acknowledged`,
|
|
1794
1859
|
data: { sessionId: session.id, kind: session.kind, attempt }
|
|
@@ -1812,7 +1877,7 @@ function armConfirmation(ctx, session, d) {
|
|
|
1812
1877
|
function settleUndelivered(ctx, session, d, reason) {
|
|
1813
1878
|
d.settled = true;
|
|
1814
1879
|
stopTimers(d);
|
|
1815
|
-
|
|
1880
|
+
emitForSession(ctx, session, {
|
|
1816
1881
|
type: "session.kickoff_undelivered",
|
|
1817
1882
|
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
1883
|
data: { sessionId: session.id, kind: session.kind, reason, attempts: d.attempts, line: d.line }
|
|
@@ -1876,7 +1941,7 @@ function armSessionReadyWatchdog(ctx, session) {
|
|
|
1876
1941
|
return;
|
|
1877
1942
|
if (!ptyIo(session.id).alive())
|
|
1878
1943
|
return;
|
|
1879
|
-
|
|
1944
|
+
emitForSession(ctx, session, {
|
|
1880
1945
|
type: "session.not_ready",
|
|
1881
1946
|
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
1947
|
data: { sessionId: session.id, kind: session.kind }
|
|
@@ -1906,6 +1971,14 @@ function mostRecentResumableSession(ctx, featureId, kind) {
|
|
|
1906
1971
|
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
1972
|
return row ? rowToSession(row) : null;
|
|
1908
1973
|
}
|
|
1974
|
+
function activeProjectSession(ctx, projectId, kind) {
|
|
1975
|
+
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();
|
|
1976
|
+
return row ? rowToSession(row) : null;
|
|
1977
|
+
}
|
|
1978
|
+
function mostRecentResumableProjectSession(ctx, projectId, kind) {
|
|
1979
|
+
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();
|
|
1980
|
+
return row ? rowToSession(row) : null;
|
|
1981
|
+
}
|
|
1909
1982
|
function mostRecentLiveSession(ctx) {
|
|
1910
1983
|
const row = ctx.db.select().from(sessions).where(eq4(sessions.status, "live")).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1911
1984
|
return row ? rowToSession(row) : null;
|
|
@@ -1924,7 +1997,8 @@ var init_sessions = __esm(() => {
|
|
|
1924
1997
|
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
1998
|
waypoint: "Proceed with your task: invoke the /runcastle:waypoint skill and work your assigned " + "waypoint to a resolution.",
|
|
1926
1999
|
converge: CONVERGE_KICKOFF_LINE,
|
|
1927
|
-
revisit: "Proceed with your task: invoke the /runcastle:revisit skill and work through what the " + "human brings up."
|
|
2000
|
+
revisit: "Proceed with your task: invoke the /runcastle:revisit skill and work through what the " + "human brings up.",
|
|
2001
|
+
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
2002
|
};
|
|
1929
2003
|
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
2004
|
pendingKickoffOverrides = new Map;
|
|
@@ -1942,7 +2016,7 @@ function reconcileStaleSessions(ctx) {
|
|
|
1942
2016
|
continue;
|
|
1943
2017
|
markSessionEnded(ctx, session.id);
|
|
1944
2018
|
const released = releaseForSession(ctx, session.id);
|
|
1945
|
-
|
|
2019
|
+
emitForSession(ctx, session, {
|
|
1946
2020
|
type: "session.reconciled",
|
|
1947
2021
|
message: `session marked ended at boot \u2014 the server restarted while it was ${session.status}`,
|
|
1948
2022
|
data: {
|
|
@@ -1966,11 +2040,11 @@ var init_reconcile = __esm(() => {
|
|
|
1966
2040
|
|
|
1967
2041
|
// src/launcher/skills-root.ts
|
|
1968
2042
|
import { existsSync as existsSync5 } from "fs";
|
|
1969
|
-
import { dirname, join as join5, resolve as
|
|
2043
|
+
import { dirname, join as join5, resolve as resolve4 } from "path";
|
|
1970
2044
|
function resolveSkillsRoot(fromDir) {
|
|
1971
2045
|
const override = process.env[SKILLS_DIR_ENV];
|
|
1972
2046
|
if (override) {
|
|
1973
|
-
const root =
|
|
2047
|
+
const root = resolve4(override);
|
|
1974
2048
|
if (existsSync5(join5(root, SKILLS_MARKER)))
|
|
1975
2049
|
return root;
|
|
1976
2050
|
throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
|
|
@@ -2011,7 +2085,7 @@ function devSpawnTarget(devCommand) {
|
|
|
2011
2085
|
return { file: "/bin/sh", args: ["-c", devCommand] };
|
|
2012
2086
|
}
|
|
2013
2087
|
function startDevPane(input) {
|
|
2014
|
-
const { ctx, featureId, repoPath, devCommand, onUrl } = input;
|
|
2088
|
+
const { ctx, featureId, repoPath, devCommand, env, onUrl } = input;
|
|
2015
2089
|
const paneId = drivePaneId(featureId);
|
|
2016
2090
|
const { file, args } = devSpawnTarget(devCommand);
|
|
2017
2091
|
try {
|
|
@@ -2019,7 +2093,7 @@ function startDevPane(input) {
|
|
|
2019
2093
|
sessionId: paneId,
|
|
2020
2094
|
cmd: file,
|
|
2021
2095
|
args,
|
|
2022
|
-
opts: { cwd: repoPath, env: process.env, cols: 80, rows: 24, useConpty: true }
|
|
2096
|
+
opts: { cwd: repoPath, env: env ?? process.env, cols: 80, rows: 24, useConpty: true }
|
|
2023
2097
|
});
|
|
2024
2098
|
} catch (err) {
|
|
2025
2099
|
emit(ctx, featureId, {
|
|
@@ -2069,10 +2143,179 @@ var init_dev_pane = __esm(() => {
|
|
|
2069
2143
|
init_registry();
|
|
2070
2144
|
});
|
|
2071
2145
|
|
|
2146
|
+
// src/services/drive-hooks.ts
|
|
2147
|
+
import { spawn as spawn2 } from "child_process";
|
|
2148
|
+
function tailLines(text2, max = OUTPUT_TAIL_LINES) {
|
|
2149
|
+
const lines = text2.replace(/\r\n/g, `
|
|
2150
|
+
`).split(`
|
|
2151
|
+
`);
|
|
2152
|
+
while (lines.length > 0 && lines[lines.length - 1]?.trim() === "")
|
|
2153
|
+
lines.pop();
|
|
2154
|
+
return lines.slice(-max).join(`
|
|
2155
|
+
`).trim();
|
|
2156
|
+
}
|
|
2157
|
+
function hookSpawnTarget(command) {
|
|
2158
|
+
if (process.platform === "win32") {
|
|
2159
|
+
return {
|
|
2160
|
+
file: process.env.ComSpec ?? "cmd.exe",
|
|
2161
|
+
args: ["/d", "/s", "/c", `"${command}"`],
|
|
2162
|
+
verbatim: true
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
return { file: "/bin/sh", args: ["-c", command], verbatim: false };
|
|
2166
|
+
}
|
|
2167
|
+
function runDriveHook(command, opts) {
|
|
2168
|
+
const spawnFn = opts.spawnFn ?? spawn2;
|
|
2169
|
+
const now = opts.now ?? Date.now;
|
|
2170
|
+
const timeoutMs = opts.timeoutMs ?? DRIVE_HOOK_TIMEOUT_MS;
|
|
2171
|
+
const startedAt = now();
|
|
2172
|
+
const { file, args, verbatim } = hookSpawnTarget(command);
|
|
2173
|
+
return new Promise((resolve5) => {
|
|
2174
|
+
let output = "";
|
|
2175
|
+
let timedOut = false;
|
|
2176
|
+
let settled = false;
|
|
2177
|
+
let timer;
|
|
2178
|
+
const finish = (exitCode) => {
|
|
2179
|
+
if (settled)
|
|
2180
|
+
return;
|
|
2181
|
+
settled = true;
|
|
2182
|
+
if (timer)
|
|
2183
|
+
clearTimeout(timer);
|
|
2184
|
+
resolve5({
|
|
2185
|
+
ok: !timedOut && exitCode === 0,
|
|
2186
|
+
exitCode,
|
|
2187
|
+
timedOut,
|
|
2188
|
+
output: tailLines(output),
|
|
2189
|
+
durationMs: now() - startedAt
|
|
2190
|
+
});
|
|
2191
|
+
};
|
|
2192
|
+
let child;
|
|
2193
|
+
try {
|
|
2194
|
+
child = spawnFn(file, args, {
|
|
2195
|
+
cwd: opts.cwd,
|
|
2196
|
+
env: opts.env ?? process.env,
|
|
2197
|
+
windowsHide: true,
|
|
2198
|
+
windowsVerbatimArguments: verbatim
|
|
2199
|
+
});
|
|
2200
|
+
} catch (err) {
|
|
2201
|
+
output = err instanceof Error ? err.message : String(err);
|
|
2202
|
+
finish(null);
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
timer = setTimeout(() => {
|
|
2206
|
+
timedOut = true;
|
|
2207
|
+
child.kill();
|
|
2208
|
+
const grace = setTimeout(() => finish(null), KILL_GRACE_MS);
|
|
2209
|
+
grace.unref?.();
|
|
2210
|
+
}, timeoutMs);
|
|
2211
|
+
timer.unref?.();
|
|
2212
|
+
child.stdout?.on("data", (d) => {
|
|
2213
|
+
output += d.toString();
|
|
2214
|
+
});
|
|
2215
|
+
child.stderr?.on("data", (d) => {
|
|
2216
|
+
output += d.toString();
|
|
2217
|
+
});
|
|
2218
|
+
child.on("error", (err) => {
|
|
2219
|
+
output += `
|
|
2220
|
+
${err instanceof Error ? err.message : String(err)}`;
|
|
2221
|
+
finish(null);
|
|
2222
|
+
});
|
|
2223
|
+
child.on("close", (code) => {
|
|
2224
|
+
finish(code);
|
|
2225
|
+
});
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
function describeHookResult(command, result) {
|
|
2229
|
+
const secs = Math.max(1, Math.round(result.durationMs / 1000));
|
|
2230
|
+
if (result.timedOut)
|
|
2231
|
+
return `\`${command}\` timed out after ${secs}s`;
|
|
2232
|
+
if (result.ok)
|
|
2233
|
+
return `\`${command}\` finished in ${secs}s`;
|
|
2234
|
+
return `\`${command}\` exited ${result.exitCode ?? "without a code"} after ${secs}s`;
|
|
2235
|
+
}
|
|
2236
|
+
var DRIVE_HOOK_TIMEOUT_MS, OUTPUT_TAIL_LINES = 40, KILL_GRACE_MS = 2000;
|
|
2237
|
+
var init_drive_hooks = __esm(() => {
|
|
2238
|
+
DRIVE_HOOK_TIMEOUT_MS = 10 * 60000;
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
// src/services/drive-env.ts
|
|
2242
|
+
import { createHash } from "crypto";
|
|
2243
|
+
function identifierSafe(slug, max = MAX_ID_LENGTH) {
|
|
2244
|
+
const cleaned = slug.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2245
|
+
const safe = cleaned === "" || /^[0-9]/.test(cleaned) ? `x_${cleaned}` : cleaned;
|
|
2246
|
+
if (safe.length <= max)
|
|
2247
|
+
return safe;
|
|
2248
|
+
const digest = createHash("sha1").update(slug).digest("hex").slice(0, 6);
|
|
2249
|
+
return `${safe.slice(0, max - digest.length - 1)}_${digest}`;
|
|
2250
|
+
}
|
|
2251
|
+
function driveVars(identity) {
|
|
2252
|
+
return {
|
|
2253
|
+
slug: identity.slug,
|
|
2254
|
+
branch: identity.branch,
|
|
2255
|
+
id: identifierSafe(identity.slug)
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
function renderTemplate(value, vars) {
|
|
2259
|
+
const unknown2 = [];
|
|
2260
|
+
const out = value.replace(PLACEHOLDER_RE, (match, name) => {
|
|
2261
|
+
const replacement = vars[name];
|
|
2262
|
+
if (replacement === undefined) {
|
|
2263
|
+
if (!unknown2.includes(name))
|
|
2264
|
+
unknown2.push(name);
|
|
2265
|
+
return match;
|
|
2266
|
+
}
|
|
2267
|
+
return replacement;
|
|
2268
|
+
});
|
|
2269
|
+
return { value: out, unknown: unknown2 };
|
|
2270
|
+
}
|
|
2271
|
+
function parseDriveEnv(raw, identity) {
|
|
2272
|
+
const vars = {};
|
|
2273
|
+
const unknown2 = [];
|
|
2274
|
+
if (!raw?.trim())
|
|
2275
|
+
return { vars, unknown: unknown2 };
|
|
2276
|
+
const substitutions = driveVars(identity);
|
|
2277
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
2278
|
+
const trimmed = line.trim();
|
|
2279
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
2280
|
+
continue;
|
|
2281
|
+
const eq5 = trimmed.indexOf("=");
|
|
2282
|
+
if (eq5 <= 0)
|
|
2283
|
+
continue;
|
|
2284
|
+
const key = trimmed.slice(0, eq5).trim().replace(/^export\s+/, "");
|
|
2285
|
+
if (key === "")
|
|
2286
|
+
continue;
|
|
2287
|
+
const rendered = renderTemplate(unquote(trimmed.slice(eq5 + 1).trim()), substitutions);
|
|
2288
|
+
vars[key] = rendered.value;
|
|
2289
|
+
for (const name of rendered.unknown)
|
|
2290
|
+
if (!unknown2.includes(name))
|
|
2291
|
+
unknown2.push(name);
|
|
2292
|
+
}
|
|
2293
|
+
return { vars, unknown: unknown2 };
|
|
2294
|
+
}
|
|
2295
|
+
function unquote(value) {
|
|
2296
|
+
if (value.length < 2)
|
|
2297
|
+
return value;
|
|
2298
|
+
const first = value[0];
|
|
2299
|
+
if ((first === '"' || first === "'") && value.endsWith(first))
|
|
2300
|
+
return value.slice(1, -1);
|
|
2301
|
+
return value;
|
|
2302
|
+
}
|
|
2303
|
+
function driveProcessEnv(overrides, base = process.env) {
|
|
2304
|
+
return { ...base, ...overrides };
|
|
2305
|
+
}
|
|
2306
|
+
function describeDriveEnv(vars) {
|
|
2307
|
+
const keys = Object.keys(vars);
|
|
2308
|
+
return `drive environment: ${keys.join(", ")}`;
|
|
2309
|
+
}
|
|
2310
|
+
var MAX_ID_LENGTH = 40, PLACEHOLDER_RE;
|
|
2311
|
+
var init_drive_env = __esm(() => {
|
|
2312
|
+
PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
|
|
2313
|
+
});
|
|
2314
|
+
|
|
2072
2315
|
// src/services/git.ts
|
|
2073
2316
|
import { existsSync as existsSync6, mkdirSync as mkdirSync2, mkdtempSync, realpathSync, rmSync } from "fs";
|
|
2074
2317
|
import { tmpdir } from "os";
|
|
2075
|
-
import { dirname as dirname2, join as join6, resolve as
|
|
2318
|
+
import { dirname as dirname2, join as join6, resolve as resolve5 } from "path";
|
|
2076
2319
|
import { simpleGit } from "simple-git";
|
|
2077
2320
|
function migrationPaths(paths) {
|
|
2078
2321
|
return paths.filter((p) => MIGRATION_DIR_RE.test(p) || DRIZZLE_SQL_RE.test(p));
|
|
@@ -2087,7 +2330,7 @@ function errMsg(e) {
|
|
|
2087
2330
|
return e instanceof Error ? e.message : String(e);
|
|
2088
2331
|
}
|
|
2089
2332
|
function canon(p) {
|
|
2090
|
-
let abs =
|
|
2333
|
+
let abs = resolve5(p);
|
|
2091
2334
|
try {
|
|
2092
2335
|
abs = realpathSync.native(abs);
|
|
2093
2336
|
} catch {}
|
|
@@ -2582,7 +2825,7 @@ async function deleteFeatureBranches(repoPath, slug) {
|
|
|
2582
2825
|
return { deleted, kept };
|
|
2583
2826
|
}
|
|
2584
2827
|
async function commitDocs(worktreePath, message) {
|
|
2585
|
-
if (!existsSync6(
|
|
2828
|
+
if (!existsSync6(resolve5(worktreePath, DOCS_PATHSPEC)))
|
|
2586
2829
|
return;
|
|
2587
2830
|
const g = git(worktreePath);
|
|
2588
2831
|
await g.add([DOCS_PATHSPEC]);
|
|
@@ -2627,6 +2870,7 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2627
2870
|
const devPaneId = testDriveState.devPaneId;
|
|
2628
2871
|
if (devPaneId)
|
|
2629
2872
|
stopDevPane(devPaneId);
|
|
2873
|
+
const teardownFailure = await runDriveHookStep(ctx, feature.id, project.repoPath, "teardown", project.driveStopCommand, driveEnvFor(ctx, project, feature, branch));
|
|
2630
2874
|
const carriedChanges = await dirtyPaths(g);
|
|
2631
2875
|
await g.checkout(previousBranch2);
|
|
2632
2876
|
if (detachedWorktree2)
|
|
@@ -2649,7 +2893,8 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2649
2893
|
ok: true,
|
|
2650
2894
|
branch: previousBranch2,
|
|
2651
2895
|
...carriedChanges.length > 0 ? { carriedChanges } : {},
|
|
2652
|
-
...dbDrift ? { dbDrift } : {}
|
|
2896
|
+
...dbDrift ? { dbDrift } : {},
|
|
2897
|
+
...teardownFailure ? { hookFailure: teardownFailure } : {}
|
|
2653
2898
|
};
|
|
2654
2899
|
}
|
|
2655
2900
|
const porcelain = (await g.raw(["status", "--porcelain"])).trim();
|
|
@@ -2674,18 +2919,76 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2674
2919
|
message: `test driving ${branch} (was on ${previousBranch})`,
|
|
2675
2920
|
data: { branch, previousBranch }
|
|
2676
2921
|
});
|
|
2922
|
+
const driveEnv = driveEnvFor(ctx, project, feature, branch);
|
|
2923
|
+
const setupFailure = await runDriveHookStep(ctx, feature.id, project.repoPath, "setup", project.driveSetupCommand, driveEnv);
|
|
2677
2924
|
if (project.devCommand) {
|
|
2678
2925
|
const devPaneId = startDevPane({
|
|
2679
2926
|
ctx,
|
|
2680
2927
|
featureId: feature.id,
|
|
2681
2928
|
repoPath: project.repoPath,
|
|
2682
2929
|
devCommand: project.devCommand,
|
|
2930
|
+
env: driveEnv,
|
|
2683
2931
|
onUrl: (url) => recordDriveUrl(ctx, feature.id, url)
|
|
2684
2932
|
});
|
|
2685
2933
|
if (devPaneId)
|
|
2686
2934
|
testDriveState.devPaneId = devPaneId;
|
|
2687
2935
|
}
|
|
2688
|
-
return { ok: true, branch };
|
|
2936
|
+
return { ok: true, branch, ...setupFailure ? { hookFailure: setupFailure } : {} };
|
|
2937
|
+
}
|
|
2938
|
+
function driveEnvFor(ctx, project, feature, branch) {
|
|
2939
|
+
const { vars, unknown: unknown2 } = parseDriveEnv(project.driveEnv, { slug: feature.slug, branch });
|
|
2940
|
+
if (unknown2.length > 0) {
|
|
2941
|
+
emit(ctx, feature.id, {
|
|
2942
|
+
type: "testdrive.env_unknown_placeholder",
|
|
2943
|
+
message: `drive environment left ${unknown2.map((u) => `{{${u}}}`).join(", ")} unsubstituted \u2014 known variables are {{slug}}, {{branch}}, {{id}}`,
|
|
2944
|
+
data: { unknown: unknown2 }
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
if (Object.keys(vars).length > 0) {
|
|
2948
|
+
emit(ctx, feature.id, {
|
|
2949
|
+
type: "testdrive.env",
|
|
2950
|
+
message: describeDriveEnv(vars),
|
|
2951
|
+
data: { keys: Object.keys(vars) }
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
return driveProcessEnv(vars);
|
|
2955
|
+
}
|
|
2956
|
+
async function runDriveHookStep(ctx, featureId, repoPath, phase, command, env) {
|
|
2957
|
+
const cmd = command?.trim();
|
|
2958
|
+
if (!cmd)
|
|
2959
|
+
return;
|
|
2960
|
+
emit(ctx, featureId, {
|
|
2961
|
+
type: `testdrive.${phase}_started`,
|
|
2962
|
+
message: phase === "setup" ? `preparing environment: \`${cmd}\`` : `tearing down environment: \`${cmd}\``,
|
|
2963
|
+
data: { command: cmd }
|
|
2964
|
+
});
|
|
2965
|
+
const result = await runDriveHook(cmd, { cwd: repoPath, env });
|
|
2966
|
+
if (result.ok) {
|
|
2967
|
+
emit(ctx, featureId, {
|
|
2968
|
+
type: `testdrive.${phase}_ok`,
|
|
2969
|
+
message: describeHookResult(cmd, result),
|
|
2970
|
+
data: { command: cmd, durationMs: result.durationMs }
|
|
2971
|
+
});
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
emit(ctx, featureId, {
|
|
2975
|
+
type: `testdrive.${phase}_failed`,
|
|
2976
|
+
message: `${describeHookResult(cmd, result)}${result.output ? `
|
|
2977
|
+
${result.output}` : ""}`,
|
|
2978
|
+
data: {
|
|
2979
|
+
command: cmd,
|
|
2980
|
+
exitCode: result.exitCode,
|
|
2981
|
+
timedOut: result.timedOut,
|
|
2982
|
+
output: result.output
|
|
2983
|
+
}
|
|
2984
|
+
});
|
|
2985
|
+
return {
|
|
2986
|
+
phase,
|
|
2987
|
+
command: cmd,
|
|
2988
|
+
exitCode: result.exitCode,
|
|
2989
|
+
timedOut: result.timedOut,
|
|
2990
|
+
output: result.output
|
|
2991
|
+
};
|
|
2689
2992
|
}
|
|
2690
2993
|
async function dirtyPaths(g) {
|
|
2691
2994
|
try {
|
|
@@ -2776,6 +3079,8 @@ var init_git = __esm(() => {
|
|
|
2776
3079
|
init_paths();
|
|
2777
3080
|
init_errors();
|
|
2778
3081
|
init_dev_pane();
|
|
3082
|
+
init_drive_hooks();
|
|
3083
|
+
init_drive_env();
|
|
2779
3084
|
init_events();
|
|
2780
3085
|
init_repo();
|
|
2781
3086
|
MIGRATION_DIR_RE = /(^|\/)(migrations|migrate)\//i;
|
|
@@ -2961,7 +3266,7 @@ function detectCycle(tickets2) {
|
|
|
2961
3266
|
}
|
|
2962
3267
|
return null;
|
|
2963
3268
|
}
|
|
2964
|
-
function
|
|
3269
|
+
function renderTemplate2(template, values) {
|
|
2965
3270
|
let out = template;
|
|
2966
3271
|
for (const [key, value] of Object.entries(values)) {
|
|
2967
3272
|
out = out.split(`{{${key}}}`).join(value);
|
|
@@ -2969,7 +3274,7 @@ function renderTemplate(template, values) {
|
|
|
2969
3274
|
return out;
|
|
2970
3275
|
}
|
|
2971
3276
|
function renderTicketPrompt(template, values) {
|
|
2972
|
-
return
|
|
3277
|
+
return renderTemplate2(template, values);
|
|
2973
3278
|
}
|
|
2974
3279
|
function buildTicketJson(ticket) {
|
|
2975
3280
|
return JSON.stringify({
|
|
@@ -3057,6 +3362,9 @@ function detectPackageManager(tc) {
|
|
|
3057
3362
|
return "npm";
|
|
3058
3363
|
return tc.hasPackageJson ? "npm" : undefined;
|
|
3059
3364
|
}
|
|
3365
|
+
function orFallback(strict, permissive) {
|
|
3366
|
+
return `( ${strict} || ${permissive} )`;
|
|
3367
|
+
}
|
|
3060
3368
|
function resolveSetupCommand(tc, override) {
|
|
3061
3369
|
const trimmed = override?.trim();
|
|
3062
3370
|
if (trimmed)
|
|
@@ -3066,13 +3374,13 @@ function resolveSetupCommand(tc, override) {
|
|
|
3066
3374
|
return;
|
|
3067
3375
|
switch (pm) {
|
|
3068
3376
|
case "bun":
|
|
3069
|
-
return tc.lockfiles.bun ? "bun install --frozen-lockfile" : "bun install";
|
|
3377
|
+
return tc.lockfiles.bun ? orFallback("bun install --frozen-lockfile", "bun install") : "bun install";
|
|
3070
3378
|
case "pnpm":
|
|
3071
|
-
return tc.lockfiles.pnpm ? "corepack pnpm install --frozen-lockfile" : "corepack pnpm install";
|
|
3379
|
+
return tc.lockfiles.pnpm ? orFallback("corepack pnpm install --frozen-lockfile", "corepack pnpm install") : "corepack pnpm install";
|
|
3072
3380
|
case "yarn":
|
|
3073
|
-
return tc.lockfiles.yarn ? "corepack yarn install --frozen-lockfile" : "corepack yarn install";
|
|
3381
|
+
return tc.lockfiles.yarn ? orFallback("corepack yarn install --frozen-lockfile", "corepack yarn install") : "corepack yarn install";
|
|
3074
3382
|
case "npm":
|
|
3075
|
-
return tc.lockfiles.npm ? "npm ci" : "npm install";
|
|
3383
|
+
return tc.lockfiles.npm ? orFallback("npm ci", "npm install") : "npm install";
|
|
3076
3384
|
}
|
|
3077
3385
|
}
|
|
3078
3386
|
function cacheMountFor(pm, hostPath) {
|
|
@@ -3156,7 +3464,7 @@ function readRepoToolchain(repoPath) {
|
|
|
3156
3464
|
}
|
|
3157
3465
|
};
|
|
3158
3466
|
}
|
|
3159
|
-
function createStreamThrottle(
|
|
3467
|
+
function createStreamThrottle(emit3, opts = {}) {
|
|
3160
3468
|
const intervalMs = opts.intervalMs ?? 2000;
|
|
3161
3469
|
const maxChars = opts.maxChars ?? 500;
|
|
3162
3470
|
const now = opts.now ?? Date.now;
|
|
@@ -3166,7 +3474,7 @@ function createStreamThrottle(emit2, opts = {}) {
|
|
|
3166
3474
|
const flushText = () => {
|
|
3167
3475
|
if (buffer.length === 0)
|
|
3168
3476
|
return;
|
|
3169
|
-
|
|
3477
|
+
emit3({ type: "burn.text", message: buffer, data: { iteration: lastIteration } });
|
|
3170
3478
|
buffer = "";
|
|
3171
3479
|
lastFlush = now();
|
|
3172
3480
|
};
|
|
@@ -3181,7 +3489,7 @@ function createStreamThrottle(emit2, opts = {}) {
|
|
|
3181
3489
|
flushText();
|
|
3182
3490
|
const rawArgs = event.formattedArgs ?? "";
|
|
3183
3491
|
const args = rawArgs.length > 200 ? `${rawArgs.slice(0, 200)}\u2026` : rawArgs;
|
|
3184
|
-
|
|
3492
|
+
emit3({
|
|
3185
3493
|
type: "burn.tool",
|
|
3186
3494
|
message: args.length > 0 ? `${event.name} ${args}` : event.name,
|
|
3187
3495
|
data: { name: event.name, args: rawArgs, iteration: event.iteration }
|
|
@@ -3293,18 +3601,18 @@ function buildRetryNotes(input) {
|
|
|
3293
3601
|
`);
|
|
3294
3602
|
}
|
|
3295
3603
|
function delayUnlessAborted(ms, signal) {
|
|
3296
|
-
return new Promise((
|
|
3604
|
+
return new Promise((resolve6) => {
|
|
3297
3605
|
if (signal.aborted) {
|
|
3298
|
-
|
|
3606
|
+
resolve6();
|
|
3299
3607
|
return;
|
|
3300
3608
|
}
|
|
3301
3609
|
const onAbort = () => {
|
|
3302
3610
|
clearTimeout(t);
|
|
3303
|
-
|
|
3611
|
+
resolve6();
|
|
3304
3612
|
};
|
|
3305
3613
|
const t = setTimeout(() => {
|
|
3306
3614
|
signal.removeEventListener("abort", onAbort);
|
|
3307
|
-
|
|
3615
|
+
resolve6();
|
|
3308
3616
|
}, ms);
|
|
3309
3617
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
3310
3618
|
});
|
|
@@ -3658,7 +3966,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3658
3966
|
const runResolver = async (input) => {
|
|
3659
3967
|
const resolveBranch = ticketBranchName(feature.slug, ticket.seq, newId("r").slice(2, 10));
|
|
3660
3968
|
const otherSide = await commitSummaries(project.repoPath, input.branch, feature.branch);
|
|
3661
|
-
const prompt =
|
|
3969
|
+
const prompt = renderTemplate2(readFileSync3(resolverTemplatePath(), "utf8"), {
|
|
3662
3970
|
TICKET_JSON: ticketJson,
|
|
3663
3971
|
FEATURE_BRIEF: featureBrief,
|
|
3664
3972
|
DOCS_DIGEST: docsDigest,
|
|
@@ -4138,8 +4446,11 @@ function errorHeadline2(s) {
|
|
|
4138
4446
|
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
4139
4447
|
return causes.at(-1) ?? lines[0] ?? "";
|
|
4140
4448
|
}
|
|
4141
|
-
function createPrepStreamThrottle(
|
|
4142
|
-
return createStreamThrottle((e) =>
|
|
4449
|
+
function createPrepStreamThrottle(emit3, opts = {}) {
|
|
4450
|
+
return createStreamThrottle((e) => emit3({ ...e, type: e.type.replace(/^burn\./, "prep.") }), opts);
|
|
4451
|
+
}
|
|
4452
|
+
function nonFatalSetup(setupCommand) {
|
|
4453
|
+
return `( ${setupCommand} || true )`;
|
|
4143
4454
|
}
|
|
4144
4455
|
function prepTemplatePath() {
|
|
4145
4456
|
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
@@ -4172,10 +4483,11 @@ async function realExecutePrepRun(ctx, config2, token, model) {
|
|
|
4172
4483
|
if (setupCommand) {
|
|
4173
4484
|
ctx.emitEvent({
|
|
4174
4485
|
type: "prep.setup",
|
|
4175
|
-
message: `
|
|
4486
|
+
message: `attempting a dependency install before the agent starts (best-effort): ${setupCommand}`
|
|
4176
4487
|
});
|
|
4177
4488
|
}
|
|
4178
|
-
const
|
|
4489
|
+
const prepSetupCommand = setupCommand ? nonFatalSetup(setupCommand) : undefined;
|
|
4490
|
+
const hookCommand = workspaceMode === "isolated" ? buildIsolatedSetupCommand(branch, prepSetupCommand, pm) : prepSetupCommand;
|
|
4179
4491
|
const runOptions = {
|
|
4180
4492
|
agent: buildBurnAgent(config2, token, model),
|
|
4181
4493
|
sandbox: selectSandbox(config2, mounts),
|
|
@@ -4275,6 +4587,9 @@ var init_project_prep = __esm(() => {
|
|
|
4275
4587
|
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
4588
|
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
4589
|
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.",
|
|
4590
|
+
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.",
|
|
4591
|
+
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.",
|
|
4592
|
+
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
4593
|
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
4594
|
};
|
|
4280
4595
|
});
|
|
@@ -4381,6 +4696,9 @@ var init_findings = __esm(() => {
|
|
|
4381
4696
|
verifyCommands: projects.verifyCommands,
|
|
4382
4697
|
knownFailures: projects.knownFailures,
|
|
4383
4698
|
devCommand: projects.devCommand,
|
|
4699
|
+
driveSetupCommand: projects.driveSetupCommand,
|
|
4700
|
+
driveStopCommand: projects.driveStopCommand,
|
|
4701
|
+
driveEnv: projects.driveEnv,
|
|
4384
4702
|
dbResetCommand: projects.dbResetCommand
|
|
4385
4703
|
};
|
|
4386
4704
|
COLUMN_NAME = {
|
|
@@ -4388,13 +4706,16 @@ var init_findings = __esm(() => {
|
|
|
4388
4706
|
verifyCommands: "verifyCommands",
|
|
4389
4707
|
knownFailures: "knownFailures",
|
|
4390
4708
|
devCommand: "devCommand",
|
|
4709
|
+
driveSetupCommand: "driveSetupCommand",
|
|
4710
|
+
driveStopCommand: "driveStopCommand",
|
|
4711
|
+
driveEnv: "driveEnv",
|
|
4391
4712
|
dbResetCommand: "dbResetCommand"
|
|
4392
4713
|
};
|
|
4393
4714
|
PREPARED_SET = new Set(PREPARED_KEYS);
|
|
4394
4715
|
});
|
|
4395
4716
|
|
|
4396
4717
|
// src/services/prep.ts
|
|
4397
|
-
import { desc as desc3, eq as eq6 } from "drizzle-orm";
|
|
4718
|
+
import { and as and6, desc as desc3, eq as eq6 } from "drizzle-orm";
|
|
4398
4719
|
function isPreparing(projectId) {
|
|
4399
4720
|
return controllers.has(projectId);
|
|
4400
4721
|
}
|
|
@@ -4403,6 +4724,13 @@ function keysToPrepare(ctx, project, opts = {}) {
|
|
|
4403
4724
|
const candidates = opts.keys ?? (opts.refresh ? PREPARED_KEYS : [...unset]);
|
|
4404
4725
|
return candidates.filter((key) => (opts.refresh || unset.has(key)) && isOverwritable(ctx, project.id, key));
|
|
4405
4726
|
}
|
|
4727
|
+
function shouldAutoPrepare(ctx, project) {
|
|
4728
|
+
if (!ctx.config.autoPrepare)
|
|
4729
|
+
return false;
|
|
4730
|
+
if (latestPrep(ctx, project.id))
|
|
4731
|
+
return false;
|
|
4732
|
+
return keysToPrepare(ctx, project).length > 0;
|
|
4733
|
+
}
|
|
4406
4734
|
async function startPrep(ctx, projectId, opts = {}) {
|
|
4407
4735
|
const project = requireProjectById(ctx, projectId);
|
|
4408
4736
|
if (controllers.has(projectId)) {
|
|
@@ -4522,6 +4850,11 @@ function latestPrep(ctx, projectId) {
|
|
|
4522
4850
|
...row.headSha !== null ? { headSha: row.headSha } : {}
|
|
4523
4851
|
};
|
|
4524
4852
|
}
|
|
4853
|
+
function latestPrepNotes(ctx, projectId) {
|
|
4854
|
+
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();
|
|
4855
|
+
const notes = row?.data?.notes;
|
|
4856
|
+
return typeof notes === "string" && notes.trim() !== "" ? notes : undefined;
|
|
4857
|
+
}
|
|
4525
4858
|
function reconcilePreps(ctx) {
|
|
4526
4859
|
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
4860
|
for (const row of stale) {
|
|
@@ -4576,7 +4909,7 @@ var init_feature_docs = __esm(() => {
|
|
|
4576
4909
|
});
|
|
4577
4910
|
|
|
4578
4911
|
// src/services/tickets.ts
|
|
4579
|
-
import { and as
|
|
4912
|
+
import { and as and7, asc as asc3, eq as eq7 } from "drizzle-orm";
|
|
4580
4913
|
function rowToTicket(row) {
|
|
4581
4914
|
return {
|
|
4582
4915
|
id: row.id,
|
|
@@ -4588,6 +4921,7 @@ function rowToTicket(row) {
|
|
|
4588
4921
|
acceptanceCriteria: row.acceptanceCriteria,
|
|
4589
4922
|
seams: row.seams,
|
|
4590
4923
|
blockedBy: row.blockedBy,
|
|
4924
|
+
lap: row.lap,
|
|
4591
4925
|
status: row.status,
|
|
4592
4926
|
commits: row.commits,
|
|
4593
4927
|
error: row.error ?? undefined,
|
|
@@ -4610,6 +4944,7 @@ function listByFeature2(ctx, featureId) {
|
|
|
4610
4944
|
function storeTickets(ctx, featureId, inputs) {
|
|
4611
4945
|
if (inputs.length === 0)
|
|
4612
4946
|
return [];
|
|
4947
|
+
const { lap } = getFeatureRow(ctx, featureId);
|
|
4613
4948
|
const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq7(tickets.featureId, featureId)).all();
|
|
4614
4949
|
const startSeq = existing.reduce((max, r) => Math.max(max, r.seq), 0) + 1;
|
|
4615
4950
|
const resolved = resolveBlocking(inputs, startSeq);
|
|
@@ -4623,6 +4958,7 @@ function storeTickets(ctx, featureId, inputs) {
|
|
|
4623
4958
|
acceptanceCriteria: t.acceptanceCriteria,
|
|
4624
4959
|
seams: t.seams,
|
|
4625
4960
|
blockedBy: resolved[i].blockedBy,
|
|
4961
|
+
lap,
|
|
4626
4962
|
status: "pending",
|
|
4627
4963
|
commits: [],
|
|
4628
4964
|
error: null,
|
|
@@ -4687,7 +5023,7 @@ function cancelTicket(ctx, id, reason) {
|
|
|
4687
5023
|
return getTicket(ctx, id);
|
|
4688
5024
|
}
|
|
4689
5025
|
function sweepOrphanedBurning(ctx, featureId, reason) {
|
|
4690
|
-
const orphaned = ctx.db.select().from(tickets).where(
|
|
5026
|
+
const orphaned = ctx.db.select().from(tickets).where(and7(eq7(tickets.featureId, featureId), eq7(tickets.status, "burning"))).all().map(rowToTicket);
|
|
4691
5027
|
for (const t of orphaned) {
|
|
4692
5028
|
updateTicket(ctx, t.id, { status: "failed", error: reason });
|
|
4693
5029
|
emit(ctx, featureId, {
|
|
@@ -4730,6 +5066,7 @@ var init_tickets = __esm(() => {
|
|
|
4730
5066
|
init_schema();
|
|
4731
5067
|
init_errors();
|
|
4732
5068
|
init_events();
|
|
5069
|
+
init_repo();
|
|
4733
5070
|
MUTABLE_STATUSES = new Set(["pending", "failed"]);
|
|
4734
5071
|
});
|
|
4735
5072
|
|
|
@@ -4753,7 +5090,7 @@ function checkGate(ctx, check, feature) {
|
|
|
4753
5090
|
case "spec-file-exists":
|
|
4754
5091
|
return fileGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
|
|
4755
5092
|
case "tickets-approved": {
|
|
4756
|
-
const count = listByFeature2(ctx, feature.id).filter((t) => t.status !== "cancelled").length;
|
|
5093
|
+
const count = listByFeature2(ctx, feature.id).filter((t) => t.lap === feature.lap && t.status !== "cancelled").length;
|
|
4757
5094
|
return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
|
|
4758
5095
|
}
|
|
4759
5096
|
case "all-tickets-terminal": {
|
|
@@ -4810,7 +5147,7 @@ var init_gates = __esm(() => {
|
|
|
4810
5147
|
|
|
4811
5148
|
// src/services/knowledge.ts
|
|
4812
5149
|
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, writeFileSync } from "fs";
|
|
4813
|
-
import { join as join10, relative, resolve as
|
|
5150
|
+
import { join as join10, relative, resolve as resolve6, sep } from "path";
|
|
4814
5151
|
function scaffoldDocs(ctx, feature) {
|
|
4815
5152
|
const project = projectForFeature(ctx, feature);
|
|
4816
5153
|
const dir = featureDocsDir(project, feature);
|
|
@@ -4870,8 +5207,8 @@ function listDocs(ctx, feature) {
|
|
|
4870
5207
|
function readDoc(ctx, feature, relPath) {
|
|
4871
5208
|
const project = projectForFeature(ctx, feature);
|
|
4872
5209
|
const dir = featureDocsDir(project, feature);
|
|
4873
|
-
const root =
|
|
4874
|
-
const target =
|
|
5210
|
+
const root = resolve6(dir);
|
|
5211
|
+
const target = resolve6(dir, relPath);
|
|
4875
5212
|
const rel = relative(root, target);
|
|
4876
5213
|
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
4877
5214
|
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
@@ -4912,7 +5249,7 @@ function endSession(ctx, sessionId) {
|
|
|
4912
5249
|
const session = markSessionEnded(ctx, sessionId);
|
|
4913
5250
|
if (session) {
|
|
4914
5251
|
releaseForSession(ctx, sessionId);
|
|
4915
|
-
|
|
5252
|
+
emitForSession(ctx, session, {
|
|
4916
5253
|
type: "session.ended",
|
|
4917
5254
|
message: "session ended by user",
|
|
4918
5255
|
data: { sessionId, killed }
|
|
@@ -4984,8 +5321,8 @@ function errorHeadline3(s) {
|
|
|
4984
5321
|
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
4985
5322
|
return causes.at(-1) ?? lines[0] ?? "";
|
|
4986
5323
|
}
|
|
4987
|
-
function createResearchStreamThrottle(
|
|
4988
|
-
return createStreamThrottle((e) =>
|
|
5324
|
+
function createResearchStreamThrottle(emit3, opts = {}) {
|
|
5325
|
+
return createStreamThrottle((e) => emit3({ ...e, type: e.type.replace(/^burn\./, "research.") }), opts);
|
|
4989
5326
|
}
|
|
4990
5327
|
function renderResearchPrompt(template, values) {
|
|
4991
5328
|
let out = template;
|
|
@@ -5193,7 +5530,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
5193
5530
|
input: opts.input,
|
|
5194
5531
|
modelOverride: opts.modelOverride,
|
|
5195
5532
|
resolveWaypoint: (id, disposition, summary) => {
|
|
5196
|
-
|
|
5533
|
+
resolve3(ctx, id, disposition, summary);
|
|
5197
5534
|
},
|
|
5198
5535
|
signal: controller.signal
|
|
5199
5536
|
};
|
|
@@ -5288,6 +5625,7 @@ async function createFeature(ctx, input) {
|
|
|
5288
5625
|
title: input.title,
|
|
5289
5626
|
oneLiner: input.oneLiner,
|
|
5290
5627
|
mapped: false,
|
|
5628
|
+
lap: 1,
|
|
5291
5629
|
phase: "ideation",
|
|
5292
5630
|
branch,
|
|
5293
5631
|
baseBranch,
|
|
@@ -5372,7 +5710,8 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
5372
5710
|
const feature = getFeatureRow(ctx, featureId);
|
|
5373
5711
|
const running = hasActiveRun(ctx, featureId);
|
|
5374
5712
|
let tickets2 = listByFeature2(ctx, featureId);
|
|
5375
|
-
const
|
|
5713
|
+
const lapTickets = tickets2.filter((t) => t.lap === feature.lap);
|
|
5714
|
+
const pending = lapTickets.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
5376
5715
|
const restarting = feature.phase === "implementation" && !running;
|
|
5377
5716
|
const iterating = feature.phase === "review" && !running && pending.length >= 1;
|
|
5378
5717
|
if (feature.phase !== "tickets" && !restarting && !iterating) {
|
|
@@ -5385,8 +5724,8 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
5385
5724
|
why = `feature must be in the tickets phase to burn (currently ${feature.phase})`;
|
|
5386
5725
|
throw new GateError(why);
|
|
5387
5726
|
}
|
|
5388
|
-
if (
|
|
5389
|
-
throw new GateError(
|
|
5727
|
+
if (lapTickets.filter((t) => t.status !== "cancelled").length < 1) {
|
|
5728
|
+
throw new GateError(lapTickets.length > 0 ? "no burnable tickets \u2014 every ticket is cancelled" : "no tickets to burn");
|
|
5390
5729
|
}
|
|
5391
5730
|
if (restarting) {
|
|
5392
5731
|
sweepOrphanedBurning(ctx, featureId, "orphaned \u2014 the previous run died while it was burning");
|
|
@@ -5410,6 +5749,22 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
5410
5749
|
});
|
|
5411
5750
|
return { runId };
|
|
5412
5751
|
}
|
|
5752
|
+
function rethink(ctx, featureId) {
|
|
5753
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
5754
|
+
if (feature.phase !== RETHINK_LOOP_BACK.from) {
|
|
5755
|
+
throw new GateError(`feature must be in the review phase to rethink (currently ${feature.phase})`);
|
|
5756
|
+
}
|
|
5757
|
+
if (hasActiveRun(ctx, featureId)) {
|
|
5758
|
+
throw new GateError("a run is burning this feature \u2014 cancel or wait for it before rethinking");
|
|
5759
|
+
}
|
|
5760
|
+
const live = activeSessionsForFeature(ctx, featureId);
|
|
5761
|
+
if (live.length > 0) {
|
|
5762
|
+
throw new GateError(`a ${live[0].kind} session is already live for ${feature.slug} \u2014 only one terminal per feature; end or resume it first`);
|
|
5763
|
+
}
|
|
5764
|
+
const lap = feature.lap + 1;
|
|
5765
|
+
ctx.db.update(features).set({ lap }).where(eq9(features.id, featureId)).run();
|
|
5766
|
+
return setPhase(ctx, featureId, RETHINK_LOOP_BACK.to, "lap.started", `rethink \u2014 lap ${lap}`);
|
|
5767
|
+
}
|
|
5413
5768
|
async function retryTicket(ctx, ticketId, opts = {}) {
|
|
5414
5769
|
const ticket = getTicket(ctx, ticketId);
|
|
5415
5770
|
const feature = getFeatureRow(ctx, ticket.featureId);
|
|
@@ -5591,6 +5946,7 @@ var init_features = __esm(() => {
|
|
|
5591
5946
|
init_git();
|
|
5592
5947
|
init_knowledge();
|
|
5593
5948
|
init_repo();
|
|
5949
|
+
init_sessions();
|
|
5594
5950
|
init_end_session();
|
|
5595
5951
|
init_tickets();
|
|
5596
5952
|
init_waypoints();
|
|
@@ -5618,8 +5974,14 @@ function resolveSession(ctx, sessionId) {
|
|
|
5618
5974
|
}
|
|
5619
5975
|
return mostRecentLiveSession(ctx);
|
|
5620
5976
|
}
|
|
5977
|
+
function requireFeatureId(session) {
|
|
5978
|
+
if (!session.featureId) {
|
|
5979
|
+
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.");
|
|
5980
|
+
}
|
|
5981
|
+
return session.featureId;
|
|
5982
|
+
}
|
|
5621
5983
|
function toolGetFeatureContext(ctx, session) {
|
|
5622
|
-
const feature = getFeatureRow(ctx, session
|
|
5984
|
+
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
5623
5985
|
const docs = listDocs(ctx, feature).map((d) => {
|
|
5624
5986
|
try {
|
|
5625
5987
|
return { relPath: d.relPath, content: readDoc(ctx, feature, d.relPath).content };
|
|
@@ -5630,6 +5992,7 @@ function toolGetFeatureContext(ctx, session) {
|
|
|
5630
5992
|
const context = {
|
|
5631
5993
|
feature,
|
|
5632
5994
|
phase: feature.phase,
|
|
5995
|
+
lap: feature.lap,
|
|
5633
5996
|
docs,
|
|
5634
5997
|
tickets: listByFeature2(ctx, feature.id)
|
|
5635
5998
|
};
|
|
@@ -5641,17 +6004,17 @@ function toolGetFeatureContext(ctx, session) {
|
|
|
5641
6004
|
return context;
|
|
5642
6005
|
}
|
|
5643
6006
|
function toolResolveWaypoint(ctx, _session, input) {
|
|
5644
|
-
|
|
6007
|
+
resolve3(ctx, input.id, input.disposition, input.summary);
|
|
5645
6008
|
return { ok: true };
|
|
5646
6009
|
}
|
|
5647
6010
|
function toolEmitTickets(ctx, session, input) {
|
|
5648
|
-
const feature = getFeatureRow(ctx, session
|
|
6011
|
+
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
5649
6012
|
const stored = storeTickets(ctx, feature.id, input.tickets);
|
|
5650
6013
|
return { stored: stored.length, ids: stored.map((t) => t.id) };
|
|
5651
6014
|
}
|
|
5652
6015
|
function requireOwnTicket(ctx, session, ticketId) {
|
|
5653
6016
|
const ticket = getTicket(ctx, ticketId);
|
|
5654
|
-
if (ticket.featureId !== session
|
|
6017
|
+
if (ticket.featureId !== requireFeatureId(session)) {
|
|
5655
6018
|
throw new GateError(`ticket ${ticketId} does not belong to this session's feature`);
|
|
5656
6019
|
}
|
|
5657
6020
|
return ticket;
|
|
@@ -5666,10 +6029,10 @@ function toolCancelTicket(ctx, session, input) {
|
|
|
5666
6029
|
return { ok: true, ticket: cancelTicket(ctx, input.id, input.reason) };
|
|
5667
6030
|
}
|
|
5668
6031
|
function toolEscalateToMap(ctx, session, input) {
|
|
5669
|
-
return escalateToMap(ctx, session
|
|
6032
|
+
return escalateToMap(ctx, requireFeatureId(session), input);
|
|
5670
6033
|
}
|
|
5671
6034
|
function toolEmitWaypoints(ctx, session, input) {
|
|
5672
|
-
const feature = getFeatureRow(ctx, session
|
|
6035
|
+
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
5673
6036
|
if (!feature.mapped) {
|
|
5674
6037
|
throw new GateError("feature is not mapped \u2014 call escalate_to_map before emitting waypoints");
|
|
5675
6038
|
}
|
|
@@ -5677,7 +6040,7 @@ function toolEmitWaypoints(ctx, session, input) {
|
|
|
5677
6040
|
return { stored: stored.length, ids: stored.map((w) => w.id) };
|
|
5678
6041
|
}
|
|
5679
6042
|
function toolRecordEvent(ctx, session, input) {
|
|
5680
|
-
emit(ctx, session
|
|
6043
|
+
emit(ctx, requireFeatureId(session), {
|
|
5681
6044
|
type: input.type,
|
|
5682
6045
|
message: input.message,
|
|
5683
6046
|
data: { source: "mcp" }
|
|
@@ -5685,7 +6048,7 @@ function toolRecordEvent(ctx, session, input) {
|
|
|
5685
6048
|
return { ok: true };
|
|
5686
6049
|
}
|
|
5687
6050
|
function toolCompletePhase(ctx, session, input) {
|
|
5688
|
-
const feature = getFeatureRow(ctx, session
|
|
6051
|
+
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
5689
6052
|
emit(ctx, feature.id, {
|
|
5690
6053
|
type: "phase.complete_requested",
|
|
5691
6054
|
message: `session marked phase '${input.phase}' complete`,
|
|
@@ -5714,13 +6077,46 @@ async function commitDocsCheckpoint(ctx, session, message) {
|
|
|
5714
6077
|
try {
|
|
5715
6078
|
await commitDocs(session.worktreePath, message);
|
|
5716
6079
|
} catch (e) {
|
|
5717
|
-
emit(ctx, session
|
|
6080
|
+
emit(ctx, requireFeatureId(session), {
|
|
5718
6081
|
type: "git.commit_pending",
|
|
5719
6082
|
message: isNotImplemented(e) ? "docs checkpoint skipped (git service pending)" : `docs checkpoint failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
5720
6083
|
data: { message }
|
|
5721
6084
|
});
|
|
5722
6085
|
}
|
|
5723
6086
|
}
|
|
6087
|
+
function requireProject(ctx, session) {
|
|
6088
|
+
if (!session.projectId) {
|
|
6089
|
+
throw new GateError(`record_finding is for a project-scoped preparation session; this is a ${session.kind} session.`);
|
|
6090
|
+
}
|
|
6091
|
+
const project = getProjectById(ctx, session.projectId);
|
|
6092
|
+
if (!project)
|
|
6093
|
+
throw new GateError(`project ${session.projectId} not found`);
|
|
6094
|
+
return project;
|
|
6095
|
+
}
|
|
6096
|
+
function toolRecordFinding(ctx, session, input) {
|
|
6097
|
+
const project = requireProject(ctx, session);
|
|
6098
|
+
const source = input.userSupplied ? "human" : "session";
|
|
6099
|
+
if (!isOverwritable(ctx, project.id, input.key) && !input.userSupplied) {
|
|
6100
|
+
return {
|
|
6101
|
+
ok: true,
|
|
6102
|
+
key: input.key,
|
|
6103
|
+
source,
|
|
6104
|
+
skipped: `${input.key} was set by a human and is not auto-overwritable \u2014 ask them to clear it first if it should change`
|
|
6105
|
+
};
|
|
6106
|
+
}
|
|
6107
|
+
recordFinding(ctx, project.id, {
|
|
6108
|
+
key: input.key,
|
|
6109
|
+
value: input.value,
|
|
6110
|
+
source,
|
|
6111
|
+
...input.evidence ? { evidence: input.evidence } : {}
|
|
6112
|
+
});
|
|
6113
|
+
emitProject(ctx, project.id, {
|
|
6114
|
+
type: "prep.finding_recorded",
|
|
6115
|
+
message: `preparation session established ${input.key} (${source})`,
|
|
6116
|
+
data: { key: input.key, source, sessionId: session.id }
|
|
6117
|
+
});
|
|
6118
|
+
return { ok: true, key: input.key, source };
|
|
6119
|
+
}
|
|
5724
6120
|
function ok(result) {
|
|
5725
6121
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
5726
6122
|
}
|
|
@@ -5742,6 +6138,21 @@ async function resolveCtxSession(extra) {
|
|
|
5742
6138
|
}
|
|
5743
6139
|
function buildMcpServer() {
|
|
5744
6140
|
const server = new McpServer({ name: "runcastle", version: "0.1.0" });
|
|
6141
|
+
server.registerTool("record_finding", {
|
|
6142
|
+
title: "Record a preparation finding",
|
|
6143
|
+
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.",
|
|
6144
|
+
inputSchema: {
|
|
6145
|
+
key: PreparedKey,
|
|
6146
|
+
value: z4.string(),
|
|
6147
|
+
evidence: z4.string().optional(),
|
|
6148
|
+
userSupplied: z4.boolean().optional()
|
|
6149
|
+
}
|
|
6150
|
+
}, async (args, extra) => {
|
|
6151
|
+
const rs = await resolveCtxSession(extra);
|
|
6152
|
+
if (!rs)
|
|
6153
|
+
return noSession();
|
|
6154
|
+
return ok(toolRecordFinding(rs.ctx, rs.session, args));
|
|
6155
|
+
});
|
|
5745
6156
|
server.registerTool("get_feature_context", {
|
|
5746
6157
|
title: "Get feature context",
|
|
5747
6158
|
description: "Full context for the current feature: the feature row, its phase, all docs/features/<slug>/*.md contents, and its tickets.",
|
|
@@ -5864,6 +6275,7 @@ var init_server = __esm(() => {
|
|
|
5864
6275
|
init_sessions();
|
|
5865
6276
|
init_features();
|
|
5866
6277
|
init_events();
|
|
6278
|
+
init_findings();
|
|
5867
6279
|
init_git();
|
|
5868
6280
|
init_knowledge();
|
|
5869
6281
|
init_repo();
|
|
@@ -5974,6 +6386,57 @@ function handleSessionStart(ctx, sessionId, feature, payload) {
|
|
|
5974
6386
|
}
|
|
5975
6387
|
};
|
|
5976
6388
|
}
|
|
6389
|
+
function handlePrepareSessionStart(ctx, session, payload) {
|
|
6390
|
+
const ccSessionId = typeof payload?.session_id === "string" ? payload.session_id : undefined;
|
|
6391
|
+
const transcriptPath = typeof payload?.transcript_path === "string" ? payload.transcript_path : undefined;
|
|
6392
|
+
const source = typeof payload?.source === "string" ? payload.source : undefined;
|
|
6393
|
+
const wasLive = getSessionRow(ctx, session.id)?.status === "live";
|
|
6394
|
+
markSessionLive(ctx, session.id, { ccSessionId, transcriptPath });
|
|
6395
|
+
if (!wasLive) {
|
|
6396
|
+
emitForSession(ctx, session, {
|
|
6397
|
+
type: "session.started",
|
|
6398
|
+
message: source === "resume" ? "preparation session live (resumed)" : "preparation session live",
|
|
6399
|
+
data: { sessionId: session.id, ccSessionId, transcriptPath, source: source ?? null }
|
|
6400
|
+
});
|
|
6401
|
+
}
|
|
6402
|
+
return {
|
|
6403
|
+
hookSpecificOutput: {
|
|
6404
|
+
hookEventName: "SessionStart",
|
|
6405
|
+
additionalContext: prepareStartContext(ctx, session)
|
|
6406
|
+
}
|
|
6407
|
+
};
|
|
6408
|
+
}
|
|
6409
|
+
function handlePrepareUserPrompt(ctx, session, payload) {
|
|
6410
|
+
const prompt = typeof payload?.prompt === "string" ? payload.prompt : undefined;
|
|
6411
|
+
noteKickoffPrompt(ctx, session.id, prompt);
|
|
6412
|
+
return {
|
|
6413
|
+
hookSpecificOutput: {
|
|
6414
|
+
hookEventName: "UserPromptSubmit",
|
|
6415
|
+
additionalContext: `[runcastle] preparation session (project-scoped, no feature)`
|
|
6416
|
+
}
|
|
6417
|
+
};
|
|
6418
|
+
}
|
|
6419
|
+
function handlePrepareSessionEnd(ctx, session) {
|
|
6420
|
+
markSessionEnded(ctx, session.id);
|
|
6421
|
+
emitForSession(ctx, session, {
|
|
6422
|
+
type: "session.ended",
|
|
6423
|
+
message: "preparation session ended",
|
|
6424
|
+
data: { sessionId: session.id }
|
|
6425
|
+
});
|
|
6426
|
+
return {};
|
|
6427
|
+
}
|
|
6428
|
+
function prepareStartContext(ctx, session) {
|
|
6429
|
+
const project = session.projectId ? getProjectById(ctx, session.projectId) : null;
|
|
6430
|
+
if (!project)
|
|
6431
|
+
return "[runcastle] preparation session \u2014 project not found";
|
|
6432
|
+
const remaining = keysToPrepare(ctx, project);
|
|
6433
|
+
return [
|
|
6434
|
+
`[runcastle] preparation session for ${project.name} (${project.repoPath}).`,
|
|
6435
|
+
remaining.length > 0 ? `Still unestablished: ${remaining.join(", ")}.` : "Every prepared field currently has a value.",
|
|
6436
|
+
"Record what you establish with the `record_finding` MCP tool \u2014 a value the user",
|
|
6437
|
+
"gives you or confirms verbatim is theirs (userSupplied: true), anything you measured is not."
|
|
6438
|
+
].join(" ");
|
|
6439
|
+
}
|
|
5977
6440
|
function handleUserPrompt(ctx, sessionId, feature, payload) {
|
|
5978
6441
|
const prompt = typeof payload?.prompt === "string" ? payload.prompt : undefined;
|
|
5979
6442
|
noteKickoffPrompt(ctx, sessionId, prompt);
|
|
@@ -6015,6 +6478,7 @@ var hooks, hooks_default;
|
|
|
6015
6478
|
var init_hooks = __esm(() => {
|
|
6016
6479
|
init_sessions();
|
|
6017
6480
|
init_events();
|
|
6481
|
+
init_prep();
|
|
6018
6482
|
init_repo();
|
|
6019
6483
|
init_tickets();
|
|
6020
6484
|
init_waypoints();
|
|
@@ -6036,6 +6500,18 @@ var init_hooks = __esm(() => {
|
|
|
6036
6500
|
const session = getSessionRow(ctx, sessionId);
|
|
6037
6501
|
if (!session)
|
|
6038
6502
|
return c.json({});
|
|
6503
|
+
if (!session.featureId) {
|
|
6504
|
+
switch (event) {
|
|
6505
|
+
case "session-start":
|
|
6506
|
+
return c.json(handlePrepareSessionStart(ctx, session, body.payload));
|
|
6507
|
+
case "user-prompt":
|
|
6508
|
+
return c.json(handlePrepareUserPrompt(ctx, session, body.payload));
|
|
6509
|
+
case "session-end":
|
|
6510
|
+
return c.json(handlePrepareSessionEnd(ctx, session));
|
|
6511
|
+
default:
|
|
6512
|
+
return c.json({});
|
|
6513
|
+
}
|
|
6514
|
+
}
|
|
6039
6515
|
const feature = tryGetFeature(ctx, session.featureId);
|
|
6040
6516
|
if (!feature)
|
|
6041
6517
|
return c.json({});
|
|
@@ -6110,7 +6586,7 @@ var init_stream = __esm(() => {
|
|
|
6110
6586
|
// src/routes/web.ts
|
|
6111
6587
|
import { existsSync as existsSync13, statSync } from "fs";
|
|
6112
6588
|
import { readFile } from "fs/promises";
|
|
6113
|
-
import { extname, join as join12, normalize, resolve as
|
|
6589
|
+
import { extname, join as join12, normalize, resolve as resolve7, sep as sep2 } from "path";
|
|
6114
6590
|
function contentType(path) {
|
|
6115
6591
|
return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
6116
6592
|
}
|
|
@@ -6136,11 +6612,11 @@ function safeJoin(root, pathname) {
|
|
|
6136
6612
|
function resolveWebDist() {
|
|
6137
6613
|
const override = process.env.RUNCASTLE_WEB_DIST;
|
|
6138
6614
|
if (override)
|
|
6139
|
-
return
|
|
6140
|
-
return
|
|
6615
|
+
return resolve7(override);
|
|
6616
|
+
return resolve7(import.meta.dirname, "..", "..", "..", "..", "apps", "web", "dist");
|
|
6141
6617
|
}
|
|
6142
6618
|
function mountWebApp(app, distDir) {
|
|
6143
|
-
const root =
|
|
6619
|
+
const root = resolve7(distDir);
|
|
6144
6620
|
const indexHtml = join12(root, "index.html");
|
|
6145
6621
|
app.get("*", async (c, next) => {
|
|
6146
6622
|
const { pathname } = new URL(c.req.url);
|
|
@@ -6431,6 +6907,69 @@ function renderRevisitPrompt(feature) {
|
|
|
6431
6907
|
].join(`
|
|
6432
6908
|
`);
|
|
6433
6909
|
}
|
|
6910
|
+
function renderPreparePrompt(brief) {
|
|
6911
|
+
const { project, remainingKeys, established, notes } = brief;
|
|
6912
|
+
return [
|
|
6913
|
+
`# runcastle \u2014 preparing ${project.name}`,
|
|
6914
|
+
"",
|
|
6915
|
+
"This is a **preparation session**: a project-scoped conversation whose job is to",
|
|
6916
|
+
"establish the settings every later agent depends on, and to record them with evidence.",
|
|
6917
|
+
"There is no feature here and no pipeline to advance.",
|
|
6918
|
+
"",
|
|
6919
|
+
"## Where this runs \u2014 and why that matters",
|
|
6920
|
+
`You are on the developer's own machine, in \`${project.repoPath}\`, NOT in a sandbox.`,
|
|
6921
|
+
"A headless preparation run measures the repo in a throwaway container, which means it",
|
|
6922
|
+
"can never verify anything about *this* machine: the dev server, the local database,",
|
|
6923
|
+
"docker, credentials. Those keys \u2014 `devCommand`, `driveSetupCommand`, `driveStopCommand`,",
|
|
6924
|
+
"`driveEnv`, `dbResetCommand` \u2014 are exactly what it has to leave as proposals.",
|
|
6925
|
+
"You can actually run them. That is the point of this session.",
|
|
6926
|
+
"",
|
|
6927
|
+
"The same access is why you must **ask before you act**. Anything that starts or stops a",
|
|
6928
|
+
"service, creates or migrates a database, or writes outside the repo needs the human to",
|
|
6929
|
+
"agree first \u2014 say what you are about to run and why, then wait.",
|
|
6930
|
+
"",
|
|
6931
|
+
...established.length > 0 ? [
|
|
6932
|
+
"## Already established \u2014 do not re-derive",
|
|
6933
|
+
"These are recorded findings. Treat them as true unless the human says otherwise;",
|
|
6934
|
+
"replacing a measured value with a fresh guess makes preparation worse, not better.",
|
|
6935
|
+
"",
|
|
6936
|
+
...established.map((f) => `- \`${f.key}\` (${f.source})${f.evidence ? ` \u2014 ${f.evidence}` : ""}`),
|
|
6937
|
+
""
|
|
6938
|
+
] : [],
|
|
6939
|
+
...notes ? [
|
|
6940
|
+
"## What the last run could not settle",
|
|
6941
|
+
"Its own words. Where it says it declined to guess, it was right to \u2014 that is the",
|
|
6942
|
+
"gap you are here to close, and closing it usually means asking one direct question.",
|
|
6943
|
+
"",
|
|
6944
|
+
notes,
|
|
6945
|
+
""
|
|
6946
|
+
] : [],
|
|
6947
|
+
"## Still open",
|
|
6948
|
+
remainingKeys.length > 0 ? remainingKeys.map((k) => `- \`${k}\``).join(`
|
|
6949
|
+
`) : "_Nothing is unset. Confirm the existing values still hold, then say so and stop._",
|
|
6950
|
+
"",
|
|
6951
|
+
"## Recording what you establish",
|
|
6952
|
+
"- `record_finding({ key, value, evidence, userSupplied })` \u2014 one call per key.",
|
|
6953
|
+
"- `userSupplied: true` means the human GAVE you this value or confirmed it verbatim.",
|
|
6954
|
+
" That marks it as theirs and permanently stops automatic runs from overwriting it.",
|
|
6955
|
+
"- Leave it false for anything you worked out yourself, even with them watching \u2014",
|
|
6956
|
+
" that stays improvable by a later run. Getting this backwards silently retires a",
|
|
6957
|
+
" field from preparation forever, and the only way back is the human clearing it.",
|
|
6958
|
+
"- `evidence` is not optional in spirit: record what you ran and what it printed, or",
|
|
6959
|
+
" what the human told you. A value with no account of itself is a guess with a source field.",
|
|
6960
|
+
"",
|
|
6961
|
+
"## Secrets",
|
|
6962
|
+
"This is a development environment and the human has agreed to supply real connection",
|
|
6963
|
+
"strings and credentials here. Store them as given. Do not paste a secret into a timeline",
|
|
6964
|
+
"note or a commit message \u2014 `record_finding` is the only place a value belongs.",
|
|
6965
|
+
"",
|
|
6966
|
+
"## Your task",
|
|
6967
|
+
"Open by telling the human which fields are still open and what you need from them for",
|
|
6968
|
+
"each. Work them one at a time: propose, ask, run it if they agree, then record it.",
|
|
6969
|
+
""
|
|
6970
|
+
].join(`
|
|
6971
|
+
`);
|
|
6972
|
+
}
|
|
6434
6973
|
function renderSettings(hookClient) {
|
|
6435
6974
|
const cmd = (event) => ({
|
|
6436
6975
|
type: "command",
|
|
@@ -6461,13 +7000,16 @@ function renderMcpConfig(session, config2) {
|
|
|
6461
7000
|
};
|
|
6462
7001
|
}
|
|
6463
7002
|
async function writeSessionArtifacts(input) {
|
|
6464
|
-
const { session, feature, config: config2, waypoint } = input;
|
|
7003
|
+
const { session, feature, config: config2, waypoint, prepare } = input;
|
|
6465
7004
|
const dir = sessionDir(session.id);
|
|
6466
7005
|
mkdirSync7(dir, { recursive: true });
|
|
6467
7006
|
const systemPromptPath = join13(dir, "system-prompt.md");
|
|
6468
7007
|
const settingsPath = join13(dir, "settings.json");
|
|
6469
7008
|
const mcpConfigPath = join13(dir, "mcp.json");
|
|
6470
|
-
|
|
7009
|
+
const systemPrompt = feature ? renderSystemPrompt(feature, session.kind, waypoint) : prepare ? renderPreparePrompt(prepare) : (() => {
|
|
7010
|
+
throw new Error(`session ${session.id} has neither a feature nor a preparation brief`);
|
|
7011
|
+
})();
|
|
7012
|
+
writeFileSync2(systemPromptPath, systemPrompt, "utf8");
|
|
6471
7013
|
writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath()), null, 2), "utf8");
|
|
6472
7014
|
writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
|
|
6473
7015
|
return { systemPromptPath, settingsPath, mcpConfigPath };
|
|
@@ -6485,7 +7027,8 @@ var init_artifacts = __esm(() => {
|
|
|
6485
7027
|
"mcp__runcastle__complete_phase",
|
|
6486
7028
|
"mcp__runcastle__escalate_to_map",
|
|
6487
7029
|
"mcp__runcastle__emit_waypoints",
|
|
6488
|
-
"mcp__runcastle__resolve_waypoint"
|
|
7030
|
+
"mcp__runcastle__resolve_waypoint",
|
|
7031
|
+
"mcp__runcastle__record_finding"
|
|
6489
7032
|
];
|
|
6490
7033
|
SESSION_BASH_ALLOW_RULES = [
|
|
6491
7034
|
"Bash(git status:*)",
|
|
@@ -6502,9 +7045,9 @@ var init_artifacts = __esm(() => {
|
|
|
6502
7045
|
|
|
6503
7046
|
// src/launcher/launcher.ts
|
|
6504
7047
|
import { existsSync as existsSync14 } from "fs";
|
|
6505
|
-
import { dirname as dirname6, join as join14, resolve as
|
|
7048
|
+
import { dirname as dirname6, join as join14, resolve as resolve8 } from "path";
|
|
6506
7049
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
6507
|
-
import { and as
|
|
7050
|
+
import { and as and8, eq as eq10 } from "drizzle-orm";
|
|
6508
7051
|
function ptyExitMessage(exitCode) {
|
|
6509
7052
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
6510
7053
|
return `terminal exited (code ${label})`;
|
|
@@ -6518,7 +7061,7 @@ function buildClaudeArgs(input) {
|
|
|
6518
7061
|
input.settingsPath,
|
|
6519
7062
|
"--mcp-config",
|
|
6520
7063
|
input.mcpConfigPath,
|
|
6521
|
-
"--strict-mcp-config",
|
|
7064
|
+
...input.strictMcp ? ["--strict-mcp-config"] : [],
|
|
6522
7065
|
"--plugin-dir",
|
|
6523
7066
|
input.pluginDir,
|
|
6524
7067
|
"--append-system-prompt-file",
|
|
@@ -6539,7 +7082,7 @@ function resolvePluginDir(fromDir = dirname6(fileURLToPath6(import.meta.url))) {
|
|
|
6539
7082
|
const rel = join14("packages", "skills", "packs", "runcastle");
|
|
6540
7083
|
const override = process.env[SKILLS_DIR_ENV];
|
|
6541
7084
|
if (override) {
|
|
6542
|
-
const dir2 = join14(
|
|
7085
|
+
const dir2 = join14(resolve8(override), "packs", "runcastle");
|
|
6543
7086
|
if (existsSync14(dir2))
|
|
6544
7087
|
return dir2;
|
|
6545
7088
|
throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
|
|
@@ -6561,7 +7104,7 @@ function resolvePluginDir(fromDir = dirname6(fileURLToPath6(import.meta.url))) {
|
|
|
6561
7104
|
`)}`);
|
|
6562
7105
|
}
|
|
6563
7106
|
function activeRunFor(ctx, featureId) {
|
|
6564
|
-
const row = ctx.db.select().from(runs).where(
|
|
7107
|
+
const row = ctx.db.select().from(runs).where(and8(eq10(runs.featureId, featureId), eq10(runs.status, "running"))).limit(1).get();
|
|
6565
7108
|
return row ? rowToRun(row) : null;
|
|
6566
7109
|
}
|
|
6567
7110
|
function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
@@ -6574,6 +7117,29 @@ function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
|
6574
7117
|
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
7118
|
}
|
|
6576
7119
|
}
|
|
7120
|
+
function sessionFinished(ctx, feature, session) {
|
|
7121
|
+
if (session.kind !== "waypoint")
|
|
7122
|
+
return feature.mapped;
|
|
7123
|
+
const own = listByFeature(ctx, feature.id).find((w) => w.lastSessionId === session.id);
|
|
7124
|
+
return !!own && (own.status === "resolved" || own.status === "dropped");
|
|
7125
|
+
}
|
|
7126
|
+
function sweepActiveSessions(ctx, feature, endLive) {
|
|
7127
|
+
for (const session of activeSessionsForFeature(ctx, feature.id)) {
|
|
7128
|
+
const finished = sessionFinished(ctx, feature, session);
|
|
7129
|
+
if (!finished && !endLive)
|
|
7130
|
+
continue;
|
|
7131
|
+
endSession(ctx, session.id);
|
|
7132
|
+
emit(ctx, feature.id, {
|
|
7133
|
+
type: "session.auto_ended",
|
|
7134
|
+
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`,
|
|
7135
|
+
data: {
|
|
7136
|
+
sessionId: session.id,
|
|
7137
|
+
kind: session.kind,
|
|
7138
|
+
reason: finished ? "finished" : "abandoned"
|
|
7139
|
+
}
|
|
7140
|
+
});
|
|
7141
|
+
}
|
|
7142
|
+
}
|
|
6577
7143
|
async function ensureWorktree(ctx, project, feature) {
|
|
6578
7144
|
try {
|
|
6579
7145
|
return await ensureTalkWorktree(project, feature);
|
|
@@ -6681,7 +7247,8 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
6681
7247
|
mcpConfigPath: artifacts.mcpConfigPath,
|
|
6682
7248
|
systemPromptPath: artifacts.systemPromptPath,
|
|
6683
7249
|
model: resolveModel(input.kind, ctx.config, project),
|
|
6684
|
-
resumeSessionId
|
|
7250
|
+
resumeSessionId,
|
|
7251
|
+
strictMcp: ctx.config.sessionMcp === "runcastleOnly"
|
|
6685
7252
|
};
|
|
6686
7253
|
if (opts.spawn === false) {
|
|
6687
7254
|
emit(ctx, feature.id, {
|
|
@@ -6701,6 +7268,75 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
6701
7268
|
});
|
|
6702
7269
|
return { sessionId: session.id };
|
|
6703
7270
|
}
|
|
7271
|
+
async function launchPrepareSession(ctx, input, opts = {}) {
|
|
7272
|
+
const project = requireProjectById(ctx, input.projectId);
|
|
7273
|
+
const session = createSessionRow(ctx, {
|
|
7274
|
+
projectId: project.id,
|
|
7275
|
+
kind: "prepare",
|
|
7276
|
+
worktreePath: project.repoPath
|
|
7277
|
+
});
|
|
7278
|
+
const resumeSessionId = mostRecentResumableProjectSession(ctx, project.id, "prepare")?.ccSessionId;
|
|
7279
|
+
emitProject(ctx, project.id, {
|
|
7280
|
+
type: "session.launching",
|
|
7281
|
+
message: "launching preparation session",
|
|
7282
|
+
data: { sessionId: session.id, kind: "prepare", worktreePath: project.repoPath }
|
|
7283
|
+
});
|
|
7284
|
+
if (resumeSessionId) {
|
|
7285
|
+
emitProject(ctx, project.id, {
|
|
7286
|
+
type: "session.resumed",
|
|
7287
|
+
message: "resuming the previous preparation conversation",
|
|
7288
|
+
data: { sessionId: session.id, resumeSessionId }
|
|
7289
|
+
});
|
|
7290
|
+
}
|
|
7291
|
+
const artifacts = await writeSessionArtifacts({
|
|
7292
|
+
session,
|
|
7293
|
+
project,
|
|
7294
|
+
config: ctx.config,
|
|
7295
|
+
prepare: await buildPrepareBrief(ctx, project)
|
|
7296
|
+
});
|
|
7297
|
+
const serverUrl = serverUrlFor(ctx.config);
|
|
7298
|
+
const buildInput = {
|
|
7299
|
+
sessionId: session.id,
|
|
7300
|
+
serverUrl,
|
|
7301
|
+
featureTitle: `preparing ${project.name}`,
|
|
7302
|
+
worktreePath: project.repoPath,
|
|
7303
|
+
pluginDir: resolvePluginDir(),
|
|
7304
|
+
settingsPath: artifacts.settingsPath,
|
|
7305
|
+
mcpConfigPath: artifacts.mcpConfigPath,
|
|
7306
|
+
systemPromptPath: artifacts.systemPromptPath,
|
|
7307
|
+
model: resolveModel("prepare", ctx.config, project),
|
|
7308
|
+
resumeSessionId,
|
|
7309
|
+
strictMcp: ctx.config.sessionMcp === "runcastleOnly"
|
|
7310
|
+
};
|
|
7311
|
+
if (opts.spawn === false) {
|
|
7312
|
+
emitProject(ctx, project.id, {
|
|
7313
|
+
type: "session.launched",
|
|
7314
|
+
message: "preparation session prepared (terminal spawn skipped)",
|
|
7315
|
+
data: {
|
|
7316
|
+
sessionId: session.id,
|
|
7317
|
+
command: ["claude", ...buildClaudeArgs(buildInput)].join(" "),
|
|
7318
|
+
spawned: false
|
|
7319
|
+
}
|
|
7320
|
+
});
|
|
7321
|
+
return { sessionId: session.id };
|
|
7322
|
+
}
|
|
7323
|
+
spawnEmbeddedPty(ctx, undefined, session, project.repoPath, serverUrl, buildClaudeArgs(buildInput), { resumeSessionId });
|
|
7324
|
+
return { sessionId: session.id };
|
|
7325
|
+
}
|
|
7326
|
+
async function buildPrepareBrief(ctx, project) {
|
|
7327
|
+
const findings = await listFindings(ctx, project);
|
|
7328
|
+
const notes = latestPrepNotes(ctx, project.id);
|
|
7329
|
+
return {
|
|
7330
|
+
project,
|
|
7331
|
+
remainingKeys: keysToPrepare(ctx, project),
|
|
7332
|
+
established: findings.map((f) => ({
|
|
7333
|
+
key: f.key,
|
|
7334
|
+
source: f.source,
|
|
7335
|
+
...f.evidence ? { evidence: f.evidence } : {}
|
|
7336
|
+
})),
|
|
7337
|
+
...notes ? { notes } : {}
|
|
7338
|
+
};
|
|
7339
|
+
}
|
|
6704
7340
|
async function workWaypoint(ctx, input, opts = {}) {
|
|
6705
7341
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
6706
7342
|
if (!feature.mapped) {
|
|
@@ -6717,6 +7353,7 @@ async function workWaypoint(ctx, input, opts = {}) {
|
|
|
6717
7353
|
});
|
|
6718
7354
|
return { runId };
|
|
6719
7355
|
}
|
|
7356
|
+
sweepActiveSessions(ctx, feature, input.endLive === true);
|
|
6720
7357
|
assertSpawnable(ctx, feature);
|
|
6721
7358
|
return launchSession(ctx, { featureId: feature.id, kind: "waypoint", waypointId: wp.id }, opts);
|
|
6722
7359
|
}
|
|
@@ -6773,7 +7410,7 @@ function handlePtyExit(ctx, feature, session, meta, exitCode) {
|
|
|
6773
7410
|
releaseForSession(ctx, session.id);
|
|
6774
7411
|
if (diedBeforeLive && meta.resumeSessionId) {
|
|
6775
7412
|
const label = meta.waypoint ? `waypoint ${meta.waypoint.seq} (${meta.waypoint.title})` : session.kind;
|
|
6776
|
-
|
|
7413
|
+
emitForSession(ctx, session, {
|
|
6777
7414
|
type: "session.resume_failed",
|
|
6778
7415
|
message: `resume failed for ${label} \u2014 the session exited before starting (code ${exitCode ?? "unknown"}); the previous conversation is still resumable`,
|
|
6779
7416
|
data: {
|
|
@@ -6784,7 +7421,7 @@ function handlePtyExit(ctx, feature, session, meta, exitCode) {
|
|
|
6784
7421
|
}
|
|
6785
7422
|
});
|
|
6786
7423
|
}
|
|
6787
|
-
|
|
7424
|
+
emitForSession(ctx, session, {
|
|
6788
7425
|
type: "session.pty_exited",
|
|
6789
7426
|
message: ptyExitMessage(exitCode),
|
|
6790
7427
|
data: { sessionId: session.id, exitCode: exitCode ?? null }
|
|
@@ -6807,7 +7444,7 @@ function spawnEmbeddedPty(ctx, feature, session, worktreePath, serverUrl, claude
|
|
|
6807
7444
|
opts: { cwd: worktreePath, env, cols: 80, rows: 24, useConpty: true },
|
|
6808
7445
|
onExit: ({ exitCode }) => handlePtyExit(ctx, feature, session, meta, exitCode)
|
|
6809
7446
|
});
|
|
6810
|
-
|
|
7447
|
+
emitForSession(ctx, session, {
|
|
6811
7448
|
type: "session.launched",
|
|
6812
7449
|
message: "embedded terminal spawned",
|
|
6813
7450
|
data: { sessionId: session.id, mode: "embedded", pid: entry.pty.pid }
|
|
@@ -6816,7 +7453,7 @@ function spawnEmbeddedPty(ctx, feature, session, worktreePath, serverUrl, claude
|
|
|
6816
7453
|
} catch (err) {
|
|
6817
7454
|
markSessionEnded(ctx, session.id);
|
|
6818
7455
|
releaseForSession(ctx, session.id);
|
|
6819
|
-
|
|
7456
|
+
emitForSession(ctx, session, {
|
|
6820
7457
|
type: "session.spawn_failed",
|
|
6821
7458
|
message: `failed to spawn embedded terminal: ${err instanceof Error ? err.message : String(err)}`,
|
|
6822
7459
|
data: { sessionId: session.id, mode: "embedded" }
|
|
@@ -6831,6 +7468,7 @@ var init_launcher = __esm(() => {
|
|
|
6831
7468
|
init_skills_root();
|
|
6832
7469
|
init_schema();
|
|
6833
7470
|
init_errors();
|
|
7471
|
+
init_end_session();
|
|
6834
7472
|
init_registry();
|
|
6835
7473
|
init_events();
|
|
6836
7474
|
init_gates();
|
|
@@ -6839,6 +7477,8 @@ var init_launcher = __esm(() => {
|
|
|
6839
7477
|
init_tickets();
|
|
6840
7478
|
init_waypoints();
|
|
6841
7479
|
init_runner();
|
|
7480
|
+
init_findings();
|
|
7481
|
+
init_prep();
|
|
6842
7482
|
init_artifacts();
|
|
6843
7483
|
init_sessions();
|
|
6844
7484
|
init_sessions();
|
|
@@ -6861,6 +7501,7 @@ var gateId, featureRouter;
|
|
|
6861
7501
|
var init_feature = __esm(() => {
|
|
6862
7502
|
init_src();
|
|
6863
7503
|
init_launcher();
|
|
7504
|
+
init_sessions();
|
|
6864
7505
|
init_events();
|
|
6865
7506
|
init_features();
|
|
6866
7507
|
init_gates();
|
|
@@ -6882,8 +7523,16 @@ var init_feature = __esm(() => {
|
|
|
6882
7523
|
kind: SessionKind,
|
|
6883
7524
|
kickoffLine: z7.string().min(1).optional()
|
|
6884
7525
|
})).mutation(({ ctx, input }) => launchSession(ctx, input)),
|
|
6885
|
-
workWaypoint: publicProcedure.input(z7.object({ featureId: z7.string(), waypointId: z7.string() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
|
|
7526
|
+
workWaypoint: publicProcedure.input(z7.object({ featureId: z7.string(), waypointId: z7.string(), endLive: z7.boolean().optional() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
|
|
6886
7527
|
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 })),
|
|
7528
|
+
rethink: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => {
|
|
7529
|
+
const feature = rethink(ctx, input.featureId);
|
|
7530
|
+
return launchSession(ctx, {
|
|
7531
|
+
featureId: input.featureId,
|
|
7532
|
+
kind: "revisit",
|
|
7533
|
+
kickoffLine: lapKickoff(feature.lap)
|
|
7534
|
+
});
|
|
7535
|
+
}),
|
|
6887
7536
|
resendKickoff: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => resendKickoff(ctx, input.sessionId)),
|
|
6888
7537
|
endSession: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => endSession(ctx, input.sessionId)),
|
|
6889
7538
|
advance: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => advance(ctx, input.featureId)),
|
|
@@ -6923,7 +7572,7 @@ var init_feature = __esm(() => {
|
|
|
6923
7572
|
// src/services/fsbrowse.ts
|
|
6924
7573
|
import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
|
|
6925
7574
|
import { homedir as homedir2 } from "os";
|
|
6926
|
-
import { basename, dirname as dirname7, isAbsolute, join as join15, resolve as
|
|
7575
|
+
import { basename, dirname as dirname7, isAbsolute, join as join15, resolve as resolve9, sep as sep3 } from "path";
|
|
6927
7576
|
function expandPath(input, home = homedir2()) {
|
|
6928
7577
|
const trimmed = input.trim();
|
|
6929
7578
|
if (trimmed === "")
|
|
@@ -6933,7 +7582,7 @@ function expandPath(input, home = homedir2()) {
|
|
|
6933
7582
|
out = home;
|
|
6934
7583
|
else if (out.startsWith("~/") || out.startsWith("~\\"))
|
|
6935
7584
|
out = join15(home, out.slice(2));
|
|
6936
|
-
return
|
|
7585
|
+
return resolve9(out);
|
|
6937
7586
|
}
|
|
6938
7587
|
function looksLikeRepo(dir) {
|
|
6939
7588
|
try {
|
|
@@ -7086,6 +7735,7 @@ async function openProject(ctx, rawPath) {
|
|
|
7086
7735
|
message: `project ${existing.name} re-opened at ${repoPath} (${mainBranch})`
|
|
7087
7736
|
});
|
|
7088
7737
|
warnIfTranslatedMount(ctx, existing.id, repoPath);
|
|
7738
|
+
maybeAutoPrepare(ctx, reopened);
|
|
7089
7739
|
return reopened;
|
|
7090
7740
|
}
|
|
7091
7741
|
const row = {
|
|
@@ -7107,7 +7757,7 @@ async function openProject(ctx, rawPath) {
|
|
|
7107
7757
|
return project;
|
|
7108
7758
|
}
|
|
7109
7759
|
function maybeAutoPrepare(ctx, project) {
|
|
7110
|
-
if (!ctx
|
|
7760
|
+
if (!shouldAutoPrepare(ctx, project))
|
|
7111
7761
|
return;
|
|
7112
7762
|
startPrep(ctx, project.id).catch((e) => {
|
|
7113
7763
|
emitProject(ctx, project.id, {
|
|
@@ -7182,6 +7832,8 @@ var init_project = __esm(() => {
|
|
|
7182
7832
|
init_fsbrowse();
|
|
7183
7833
|
init_git();
|
|
7184
7834
|
init_prep();
|
|
7835
|
+
init_launcher();
|
|
7836
|
+
init_sessions();
|
|
7185
7837
|
init_projects();
|
|
7186
7838
|
init_repo();
|
|
7187
7839
|
init_context();
|
|
@@ -7204,6 +7856,8 @@ var init_project = __esm(() => {
|
|
|
7204
7856
|
});
|
|
7205
7857
|
return { prepId, keys };
|
|
7206
7858
|
}),
|
|
7859
|
+
talkToPrep: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ ctx, input }) => launchPrepareSession(ctx, { projectId: input.projectId })),
|
|
7860
|
+
prepSession: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => activeProjectSession(ctx, input.projectId, "prepare")),
|
|
7207
7861
|
cancelPrepare: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ input }) => {
|
|
7208
7862
|
cancelPrep(input.projectId);
|
|
7209
7863
|
return { ok: true };
|
|
@@ -7278,7 +7932,10 @@ function projectOverrides(ctx, projectId) {
|
|
|
7278
7932
|
setupCommand: projects.setupCommand,
|
|
7279
7933
|
verifyCommands: projects.verifyCommands,
|
|
7280
7934
|
knownFailures: projects.knownFailures,
|
|
7281
|
-
dbResetCommand: projects.dbResetCommand
|
|
7935
|
+
dbResetCommand: projects.dbResetCommand,
|
|
7936
|
+
driveSetupCommand: projects.driveSetupCommand,
|
|
7937
|
+
driveStopCommand: projects.driveStopCommand,
|
|
7938
|
+
driveEnv: projects.driveEnv
|
|
7282
7939
|
}).from(projects).where(eq12(projects.id, projectId)).get();
|
|
7283
7940
|
return {
|
|
7284
7941
|
model: row?.model ?? null,
|
|
@@ -7287,7 +7944,10 @@ function projectOverrides(ctx, projectId) {
|
|
|
7287
7944
|
setupCommand: row?.setupCommand ?? null,
|
|
7288
7945
|
verifyCommands: row?.verifyCommands ?? null,
|
|
7289
7946
|
knownFailures: row?.knownFailures ?? null,
|
|
7290
|
-
dbResetCommand: row?.dbResetCommand ?? null
|
|
7947
|
+
dbResetCommand: row?.dbResetCommand ?? null,
|
|
7948
|
+
driveSetupCommand: row?.driveSetupCommand ?? null,
|
|
7949
|
+
driveStopCommand: row?.driveStopCommand ?? null,
|
|
7950
|
+
driveEnv: row?.driveEnv ?? null
|
|
7291
7951
|
};
|
|
7292
7952
|
}
|
|
7293
7953
|
function resolveField(desc5, layers) {
|
|
@@ -7473,6 +8133,14 @@ var init_settings = __esm(() => {
|
|
|
7473
8133
|
valueSchema: z10.string().min(1),
|
|
7474
8134
|
parseEnv: idEnv
|
|
7475
8135
|
},
|
|
8136
|
+
{
|
|
8137
|
+
key: "sessionMcp",
|
|
8138
|
+
configKey: "sessionMcp",
|
|
8139
|
+
envVar: "RUNCASTLE_SESSION_MCP",
|
|
8140
|
+
restartRequired: false,
|
|
8141
|
+
valueSchema: z10.enum(["inherit", "runcastleOnly"]),
|
|
8142
|
+
parseEnv: idEnv
|
|
8143
|
+
},
|
|
7476
8144
|
{
|
|
7477
8145
|
key: "burnConcurrency",
|
|
7478
8146
|
configKey: "burnConcurrency",
|
|
@@ -7569,6 +8237,27 @@ var init_settings = __esm(() => {
|
|
|
7569
8237
|
restartRequired: false,
|
|
7570
8238
|
valueSchema: z10.string().min(1),
|
|
7571
8239
|
parseEnv: idEnv
|
|
8240
|
+
},
|
|
8241
|
+
{
|
|
8242
|
+
key: "driveSetupCommand",
|
|
8243
|
+
projectColumn: "driveSetupCommand",
|
|
8244
|
+
restartRequired: false,
|
|
8245
|
+
valueSchema: z10.string().min(1),
|
|
8246
|
+
parseEnv: idEnv
|
|
8247
|
+
},
|
|
8248
|
+
{
|
|
8249
|
+
key: "driveStopCommand",
|
|
8250
|
+
projectColumn: "driveStopCommand",
|
|
8251
|
+
restartRequired: false,
|
|
8252
|
+
valueSchema: z10.string().min(1),
|
|
8253
|
+
parseEnv: idEnv
|
|
8254
|
+
},
|
|
8255
|
+
{
|
|
8256
|
+
key: "driveEnv",
|
|
8257
|
+
projectColumn: "driveEnv",
|
|
8258
|
+
restartRequired: false,
|
|
8259
|
+
valueSchema: z10.string().min(1),
|
|
8260
|
+
parseEnv: idEnv
|
|
7572
8261
|
}
|
|
7573
8262
|
];
|
|
7574
8263
|
DEFAULTS = RuncastleConfig.parse({});
|
|
@@ -7750,13 +8439,13 @@ var init_doctor = __esm(() => {
|
|
|
7750
8439
|
});
|
|
7751
8440
|
|
|
7752
8441
|
// src/doctor/system-exec.ts
|
|
7753
|
-
import { spawn as
|
|
8442
|
+
import { spawn as spawn3 } from "child_process";
|
|
7754
8443
|
function createSystemExec(opts = {}) {
|
|
7755
|
-
return (command, args) => new Promise((
|
|
8444
|
+
return (command, args) => new Promise((resolve10) => {
|
|
7756
8445
|
const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
|
|
7757
8446
|
let stdout = "";
|
|
7758
8447
|
let stderr = "";
|
|
7759
|
-
const child =
|
|
8448
|
+
const child = spawn3(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
|
|
7760
8449
|
child.stdout?.on("data", (d) => {
|
|
7761
8450
|
stdout += d.toString();
|
|
7762
8451
|
});
|
|
@@ -7764,10 +8453,10 @@ function createSystemExec(opts = {}) {
|
|
|
7764
8453
|
stderr += d.toString();
|
|
7765
8454
|
});
|
|
7766
8455
|
child.on("error", (err) => {
|
|
7767
|
-
|
|
8456
|
+
resolve10({ ok: false, code: null, stdout, stderr: stderr || String(err) });
|
|
7768
8457
|
});
|
|
7769
8458
|
child.on("close", (code) => {
|
|
7770
|
-
|
|
8459
|
+
resolve10({ ok: true, code: code ?? 0, stdout, stderr });
|
|
7771
8460
|
});
|
|
7772
8461
|
});
|
|
7773
8462
|
}
|
|
@@ -8216,7 +8905,7 @@ import { Hono as Hono4 } from "hono";
|
|
|
8216
8905
|
function buildApp(ctx) {
|
|
8217
8906
|
const app = new Hono4;
|
|
8218
8907
|
setRuntimeCtx(ctx);
|
|
8219
|
-
app.get("/health", (c) => c.json({ ok: true }));
|
|
8908
|
+
app.get("/health", (c) => c.json({ ok: true, dataDir: dataDir() }));
|
|
8220
8909
|
app.use("/api/trpc/*", async (c, next) => {
|
|
8221
8910
|
await next();
|
|
8222
8911
|
if (c.res.headers.get("content-type") === "application/json") {
|
|
@@ -8261,7 +8950,7 @@ async function startServer() {
|
|
|
8261
8950
|
},
|
|
8262
8951
|
websocket: terminalWebSocket
|
|
8263
8952
|
});
|
|
8264
|
-
console.log(`runcastle server listening on http://localhost:${config2.serverPort}`);
|
|
8953
|
+
console.log(`runcastle${process.env.RUNCASTLE_DEV ? " [dev]" : ""} server listening on ` + `http://localhost:${config2.serverPort} \u2014 data dir: ${dataDir()}`);
|
|
8265
8954
|
const SHUTDOWN_KEY = Symbol.for("runcastle.shutdown.wired");
|
|
8266
8955
|
const g = globalThis;
|
|
8267
8956
|
if (!g[SHUTDOWN_KEY]) {
|