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