runcastle 1.0.24 → 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/runcastle.js +2413 -895
- package/drizzle/0010_misty_ken_ellis.sql +24 -0
- package/drizzle/0011_steep_ted_forrester.sql +2 -0
- package/drizzle/0012_greedy_lionheart.sql +1 -0
- package/drizzle/0013_sudden_naoko.sql +21 -0
- package/drizzle/meta/0010_snapshot.json +757 -0
- package/drizzle/meta/0011_snapshot.json +771 -0
- package/drizzle/meta/0012_snapshot.json +778 -0
- package/drizzle/meta/0013_snapshot.json +785 -0
- package/drizzle/meta/_journal.json +28 -0
- package/index.js +2407 -889
- package/package.json +1 -1
- package/skills/burner/prepare-project.md +103 -0
- package/skills/packs/README.md +1 -1
- package/skills/packs/runcastle/skills/tickets/SKILL.md +22 -6
- package/web/assets/index-BLrA7l4N.css +1 -0
- package/web/assets/index-BcwSVMee.js +138 -0
- package/web/index.html +2 -2
- package/web/assets/index-Cpwb7Nqn.js +0 -110
- package/web/assets/index-DBVAMsFt.css +0 -1
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);
|
|
@@ -69,6 +91,20 @@ import * as z from "zod";
|
|
|
69
91
|
function resolveSandboxImage(config) {
|
|
70
92
|
return config.sandboxImage ?? DEFAULT_SANDBOX_IMAGE;
|
|
71
93
|
}
|
|
94
|
+
function resolvePreparedSettings(config, project) {
|
|
95
|
+
const pick = (a, b) => {
|
|
96
|
+
const own = a?.trim();
|
|
97
|
+
if (own)
|
|
98
|
+
return own;
|
|
99
|
+
const inherited = b?.trim();
|
|
100
|
+
return inherited || undefined;
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
setupCommand: pick(project?.setupCommand, config.setupCommand),
|
|
104
|
+
verifyCommands: pick(project?.verifyCommands, config.verifyCommands),
|
|
105
|
+
knownFailures: pick(project?.knownFailures, config.knownFailures)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
72
108
|
function resolveModel(step, config, project, runOverride) {
|
|
73
109
|
return runOverride ?? config.stepModels[step] ?? project?.model ?? config.model;
|
|
74
110
|
}
|
|
@@ -94,6 +130,7 @@ var init_config = __esm(() => {
|
|
|
94
130
|
"revisit",
|
|
95
131
|
"research",
|
|
96
132
|
"implement",
|
|
133
|
+
"prepare",
|
|
97
134
|
"smoke"
|
|
98
135
|
];
|
|
99
136
|
ModelStep = z.enum(MODEL_STEPS);
|
|
@@ -102,6 +139,7 @@ var init_config = __esm(() => {
|
|
|
102
139
|
model: z.string().default("claude-opus-5"),
|
|
103
140
|
stepModels: z.partialRecord(ModelStep, z.string()).default({ smoke: DEFAULT_SMOKE_MODEL }),
|
|
104
141
|
sandbox: z.enum(["docker", "podman", "noSandbox"]).default("docker"),
|
|
142
|
+
sessionMcp: z.enum(["inherit", "runcastleOnly"]).default("inherit"),
|
|
105
143
|
mainBranch: z.string().default("main"),
|
|
106
144
|
sandboxImage: z.string().optional(),
|
|
107
145
|
burnConcurrency: z.number().int().min(1).max(8).default(3),
|
|
@@ -110,6 +148,7 @@ var init_config = __esm(() => {
|
|
|
110
148
|
burnMaxIterations: z.number().int().min(1).max(10).default(3),
|
|
111
149
|
burnAttempts: z.number().int().min(1).max(5).default(3),
|
|
112
150
|
burnConflictAttempts: z.number().int().min(0).max(3).default(2),
|
|
151
|
+
autoPrepare: z.boolean().default(true),
|
|
113
152
|
setupCommand: z.string().optional(),
|
|
114
153
|
verifyCommands: z.string().optional(),
|
|
115
154
|
knownFailures: z.string().optional(),
|
|
@@ -142,6 +181,8 @@ function loadConfig(env = process.env) {
|
|
|
142
181
|
overrides.smokeModel = env.RUNCASTLE_SMOKE_MODEL;
|
|
143
182
|
if (env.RUNCASTLE_SANDBOX)
|
|
144
183
|
overrides.sandbox = env.RUNCASTLE_SANDBOX;
|
|
184
|
+
if (env.RUNCASTLE_SESSION_MCP)
|
|
185
|
+
overrides.sessionMcp = env.RUNCASTLE_SESSION_MCP;
|
|
145
186
|
if (env.RUNCASTLE_MAIN_BRANCH)
|
|
146
187
|
overrides.mainBranch = env.RUNCASTLE_MAIN_BRANCH;
|
|
147
188
|
if (env.RUNCASTLE_SANDBOX_IMAGE)
|
|
@@ -203,7 +244,7 @@ var init_ids = () => {};
|
|
|
203
244
|
|
|
204
245
|
// ../core/src/schemas.ts
|
|
205
246
|
import * as z2 from "zod";
|
|
206
|
-
var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, Project, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
247
|
+
var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, PREPARED_KEYS, PreparedKey, FindingSource, Project, ProjectFinding, PrepStatus, PrepRun, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
207
248
|
var init_schemas = __esm(() => {
|
|
208
249
|
Phase = z2.enum([
|
|
209
250
|
"ideation",
|
|
@@ -214,7 +255,7 @@ var init_schemas = __esm(() => {
|
|
|
214
255
|
"shipped"
|
|
215
256
|
]);
|
|
216
257
|
TicketStatus = z2.enum(["pending", "burning", "done", "failed", "cancelled"]);
|
|
217
|
-
SessionKind = z2.enum(["ideation", "qa", "waypoint", "converge", "revisit"]);
|
|
258
|
+
SessionKind = z2.enum(["ideation", "qa", "waypoint", "converge", "revisit", "prepare"]);
|
|
218
259
|
RunStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
|
|
219
260
|
FeatureStatus = z2.enum(["active", "shipped", "archived"]);
|
|
220
261
|
SessionStatus = z2.enum(["launching", "live", "ended"]);
|
|
@@ -256,13 +297,50 @@ var init_schemas = __esm(() => {
|
|
|
256
297
|
lastSessionId: z2.string().optional(),
|
|
257
298
|
summary: z2.string().optional()
|
|
258
299
|
});
|
|
300
|
+
PREPARED_KEYS = [
|
|
301
|
+
"setupCommand",
|
|
302
|
+
"verifyCommands",
|
|
303
|
+
"knownFailures",
|
|
304
|
+
"devCommand",
|
|
305
|
+
"driveSetupCommand",
|
|
306
|
+
"driveStopCommand",
|
|
307
|
+
"driveEnv",
|
|
308
|
+
"dbResetCommand"
|
|
309
|
+
];
|
|
310
|
+
PreparedKey = z2.enum(PREPARED_KEYS);
|
|
311
|
+
FindingSource = z2.enum(["prep", "human", "session"]);
|
|
259
312
|
Project = z2.object({
|
|
260
313
|
id: z2.string(),
|
|
261
314
|
name: z2.string(),
|
|
262
315
|
repoPath: z2.string(),
|
|
263
316
|
mainBranch: z2.string(),
|
|
264
317
|
devCommand: z2.string().optional(),
|
|
265
|
-
model: z2.string().optional()
|
|
318
|
+
model: z2.string().optional(),
|
|
319
|
+
setupCommand: z2.string().optional(),
|
|
320
|
+
verifyCommands: z2.string().optional(),
|
|
321
|
+
knownFailures: z2.string().optional(),
|
|
322
|
+
dbResetCommand: z2.string().optional(),
|
|
323
|
+
driveSetupCommand: z2.string().optional(),
|
|
324
|
+
driveStopCommand: z2.string().optional(),
|
|
325
|
+
driveEnv: z2.string().optional()
|
|
326
|
+
});
|
|
327
|
+
ProjectFinding = z2.object({
|
|
328
|
+
key: PreparedKey,
|
|
329
|
+
source: FindingSource,
|
|
330
|
+
evidence: z2.string().optional(),
|
|
331
|
+
establishedAt: z2.number(),
|
|
332
|
+
establishedSha: z2.string().optional(),
|
|
333
|
+
staleCommits: z2.number().optional()
|
|
334
|
+
});
|
|
335
|
+
PrepStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
|
|
336
|
+
PrepRun = z2.object({
|
|
337
|
+
id: z2.string(),
|
|
338
|
+
projectId: z2.string(),
|
|
339
|
+
status: PrepStatus,
|
|
340
|
+
startedAt: z2.number(),
|
|
341
|
+
endedAt: z2.number().optional(),
|
|
342
|
+
summary: z2.string().optional(),
|
|
343
|
+
headSha: z2.string().optional()
|
|
266
344
|
});
|
|
267
345
|
Feature = z2.object({
|
|
268
346
|
id: z2.string(),
|
|
@@ -279,7 +357,8 @@ var init_schemas = __esm(() => {
|
|
|
279
357
|
});
|
|
280
358
|
SessionRow = z2.object({
|
|
281
359
|
id: z2.string(),
|
|
282
|
-
featureId: z2.string(),
|
|
360
|
+
featureId: z2.string().optional(),
|
|
361
|
+
projectId: z2.string().optional(),
|
|
283
362
|
kind: SessionKind,
|
|
284
363
|
ccSessionId: z2.string().optional(),
|
|
285
364
|
transcriptPath: z2.string().optional(),
|
|
@@ -461,8 +540,8 @@ var init_pipeline = __esm(() => {
|
|
|
461
540
|
});
|
|
462
541
|
|
|
463
542
|
// ../core/src/db-schema.ts
|
|
464
|
-
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
465
|
-
var projects, features, sessions, tickets, waypoints, runs, events, gateOverrides;
|
|
543
|
+
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
544
|
+
var projects, projectFindings, projectPreps, features, sessions, tickets, waypoints, runs, events, gateOverrides;
|
|
466
545
|
var init_db_schema = __esm(() => {
|
|
467
546
|
projects = sqliteTable("projects", {
|
|
468
547
|
id: text("id").primaryKey(),
|
|
@@ -472,7 +551,31 @@ var init_db_schema = __esm(() => {
|
|
|
472
551
|
devCommand: text("dev_command"),
|
|
473
552
|
model: text("model"),
|
|
474
553
|
sandbox: text("sandbox"),
|
|
475
|
-
closedAt: integer("closed_at")
|
|
554
|
+
closedAt: integer("closed_at"),
|
|
555
|
+
setupCommand: text("setup_command"),
|
|
556
|
+
verifyCommands: text("verify_commands"),
|
|
557
|
+
knownFailures: text("known_failures"),
|
|
558
|
+
dbResetCommand: text("db_reset_command"),
|
|
559
|
+
driveSetupCommand: text("drive_setup_command"),
|
|
560
|
+
driveStopCommand: text("drive_stop_command"),
|
|
561
|
+
driveEnv: text("drive_env")
|
|
562
|
+
});
|
|
563
|
+
projectFindings = sqliteTable("project_findings", {
|
|
564
|
+
projectId: text("project_id").notNull(),
|
|
565
|
+
key: text("key").notNull(),
|
|
566
|
+
source: text("source").notNull().$type(),
|
|
567
|
+
evidence: text("evidence"),
|
|
568
|
+
establishedAt: integer("established_at").notNull(),
|
|
569
|
+
establishedSha: text("established_sha")
|
|
570
|
+
}, (t) => [primaryKey({ columns: [t.projectId, t.key] })]);
|
|
571
|
+
projectPreps = sqliteTable("project_preps", {
|
|
572
|
+
id: text("id").primaryKey(),
|
|
573
|
+
projectId: text("project_id").notNull(),
|
|
574
|
+
status: text("status").notNull().$type(),
|
|
575
|
+
startedAt: integer("started_at").notNull(),
|
|
576
|
+
endedAt: integer("ended_at"),
|
|
577
|
+
summary: text("summary"),
|
|
578
|
+
headSha: text("head_sha")
|
|
476
579
|
});
|
|
477
580
|
features = sqliteTable("features", {
|
|
478
581
|
id: text("id").primaryKey(),
|
|
@@ -489,7 +592,8 @@ var init_db_schema = __esm(() => {
|
|
|
489
592
|
});
|
|
490
593
|
sessions = sqliteTable("sessions", {
|
|
491
594
|
id: text("id").primaryKey(),
|
|
492
|
-
featureId: text("feature_id")
|
|
595
|
+
featureId: text("feature_id"),
|
|
596
|
+
projectId: text("project_id"),
|
|
493
597
|
kind: text("kind").notNull().$type(),
|
|
494
598
|
ccSessionId: text("cc_session_id"),
|
|
495
599
|
transcriptPath: text("transcript_path"),
|
|
@@ -576,7 +680,9 @@ var init_schema = __esm(() => {
|
|
|
576
680
|
waypoints,
|
|
577
681
|
runs,
|
|
578
682
|
events,
|
|
579
|
-
gateOverrides
|
|
683
|
+
gateOverrides,
|
|
684
|
+
projectFindings,
|
|
685
|
+
projectPreps
|
|
580
686
|
};
|
|
581
687
|
});
|
|
582
688
|
|
|
@@ -599,11 +705,11 @@ var init_client = __esm(() => {
|
|
|
599
705
|
|
|
600
706
|
// src/launcher/asset-paths.ts
|
|
601
707
|
import { existsSync as existsSync2 } from "fs";
|
|
602
|
-
import { join as join3, resolve } from "path";
|
|
708
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
603
709
|
function resolveAsset(envVar, fallback) {
|
|
604
710
|
const override = process.env[envVar];
|
|
605
711
|
if (override) {
|
|
606
|
-
const abs =
|
|
712
|
+
const abs = resolve2(override);
|
|
607
713
|
if (existsSync2(abs))
|
|
608
714
|
return abs;
|
|
609
715
|
throw new Error(`${envVar}=${override} does not exist (resolved ${abs})`);
|
|
@@ -1283,6 +1389,13 @@ function emit(ctx, featureId, e) {
|
|
|
1283
1389
|
function emitProject(ctx, projectId, e) {
|
|
1284
1390
|
return insertEvent(ctx, projectId, null, e);
|
|
1285
1391
|
}
|
|
1392
|
+
function emitForSession(ctx, session, e) {
|
|
1393
|
+
if (session.featureId)
|
|
1394
|
+
return emit(ctx, session.featureId, e);
|
|
1395
|
+
if (session.projectId)
|
|
1396
|
+
return emitProject(ctx, session.projectId, e);
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1286
1399
|
function insertEvent(ctx, projectId, featureId, e) {
|
|
1287
1400
|
const row = ctx.db.insert(events).values({
|
|
1288
1401
|
projectId,
|
|
@@ -1341,7 +1454,14 @@ function rowToProject(row) {
|
|
|
1341
1454
|
repoPath: row.repoPath,
|
|
1342
1455
|
mainBranch: row.mainBranch,
|
|
1343
1456
|
devCommand: row.devCommand ?? undefined,
|
|
1344
|
-
model: row.model ?? undefined
|
|
1457
|
+
model: row.model ?? undefined,
|
|
1458
|
+
setupCommand: row.setupCommand ?? undefined,
|
|
1459
|
+
verifyCommands: row.verifyCommands ?? undefined,
|
|
1460
|
+
knownFailures: row.knownFailures ?? undefined,
|
|
1461
|
+
dbResetCommand: row.dbResetCommand ?? undefined,
|
|
1462
|
+
driveSetupCommand: row.driveSetupCommand ?? undefined,
|
|
1463
|
+
driveStopCommand: row.driveStopCommand ?? undefined,
|
|
1464
|
+
driveEnv: row.driveEnv ?? undefined
|
|
1345
1465
|
};
|
|
1346
1466
|
}
|
|
1347
1467
|
function rowToRun(row) {
|
|
@@ -1358,7 +1478,8 @@ function rowToRun(row) {
|
|
|
1358
1478
|
function rowToSession(row) {
|
|
1359
1479
|
return {
|
|
1360
1480
|
id: row.id,
|
|
1361
|
-
featureId: row.featureId,
|
|
1481
|
+
featureId: row.featureId ?? undefined,
|
|
1482
|
+
projectId: row.projectId ?? undefined,
|
|
1362
1483
|
kind: row.kind,
|
|
1363
1484
|
ccSessionId: row.ccSessionId ?? undefined,
|
|
1364
1485
|
transcriptPath: row.transcriptPath ?? undefined,
|
|
@@ -1575,7 +1696,7 @@ function releaseForSession(ctx, claimant) {
|
|
|
1575
1696
|
const held = ctx.db.select().from(waypoints).where(and3(eq3(waypoints.claimedBy, claimant), eq3(waypoints.status, "claimed"))).all().map(rowToWaypoint);
|
|
1576
1697
|
return held.map((w) => release(ctx, w.id));
|
|
1577
1698
|
}
|
|
1578
|
-
function
|
|
1699
|
+
function resolve3(ctx, id, disposition, summary) {
|
|
1579
1700
|
const wp = getWaypoint(ctx, id);
|
|
1580
1701
|
const before = listByFeature(ctx, wp.featureId);
|
|
1581
1702
|
const bySeqBefore = new Map(before.map((w) => [w.seq, w]));
|
|
@@ -1618,7 +1739,8 @@ import { and as and4, desc as desc2, eq as eq4, inArray, isNotNull, sql as sql2
|
|
|
1618
1739
|
function createSessionRow(ctx, input) {
|
|
1619
1740
|
const row = ctx.db.insert(sessions).values({
|
|
1620
1741
|
id: newId("sess"),
|
|
1621
|
-
featureId: input.featureId,
|
|
1742
|
+
featureId: input.featureId ?? null,
|
|
1743
|
+
projectId: input.projectId ?? null,
|
|
1622
1744
|
kind: input.kind,
|
|
1623
1745
|
ccSessionId: null,
|
|
1624
1746
|
transcriptPath: null,
|
|
@@ -1673,34 +1795,159 @@ function writeKickoffSequence(line, io, submitDelayMs = KICKOFF_SUBMIT_DELAY_MS)
|
|
|
1673
1795
|
}, submitDelayMs);
|
|
1674
1796
|
submit.unref?.();
|
|
1675
1797
|
}
|
|
1676
|
-
function
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1798
|
+
function stopTimers(d) {
|
|
1799
|
+
for (const t of d.timers)
|
|
1800
|
+
clearTimeout(t);
|
|
1801
|
+
d.timers.clear();
|
|
1802
|
+
}
|
|
1803
|
+
function forgetKickoff(sessionId) {
|
|
1804
|
+
const d = deliveries.get(sessionId);
|
|
1805
|
+
if (d)
|
|
1806
|
+
stopTimers(d);
|
|
1807
|
+
deliveries.delete(sessionId);
|
|
1808
|
+
pendingKickoffOverrides.delete(sessionId);
|
|
1809
|
+
}
|
|
1810
|
+
function ptyIo(sessionId) {
|
|
1811
|
+
return {
|
|
1812
|
+
write: (data) => ptyRegistry().get(sessionId)?.pty.write(data),
|
|
1813
|
+
alive: () => {
|
|
1814
|
+
const entry = ptyRegistry().get(sessionId);
|
|
1815
|
+
return !!entry && !entry.exited;
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
function track(d, timer) {
|
|
1820
|
+
timer.unref?.();
|
|
1821
|
+
d.timers.add(timer);
|
|
1822
|
+
}
|
|
1823
|
+
function attemptKickoff(ctx, session, d, delayMs, clearFirst = false) {
|
|
1824
|
+
track(d, setTimeout(() => {
|
|
1825
|
+
if (d.settled)
|
|
1826
|
+
return;
|
|
1827
|
+
const io = ptyIo(session.id);
|
|
1828
|
+
if (!io.alive()) {
|
|
1829
|
+
d.settled = true;
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
d.attempts += 1;
|
|
1833
|
+
const attempt = d.attempts;
|
|
1680
1834
|
try {
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
data: { sessionId: session.id, kind: session.kind }
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1835
|
+
if (clearFirst)
|
|
1836
|
+
io.write(CLEAR_INPUT);
|
|
1837
|
+
writeKickoffSequence(d.line, {
|
|
1838
|
+
write: io.write,
|
|
1839
|
+
alive: io.alive,
|
|
1840
|
+
onSubmitted: () => emitForSession(ctx, session, {
|
|
1841
|
+
type: "session.kickoff",
|
|
1842
|
+
message: attempt === 1 ? `${session.kind} session kicked off automatically` : `${session.kind} kickoff re-sent (attempt ${attempt}) \u2014 the first was never acknowledged`,
|
|
1843
|
+
data: { sessionId: session.id, kind: session.kind, attempt }
|
|
1844
|
+
})
|
|
1694
1845
|
});
|
|
1695
1846
|
} catch {}
|
|
1696
|
-
|
|
1847
|
+
armConfirmation(ctx, session, d);
|
|
1848
|
+
}, delayMs));
|
|
1849
|
+
}
|
|
1850
|
+
function armConfirmation(ctx, session, d) {
|
|
1851
|
+
track(d, setTimeout(() => {
|
|
1852
|
+
if (d.settled || d.confirmed)
|
|
1853
|
+
return;
|
|
1854
|
+
if (d.attempts < KICKOFF_MAX_ATTEMPTS && ptyIo(session.id).alive()) {
|
|
1855
|
+
attemptKickoff(ctx, session, d, 0, true);
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
settleUndelivered(ctx, session, d, "unacknowledged");
|
|
1859
|
+
}, KICKOFF_CONFIRM_MS));
|
|
1860
|
+
}
|
|
1861
|
+
function settleUndelivered(ctx, session, d, reason) {
|
|
1862
|
+
d.settled = true;
|
|
1863
|
+
stopTimers(d);
|
|
1864
|
+
emitForSession(ctx, session, {
|
|
1865
|
+
type: "session.kickoff_undelivered",
|
|
1866
|
+
message: reason === "superseded" ? `the ${session.kind} briefing was never delivered \u2014 you typed first, so runcastle stopped injecting it` : `the ${session.kind} briefing was typed ${d.attempts}\xD7 but Claude Code never acknowledged it \u2014 send it again from the session strip`,
|
|
1867
|
+
data: { sessionId: session.id, kind: session.kind, reason, attempts: d.attempts, line: d.line }
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
function noteKickoffPrompt(ctx, sessionId, prompt) {
|
|
1871
|
+
const d = deliveries.get(sessionId);
|
|
1872
|
+
if (!d || d.confirmed)
|
|
1873
|
+
return;
|
|
1874
|
+
const session = getSessionRow(ctx, sessionId);
|
|
1875
|
+
if (!session)
|
|
1876
|
+
return;
|
|
1877
|
+
if (promptMatchesKickoff(d.line, prompt)) {
|
|
1878
|
+
d.confirmed = true;
|
|
1879
|
+
d.settled = true;
|
|
1880
|
+
stopTimers(d);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
if (d.settled)
|
|
1884
|
+
return;
|
|
1885
|
+
settleUndelivered(ctx, session, d, "superseded");
|
|
1886
|
+
}
|
|
1887
|
+
function promptMatchesKickoff(line, prompt) {
|
|
1888
|
+
if (!prompt)
|
|
1889
|
+
return false;
|
|
1890
|
+
const norm = (s) => s.replace(/\s+/g, " ").trim().toLowerCase();
|
|
1891
|
+
const want = norm(line);
|
|
1892
|
+
const got = norm(prompt);
|
|
1893
|
+
if (!want || !got)
|
|
1894
|
+
return false;
|
|
1895
|
+
return got.includes(want.slice(0, MATCH_PREFIX));
|
|
1896
|
+
}
|
|
1897
|
+
function resendKickoff(ctx, sessionId) {
|
|
1898
|
+
const session = getSessionRow(ctx, sessionId);
|
|
1899
|
+
if (!session)
|
|
1900
|
+
throw new Error(`unknown session ${sessionId}`);
|
|
1901
|
+
if (session.status === "ended")
|
|
1902
|
+
throw new Error("that session has ended \u2014 open a new terminal");
|
|
1903
|
+
if (!ptyIo(sessionId).alive())
|
|
1904
|
+
throw new Error("the terminal for that session is no longer running");
|
|
1905
|
+
const existing = deliveries.get(sessionId);
|
|
1906
|
+
const d = existing ?? {
|
|
1907
|
+
line: kickoffLineFor(session.kind, pendingKickoffOverrides.get(sessionId)),
|
|
1908
|
+
attempts: 0,
|
|
1909
|
+
confirmed: false,
|
|
1910
|
+
settled: false,
|
|
1911
|
+
timers: new Set
|
|
1912
|
+
};
|
|
1913
|
+
stopTimers(d);
|
|
1914
|
+
d.attempts = 0;
|
|
1915
|
+
d.confirmed = false;
|
|
1916
|
+
d.settled = false;
|
|
1917
|
+
deliveries.set(sessionId, d);
|
|
1918
|
+
attemptKickoff(ctx, session, d, 0, true);
|
|
1919
|
+
return { line: d.line };
|
|
1920
|
+
}
|
|
1921
|
+
function armSessionReadyWatchdog(ctx, session) {
|
|
1922
|
+
const timer = setTimeout(() => {
|
|
1923
|
+
const row = getSessionRow(ctx, session.id);
|
|
1924
|
+
if (!row || row.status !== "launching")
|
|
1925
|
+
return;
|
|
1926
|
+
if (!ptyIo(session.id).alive())
|
|
1927
|
+
return;
|
|
1928
|
+
emitForSession(ctx, session, {
|
|
1929
|
+
type: "session.not_ready",
|
|
1930
|
+
message: `the ${session.kind} terminal is open but Claude Code has not reported ready \u2014 check it for a prompt or dialog waiting on you, then send the briefing`,
|
|
1931
|
+
data: { sessionId: session.id, kind: session.kind }
|
|
1932
|
+
});
|
|
1933
|
+
}, SESSION_READY_TIMEOUT_MS);
|
|
1697
1934
|
timer.unref?.();
|
|
1698
1935
|
}
|
|
1936
|
+
function scheduleKickoff(ctx, session) {
|
|
1937
|
+
const line = kickoffLineFor(session.kind, pendingKickoffOverrides.get(session.id));
|
|
1938
|
+
pendingKickoffOverrides.delete(session.id);
|
|
1939
|
+
const existing = deliveries.get(session.id);
|
|
1940
|
+
if (existing)
|
|
1941
|
+
stopTimers(existing);
|
|
1942
|
+
const d = { line, attempts: 0, confirmed: false, settled: false, timers: new Set };
|
|
1943
|
+
deliveries.set(session.id, d);
|
|
1944
|
+
attemptKickoff(ctx, session, d, KICKOFF_DELAY_MS);
|
|
1945
|
+
}
|
|
1699
1946
|
function markSessionEnded(ctx, id) {
|
|
1700
1947
|
const existing = getSessionRow(ctx, id);
|
|
1701
1948
|
if (!existing)
|
|
1702
1949
|
return null;
|
|
1703
|
-
|
|
1950
|
+
forgetKickoff(id);
|
|
1704
1951
|
ctx.db.update(sessions).set({ status: "ended" }).where(eq4(sessions.id, id)).run();
|
|
1705
1952
|
return getSessionRow(ctx, id);
|
|
1706
1953
|
}
|
|
@@ -1708,11 +1955,19 @@ function mostRecentResumableSession(ctx, featureId, kind) {
|
|
|
1708
1955
|
const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.featureId, featureId), eq4(sessions.status, "ended"), isNotNull(sessions.ccSessionId), ...kind ? [eq4(sessions.kind, kind)] : [])).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1709
1956
|
return row ? rowToSession(row) : null;
|
|
1710
1957
|
}
|
|
1958
|
+
function activeProjectSession(ctx, projectId, kind) {
|
|
1959
|
+
const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.projectId, projectId), eq4(sessions.kind, kind), inArray(sessions.status, ["launching", "live"]))).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1960
|
+
return row ? rowToSession(row) : null;
|
|
1961
|
+
}
|
|
1962
|
+
function mostRecentResumableProjectSession(ctx, projectId, kind) {
|
|
1963
|
+
const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.projectId, projectId), eq4(sessions.kind, kind), eq4(sessions.status, "ended"), isNotNull(sessions.ccSessionId))).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1964
|
+
return row ? rowToSession(row) : null;
|
|
1965
|
+
}
|
|
1711
1966
|
function mostRecentLiveSession(ctx) {
|
|
1712
1967
|
const row = ctx.db.select().from(sessions).where(eq4(sessions.status, "live")).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1713
1968
|
return row ? rowToSession(row) : null;
|
|
1714
1969
|
}
|
|
1715
|
-
var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides;
|
|
1970
|
+
var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, KICKOFF_CONFIRM_MS = 12000, KICKOFF_MAX_ATTEMPTS = 3, CLEAR_INPUT = "\x15", SESSION_READY_TIMEOUT_MS = 25000, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides, deliveries, MATCH_PREFIX = 40;
|
|
1716
1971
|
var init_sessions = __esm(() => {
|
|
1717
1972
|
init_src();
|
|
1718
1973
|
init_schema();
|
|
@@ -1726,10 +1981,12 @@ var init_sessions = __esm(() => {
|
|
|
1726
1981
|
qa: "Proceed with your task: invoke the /runcastle:qa skill and answer questions from the " + "docs and code \u2014 do not advance phases or emit tickets.",
|
|
1727
1982
|
waypoint: "Proceed with your task: invoke the /runcastle:waypoint skill and work your assigned " + "waypoint to a resolution.",
|
|
1728
1983
|
converge: CONVERGE_KICKOFF_LINE,
|
|
1729
|
-
revisit: "Proceed with your task: invoke the /runcastle:revisit skill and work through what the " + "human brings up."
|
|
1984
|
+
revisit: "Proceed with your task: invoke the /runcastle:revisit skill and work through what the " + "human brings up.",
|
|
1985
|
+
prepare: "Proceed with your task: work through the unestablished preparation fields with the human. " + "Start by telling them which fields are still open and what you need from them for each; " + "ask before running anything that touches their database or services."
|
|
1730
1986
|
};
|
|
1731
1987
|
RESUME_KICKOFF_PREFIX = "We are picking this conversation back up \u2014 runcastle restarted and closed the terminal, " + "but this conversation is intact. Do NOT start over: tell me in one or two lines where we " + "left off and what is next, then carry on. Your original instruction was: ";
|
|
1732
1988
|
pendingKickoffOverrides = new Map;
|
|
1989
|
+
deliveries = new Map;
|
|
1733
1990
|
});
|
|
1734
1991
|
|
|
1735
1992
|
// src/launcher/reconcile.ts
|
|
@@ -1743,7 +2000,7 @@ function reconcileStaleSessions(ctx) {
|
|
|
1743
2000
|
continue;
|
|
1744
2001
|
markSessionEnded(ctx, session.id);
|
|
1745
2002
|
const released = releaseForSession(ctx, session.id);
|
|
1746
|
-
|
|
2003
|
+
emitForSession(ctx, session, {
|
|
1747
2004
|
type: "session.reconciled",
|
|
1748
2005
|
message: `session marked ended at boot \u2014 the server restarted while it was ${session.status}`,
|
|
1749
2006
|
data: {
|
|
@@ -1765,351 +2022,288 @@ var init_reconcile = __esm(() => {
|
|
|
1765
2022
|
init_sessions();
|
|
1766
2023
|
});
|
|
1767
2024
|
|
|
1768
|
-
// src/launcher/
|
|
1769
|
-
function setRuntimeCtx(ctx) {
|
|
1770
|
-
current = ctx;
|
|
1771
|
-
}
|
|
1772
|
-
async function getRuntimeCtx() {
|
|
1773
|
-
if (current)
|
|
1774
|
-
return current;
|
|
1775
|
-
const [{ createDb: createDb2 }, { dbPath: dbPath2 }, { loadConfig: loadConfig2 }] = await Promise.all([
|
|
1776
|
-
Promise.resolve().then(() => (init_client(), exports_client)),
|
|
1777
|
-
Promise.resolve().then(() => (init_paths(), exports_paths)),
|
|
1778
|
-
Promise.resolve().then(() => (init_config_load(), exports_config_load))
|
|
1779
|
-
]);
|
|
1780
|
-
const db = createDb2(dbPath2());
|
|
1781
|
-
current = { db, config: loadConfig2() };
|
|
1782
|
-
return current;
|
|
1783
|
-
}
|
|
1784
|
-
var current = null;
|
|
1785
|
-
|
|
1786
|
-
// src/services/feature-docs.ts
|
|
2025
|
+
// src/launcher/skills-root.ts
|
|
1787
2026
|
import { existsSync as existsSync5 } from "fs";
|
|
1788
|
-
import { join as join5 } from "path";
|
|
1789
|
-
function
|
|
1790
|
-
const
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2027
|
+
import { dirname, join as join5, resolve as resolve4 } from "path";
|
|
2028
|
+
function resolveSkillsRoot(fromDir) {
|
|
2029
|
+
const override = process.env[SKILLS_DIR_ENV];
|
|
2030
|
+
if (override) {
|
|
2031
|
+
const root = resolve4(override);
|
|
2032
|
+
if (existsSync5(join5(root, SKILLS_MARKER)))
|
|
2033
|
+
return root;
|
|
2034
|
+
throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
|
|
2035
|
+
}
|
|
2036
|
+
const searched = [];
|
|
2037
|
+
let dir = fromDir;
|
|
2038
|
+
for (let i = 0;i < 8; i += 1) {
|
|
2039
|
+
const candidate = join5(dir, "packages", "skills");
|
|
2040
|
+
searched.push(candidate);
|
|
2041
|
+
if (existsSync5(join5(candidate, SKILLS_MARKER)))
|
|
2042
|
+
return candidate;
|
|
2043
|
+
const parent = dirname(dir);
|
|
2044
|
+
if (parent === dir)
|
|
2045
|
+
break;
|
|
2046
|
+
dir = parent;
|
|
2047
|
+
}
|
|
2048
|
+
throw new Error(`runcastle skills root not found; searched:
|
|
2049
|
+
${searched.join(`
|
|
2050
|
+
`)}`);
|
|
1797
2051
|
}
|
|
1798
|
-
var
|
|
1799
|
-
|
|
2052
|
+
var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
|
|
2053
|
+
var init_skills_root = __esm(() => {
|
|
2054
|
+
SKILLS_MARKER = join5("packs", "runcastle");
|
|
1800
2055
|
});
|
|
1801
2056
|
|
|
1802
|
-
// src/
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
return {
|
|
1806
|
-
id: row.id,
|
|
1807
|
-
featureId: row.featureId,
|
|
1808
|
-
seq: row.seq,
|
|
1809
|
-
title: row.title,
|
|
1810
|
-
goal: row.goal,
|
|
1811
|
-
context: row.context,
|
|
1812
|
-
acceptanceCriteria: row.acceptanceCriteria,
|
|
1813
|
-
seams: row.seams,
|
|
1814
|
-
blockedBy: row.blockedBy,
|
|
1815
|
-
status: row.status,
|
|
1816
|
-
commits: row.commits,
|
|
1817
|
-
error: row.error ?? undefined,
|
|
1818
|
-
attemptBranch: row.attemptBranch ?? undefined,
|
|
1819
|
-
conflictFiles: row.conflictFiles ?? undefined
|
|
1820
|
-
};
|
|
2057
|
+
// src/pty/dev-pane.ts
|
|
2058
|
+
function drivePaneId(featureId) {
|
|
2059
|
+
return `${DRIVE_PREFIX}${featureId}`;
|
|
1821
2060
|
}
|
|
1822
|
-
function
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
} catch (e) {
|
|
1826
|
-
if (e instanceof BlockingEdgeError)
|
|
1827
|
-
throw new InvalidInputError(e.message);
|
|
1828
|
-
throw e;
|
|
1829
|
-
}
|
|
2061
|
+
function sniffDevUrl(text2) {
|
|
2062
|
+
const m = text2.match(/https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/[^\s"'<>`\x1b]*)?/i);
|
|
2063
|
+
return m ? m[0] : undefined;
|
|
1830
2064
|
}
|
|
1831
|
-
function
|
|
1832
|
-
|
|
2065
|
+
function devSpawnTarget(devCommand) {
|
|
2066
|
+
if (process.platform === "win32") {
|
|
2067
|
+
return { file: process.env.ComSpec ?? "cmd.exe", args: ["/d", "/s", "/c", devCommand] };
|
|
2068
|
+
}
|
|
2069
|
+
return { file: "/bin/sh", args: ["-c", devCommand] };
|
|
1833
2070
|
}
|
|
1834
|
-
function
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2071
|
+
function startDevPane(input) {
|
|
2072
|
+
const { ctx, featureId, repoPath, devCommand, env, onUrl } = input;
|
|
2073
|
+
const paneId = drivePaneId(featureId);
|
|
2074
|
+
const { file, args } = devSpawnTarget(devCommand);
|
|
2075
|
+
try {
|
|
2076
|
+
ptyRegistry().create({
|
|
2077
|
+
sessionId: paneId,
|
|
2078
|
+
cmd: file,
|
|
2079
|
+
args,
|
|
2080
|
+
opts: { cwd: repoPath, env: env ?? process.env, cols: 80, rows: 24, useConpty: true }
|
|
2081
|
+
});
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
emit(ctx, featureId, {
|
|
2084
|
+
type: "testdrive.dev_failed",
|
|
2085
|
+
message: `dev server failed to start: ${err instanceof Error ? err.message : String(err)}`,
|
|
2086
|
+
data: { paneId }
|
|
2087
|
+
});
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
let scanned = "";
|
|
2091
|
+
let found = false;
|
|
2092
|
+
const sniffer = {
|
|
2093
|
+
sendData(chunk) {
|
|
2094
|
+
if (found)
|
|
2095
|
+
return;
|
|
2096
|
+
scanned = (scanned + chunk.toString("utf8")).slice(-SNIFF_WINDOW);
|
|
2097
|
+
const url = sniffDevUrl(scanned);
|
|
2098
|
+
if (url) {
|
|
2099
|
+
found = true;
|
|
2100
|
+
onUrl(url);
|
|
2101
|
+
}
|
|
2102
|
+
},
|
|
2103
|
+
sendControl() {}
|
|
2104
|
+
};
|
|
2105
|
+
ptyRegistry().attach(paneId, sniffer);
|
|
1857
2106
|
emit(ctx, featureId, {
|
|
1858
|
-
type: "
|
|
1859
|
-
message:
|
|
1860
|
-
data: {
|
|
2107
|
+
type: "testdrive.dev_started",
|
|
2108
|
+
message: `dev server spawned: ${devCommand}`,
|
|
2109
|
+
data: { paneId }
|
|
1861
2110
|
});
|
|
1862
|
-
return
|
|
2111
|
+
return paneId;
|
|
1863
2112
|
}
|
|
1864
|
-
function
|
|
1865
|
-
const
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2113
|
+
function stopDevPane(paneId) {
|
|
2114
|
+
const reg = ptyRegistry();
|
|
2115
|
+
const entry = reg.get(paneId);
|
|
2116
|
+
if (entry && !entry.exited && process.platform !== "win32") {
|
|
2117
|
+
try {
|
|
2118
|
+
process.kill(-entry.pty.pid, "SIGTERM");
|
|
2119
|
+
} catch {}
|
|
2120
|
+
}
|
|
2121
|
+
reg.kill(paneId);
|
|
2122
|
+
reg.remove(paneId);
|
|
1869
2123
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2124
|
+
var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192;
|
|
2125
|
+
var init_dev_pane = __esm(() => {
|
|
2126
|
+
init_events();
|
|
2127
|
+
init_registry();
|
|
2128
|
+
});
|
|
2129
|
+
|
|
2130
|
+
// src/services/drive-hooks.ts
|
|
2131
|
+
import { spawn as spawn2 } from "child_process";
|
|
2132
|
+
function tailLines(text2, max = OUTPUT_TAIL_LINES) {
|
|
2133
|
+
const lines = text2.replace(/\r\n/g, `
|
|
2134
|
+
`).split(`
|
|
2135
|
+
`);
|
|
2136
|
+
while (lines.length > 0 && lines[lines.length - 1]?.trim() === "")
|
|
2137
|
+
lines.pop();
|
|
2138
|
+
return lines.slice(-max).join(`
|
|
2139
|
+
`).trim();
|
|
2140
|
+
}
|
|
2141
|
+
function hookSpawnTarget(command) {
|
|
2142
|
+
if (process.platform === "win32") {
|
|
2143
|
+
return {
|
|
2144
|
+
file: process.env.ComSpec ?? "cmd.exe",
|
|
2145
|
+
args: ["/d", "/s", "/c", `"${command}"`],
|
|
2146
|
+
verbatim: true
|
|
2147
|
+
};
|
|
1873
2148
|
}
|
|
2149
|
+
return { file: "/bin/sh", args: ["-c", command], verbatim: false };
|
|
1874
2150
|
}
|
|
1875
|
-
function
|
|
1876
|
-
const
|
|
1877
|
-
|
|
1878
|
-
const
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
}
|
|
1913
|
-
function sweepOrphanedBurning(ctx, featureId, reason) {
|
|
1914
|
-
const orphaned = ctx.db.select().from(tickets).where(and5(eq5(tickets.featureId, featureId), eq5(tickets.status, "burning"))).all().map(rowToTicket);
|
|
1915
|
-
for (const t of orphaned) {
|
|
1916
|
-
updateTicket(ctx, t.id, { status: "failed", error: reason });
|
|
1917
|
-
emit(ctx, featureId, {
|
|
1918
|
-
type: "ticket.failed",
|
|
1919
|
-
message: `ticket ${t.seq} failed: ${reason}`,
|
|
1920
|
-
ticketId: t.id,
|
|
1921
|
-
data: { error: reason, orphaned: true }
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
|
-
return orphaned.map((t) => ({ ...t, status: "failed", error: reason }));
|
|
1925
|
-
}
|
|
1926
|
-
function updateTicket(ctx, id, patch) {
|
|
1927
|
-
const current2 = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
1928
|
-
if (!current2)
|
|
1929
|
-
throw new NotFoundError(`ticket ${id} not found`);
|
|
1930
|
-
const set = {};
|
|
1931
|
-
if (patch.status !== undefined)
|
|
1932
|
-
set.status = patch.status;
|
|
1933
|
-
if (patch.commits !== undefined)
|
|
1934
|
-
set.commits = patch.commits;
|
|
1935
|
-
if (patch.error !== undefined)
|
|
1936
|
-
set.error = patch.error;
|
|
1937
|
-
if (patch.attemptBranch !== undefined)
|
|
1938
|
-
set.attemptBranch = patch.attemptBranch;
|
|
1939
|
-
if (patch.conflictFiles !== undefined)
|
|
1940
|
-
set.conflictFiles = patch.conflictFiles;
|
|
1941
|
-
ctx.db.update(tickets).set(set).where(eq5(tickets.id, id)).run();
|
|
1942
|
-
emit(ctx, current2.featureId, {
|
|
1943
|
-
type: "ticket.updated",
|
|
1944
|
-
message: `ticket ${current2.seq} \u2192 ${patch.status ?? current2.status}`,
|
|
1945
|
-
ticketId: id,
|
|
1946
|
-
data: patch
|
|
1947
|
-
});
|
|
1948
|
-
const updated = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
1949
|
-
return rowToTicket(updated);
|
|
1950
|
-
}
|
|
1951
|
-
var MUTABLE_STATUSES;
|
|
1952
|
-
var init_tickets = __esm(() => {
|
|
1953
|
-
init_src();
|
|
1954
|
-
init_schema();
|
|
1955
|
-
init_errors();
|
|
1956
|
-
init_events();
|
|
1957
|
-
MUTABLE_STATUSES = new Set(["pending", "failed"]);
|
|
1958
|
-
});
|
|
1959
|
-
|
|
1960
|
-
// src/services/gates.ts
|
|
1961
|
-
import { existsSync as existsSync6 } from "fs";
|
|
1962
|
-
function checkGate(ctx, check, feature) {
|
|
1963
|
-
switch (check) {
|
|
1964
|
-
case "decisions-file-exists":
|
|
1965
|
-
return fileGate(ctx, feature, "decisions.md", "run the ideation session to capture decisions first");
|
|
1966
|
-
case "all-waypoints-terminal": {
|
|
1967
|
-
const wps = listByFeature(ctx, feature.id);
|
|
1968
|
-
if (wps.length === 0) {
|
|
1969
|
-
return { satisfied: false, reason: "no waypoints charted yet \u2014 chart the map before converging" };
|
|
1970
|
-
}
|
|
1971
|
-
const open = wps.filter((w) => w.status !== "resolved" && w.status !== "dropped");
|
|
1972
|
-
return open.length === 0 ? { satisfied: true } : {
|
|
1973
|
-
satisfied: false,
|
|
1974
|
-
reason: notYetTerminal("waypoint", open.map((w) => w.status), ["open", "claimed"])
|
|
1975
|
-
};
|
|
1976
|
-
}
|
|
1977
|
-
case "spec-file-exists":
|
|
1978
|
-
return fileGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
|
|
1979
|
-
case "tickets-approved": {
|
|
1980
|
-
const count = listByFeature2(ctx, feature.id).filter((t) => t.status !== "cancelled").length;
|
|
1981
|
-
return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
|
|
1982
|
-
}
|
|
1983
|
-
case "all-tickets-terminal": {
|
|
1984
|
-
const tickets2 = listByFeature2(ctx, feature.id);
|
|
1985
|
-
if (tickets2.length === 0)
|
|
1986
|
-
return { satisfied: false, reason: "no tickets have run yet" };
|
|
1987
|
-
const open = tickets2.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
1988
|
-
return open.length === 0 ? { satisfied: true } : {
|
|
1989
|
-
satisfied: false,
|
|
1990
|
-
reason: notYetTerminal("ticket", open.map((t) => t.status), ["pending", "burning"])
|
|
1991
|
-
};
|
|
2151
|
+
function runDriveHook(command, opts) {
|
|
2152
|
+
const spawnFn = opts.spawnFn ?? spawn2;
|
|
2153
|
+
const now = opts.now ?? Date.now;
|
|
2154
|
+
const timeoutMs = opts.timeoutMs ?? DRIVE_HOOK_TIMEOUT_MS;
|
|
2155
|
+
const startedAt = now();
|
|
2156
|
+
const { file, args, verbatim } = hookSpawnTarget(command);
|
|
2157
|
+
return new Promise((resolve5) => {
|
|
2158
|
+
let output = "";
|
|
2159
|
+
let timedOut = false;
|
|
2160
|
+
let settled = false;
|
|
2161
|
+
let timer;
|
|
2162
|
+
const finish = (exitCode) => {
|
|
2163
|
+
if (settled)
|
|
2164
|
+
return;
|
|
2165
|
+
settled = true;
|
|
2166
|
+
if (timer)
|
|
2167
|
+
clearTimeout(timer);
|
|
2168
|
+
resolve5({
|
|
2169
|
+
ok: !timedOut && exitCode === 0,
|
|
2170
|
+
exitCode,
|
|
2171
|
+
timedOut,
|
|
2172
|
+
output: tailLines(output),
|
|
2173
|
+
durationMs: now() - startedAt
|
|
2174
|
+
});
|
|
2175
|
+
};
|
|
2176
|
+
let child;
|
|
2177
|
+
try {
|
|
2178
|
+
child = spawnFn(file, args, {
|
|
2179
|
+
cwd: opts.cwd,
|
|
2180
|
+
env: opts.env ?? process.env,
|
|
2181
|
+
windowsHide: true,
|
|
2182
|
+
windowsVerbatimArguments: verbatim
|
|
2183
|
+
});
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
output = err instanceof Error ? err.message : String(err);
|
|
2186
|
+
finish(null);
|
|
2187
|
+
return;
|
|
1992
2188
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
ctx.db.insert(gateOverrides).values({ featureId, gate, reason, ts: Date.now() }).run();
|
|
2015
|
-
emit(ctx, featureId, {
|
|
2016
|
-
type: "gate.overridden",
|
|
2017
|
-
message: `gate ${gate} overridden: ${reason}`,
|
|
2018
|
-
data: { gate, reason }
|
|
2189
|
+
timer = setTimeout(() => {
|
|
2190
|
+
timedOut = true;
|
|
2191
|
+
child.kill();
|
|
2192
|
+
const grace = setTimeout(() => finish(null), KILL_GRACE_MS);
|
|
2193
|
+
grace.unref?.();
|
|
2194
|
+
}, timeoutMs);
|
|
2195
|
+
timer.unref?.();
|
|
2196
|
+
child.stdout?.on("data", (d) => {
|
|
2197
|
+
output += d.toString();
|
|
2198
|
+
});
|
|
2199
|
+
child.stderr?.on("data", (d) => {
|
|
2200
|
+
output += d.toString();
|
|
2201
|
+
});
|
|
2202
|
+
child.on("error", (err) => {
|
|
2203
|
+
output += `
|
|
2204
|
+
${err instanceof Error ? err.message : String(err)}`;
|
|
2205
|
+
finish(null);
|
|
2206
|
+
});
|
|
2207
|
+
child.on("close", (code) => {
|
|
2208
|
+
finish(code);
|
|
2209
|
+
});
|
|
2019
2210
|
});
|
|
2020
|
-
const next = nextPhase(feature);
|
|
2021
|
-
if (next)
|
|
2022
|
-
return setPhase(ctx, featureId, next, "phase.advanced", `advanced to ${next} (override)`);
|
|
2023
|
-
return feature;
|
|
2024
2211
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2212
|
+
function describeHookResult(command, result) {
|
|
2213
|
+
const secs = Math.max(1, Math.round(result.durationMs / 1000));
|
|
2214
|
+
if (result.timedOut)
|
|
2215
|
+
return `\`${command}\` timed out after ${secs}s`;
|
|
2216
|
+
if (result.ok)
|
|
2217
|
+
return `\`${command}\` finished in ${secs}s`;
|
|
2218
|
+
return `\`${command}\` exited ${result.exitCode ?? "without a code"} after ${secs}s`;
|
|
2219
|
+
}
|
|
2220
|
+
var DRIVE_HOOK_TIMEOUT_MS, OUTPUT_TAIL_LINES = 40, KILL_GRACE_MS = 2000;
|
|
2221
|
+
var init_drive_hooks = __esm(() => {
|
|
2222
|
+
DRIVE_HOOK_TIMEOUT_MS = 10 * 60000;
|
|
2033
2223
|
});
|
|
2034
2224
|
|
|
2035
|
-
// src/
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
const { ctx, featureId, repoPath, devCommand, onUrl } = input;
|
|
2051
|
-
const paneId = drivePaneId(featureId);
|
|
2052
|
-
const { file, args } = devSpawnTarget(devCommand);
|
|
2053
|
-
try {
|
|
2054
|
-
ptyRegistry().create({
|
|
2055
|
-
sessionId: paneId,
|
|
2056
|
-
cmd: file,
|
|
2057
|
-
args,
|
|
2058
|
-
opts: { cwd: repoPath, env: process.env, cols: 80, rows: 24, useConpty: true }
|
|
2059
|
-
});
|
|
2060
|
-
} catch (err) {
|
|
2061
|
-
emit(ctx, featureId, {
|
|
2062
|
-
type: "testdrive.dev_failed",
|
|
2063
|
-
message: `dev server failed to start: ${err instanceof Error ? err.message : String(err)}`,
|
|
2064
|
-
data: { paneId }
|
|
2065
|
-
});
|
|
2066
|
-
return;
|
|
2067
|
-
}
|
|
2068
|
-
let scanned = "";
|
|
2069
|
-
let found = false;
|
|
2070
|
-
const sniffer = {
|
|
2071
|
-
sendData(chunk) {
|
|
2072
|
-
if (found)
|
|
2073
|
-
return;
|
|
2074
|
-
scanned = (scanned + chunk.toString("utf8")).slice(-SNIFF_WINDOW);
|
|
2075
|
-
const url = sniffDevUrl(scanned);
|
|
2076
|
-
if (url) {
|
|
2077
|
-
found = true;
|
|
2078
|
-
onUrl(url);
|
|
2079
|
-
}
|
|
2080
|
-
},
|
|
2081
|
-
sendControl() {}
|
|
2225
|
+
// src/services/drive-env.ts
|
|
2226
|
+
import { createHash } from "crypto";
|
|
2227
|
+
function identifierSafe(slug, max = MAX_ID_LENGTH) {
|
|
2228
|
+
const cleaned = slug.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2229
|
+
const safe = cleaned === "" || /^[0-9]/.test(cleaned) ? `x_${cleaned}` : cleaned;
|
|
2230
|
+
if (safe.length <= max)
|
|
2231
|
+
return safe;
|
|
2232
|
+
const digest = createHash("sha1").update(slug).digest("hex").slice(0, 6);
|
|
2233
|
+
return `${safe.slice(0, max - digest.length - 1)}_${digest}`;
|
|
2234
|
+
}
|
|
2235
|
+
function driveVars(identity) {
|
|
2236
|
+
return {
|
|
2237
|
+
slug: identity.slug,
|
|
2238
|
+
branch: identity.branch,
|
|
2239
|
+
id: identifierSafe(identity.slug)
|
|
2082
2240
|
};
|
|
2083
|
-
ptyRegistry().attach(paneId, sniffer);
|
|
2084
|
-
emit(ctx, featureId, {
|
|
2085
|
-
type: "testdrive.dev_started",
|
|
2086
|
-
message: `dev server spawned: ${devCommand}`,
|
|
2087
|
-
data: { paneId }
|
|
2088
|
-
});
|
|
2089
|
-
return paneId;
|
|
2090
|
-
}
|
|
2091
|
-
function stopDevPane(paneId) {
|
|
2092
|
-
const reg = ptyRegistry();
|
|
2093
|
-
const entry = reg.get(paneId);
|
|
2094
|
-
if (entry && !entry.exited && process.platform !== "win32") {
|
|
2095
|
-
try {
|
|
2096
|
-
process.kill(-entry.pty.pid, "SIGTERM");
|
|
2097
|
-
} catch {}
|
|
2098
|
-
}
|
|
2099
|
-
reg.kill(paneId);
|
|
2100
|
-
reg.remove(paneId);
|
|
2101
2241
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2242
|
+
function renderTemplate(value, vars) {
|
|
2243
|
+
const unknown2 = [];
|
|
2244
|
+
const out = value.replace(PLACEHOLDER_RE, (match, name) => {
|
|
2245
|
+
const replacement = vars[name];
|
|
2246
|
+
if (replacement === undefined) {
|
|
2247
|
+
if (!unknown2.includes(name))
|
|
2248
|
+
unknown2.push(name);
|
|
2249
|
+
return match;
|
|
2250
|
+
}
|
|
2251
|
+
return replacement;
|
|
2252
|
+
});
|
|
2253
|
+
return { value: out, unknown: unknown2 };
|
|
2254
|
+
}
|
|
2255
|
+
function parseDriveEnv(raw, identity) {
|
|
2256
|
+
const vars = {};
|
|
2257
|
+
const unknown2 = [];
|
|
2258
|
+
if (!raw?.trim())
|
|
2259
|
+
return { vars, unknown: unknown2 };
|
|
2260
|
+
const substitutions = driveVars(identity);
|
|
2261
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
2262
|
+
const trimmed = line.trim();
|
|
2263
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
2264
|
+
continue;
|
|
2265
|
+
const eq5 = trimmed.indexOf("=");
|
|
2266
|
+
if (eq5 <= 0)
|
|
2267
|
+
continue;
|
|
2268
|
+
const key = trimmed.slice(0, eq5).trim().replace(/^export\s+/, "");
|
|
2269
|
+
if (key === "")
|
|
2270
|
+
continue;
|
|
2271
|
+
const rendered = renderTemplate(unquote(trimmed.slice(eq5 + 1).trim()), substitutions);
|
|
2272
|
+
vars[key] = rendered.value;
|
|
2273
|
+
for (const name of rendered.unknown)
|
|
2274
|
+
if (!unknown2.includes(name))
|
|
2275
|
+
unknown2.push(name);
|
|
2276
|
+
}
|
|
2277
|
+
return { vars, unknown: unknown2 };
|
|
2278
|
+
}
|
|
2279
|
+
function unquote(value) {
|
|
2280
|
+
if (value.length < 2)
|
|
2281
|
+
return value;
|
|
2282
|
+
const first = value[0];
|
|
2283
|
+
if ((first === '"' || first === "'") && value.endsWith(first))
|
|
2284
|
+
return value.slice(1, -1);
|
|
2285
|
+
return value;
|
|
2286
|
+
}
|
|
2287
|
+
function driveProcessEnv(overrides, base = process.env) {
|
|
2288
|
+
return { ...base, ...overrides };
|
|
2289
|
+
}
|
|
2290
|
+
function describeDriveEnv(vars) {
|
|
2291
|
+
const keys = Object.keys(vars);
|
|
2292
|
+
return `drive environment: ${keys.join(", ")}`;
|
|
2293
|
+
}
|
|
2294
|
+
var MAX_ID_LENGTH = 40, PLACEHOLDER_RE;
|
|
2295
|
+
var init_drive_env = __esm(() => {
|
|
2296
|
+
PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
|
|
2106
2297
|
});
|
|
2107
2298
|
|
|
2108
2299
|
// src/services/git.ts
|
|
2109
|
-
import { existsSync as
|
|
2300
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, mkdtempSync, realpathSync, rmSync } from "fs";
|
|
2110
2301
|
import { tmpdir } from "os";
|
|
2111
|
-
import { dirname, join as join6, resolve as
|
|
2302
|
+
import { dirname as dirname2, join as join6, resolve as resolve5 } from "path";
|
|
2112
2303
|
import { simpleGit } from "simple-git";
|
|
2304
|
+
function migrationPaths(paths) {
|
|
2305
|
+
return paths.filter((p) => MIGRATION_DIR_RE.test(p) || DRIZZLE_SQL_RE.test(p));
|
|
2306
|
+
}
|
|
2113
2307
|
function featureBranch(slug) {
|
|
2114
2308
|
return `feature/${slug}`;
|
|
2115
2309
|
}
|
|
@@ -2120,14 +2314,14 @@ function errMsg(e) {
|
|
|
2120
2314
|
return e instanceof Error ? e.message : String(e);
|
|
2121
2315
|
}
|
|
2122
2316
|
function canon(p) {
|
|
2123
|
-
let abs =
|
|
2317
|
+
let abs = resolve5(p);
|
|
2124
2318
|
try {
|
|
2125
2319
|
abs = realpathSync.native(abs);
|
|
2126
2320
|
} catch {}
|
|
2127
2321
|
return process.platform === "win32" ? abs.replace(/\\/g, "/").toLowerCase() : abs;
|
|
2128
2322
|
}
|
|
2129
2323
|
async function assertRepo(repoPath) {
|
|
2130
|
-
if (!
|
|
2324
|
+
if (!existsSync6(repoPath)) {
|
|
2131
2325
|
throw new InvalidInputError(`path does not exist: ${repoPath}`);
|
|
2132
2326
|
}
|
|
2133
2327
|
let isRepo = false;
|
|
@@ -2216,7 +2410,7 @@ async function ensureTalkWorktree(project, feature) {
|
|
|
2216
2410
|
await ensureBranchExists(g, branch, feature.baseBranch ?? project.mainBranch);
|
|
2217
2411
|
if (await worktreeIsValid(g, worktreePath, branch))
|
|
2218
2412
|
return worktreePath;
|
|
2219
|
-
mkdirSync2(
|
|
2413
|
+
mkdirSync2(dirname2(worktreePath), { recursive: true });
|
|
2220
2414
|
try {
|
|
2221
2415
|
await g.raw(["worktree", "add", worktreePath, branch]);
|
|
2222
2416
|
return worktreePath;
|
|
@@ -2239,7 +2433,7 @@ async function ensureBranchExists(g, branch, from) {
|
|
|
2239
2433
|
}
|
|
2240
2434
|
}
|
|
2241
2435
|
async function worktreeIsValid(g, worktreePath, branch) {
|
|
2242
|
-
if (!
|
|
2436
|
+
if (!existsSync6(worktreePath))
|
|
2243
2437
|
return false;
|
|
2244
2438
|
const registered = await registeredWorktrees(g);
|
|
2245
2439
|
if (!registered.has(canon(worktreePath)))
|
|
@@ -2285,7 +2479,7 @@ async function worktreesOnBranch(g, branch, mainRepoPath) {
|
|
|
2285
2479
|
return (await listWorktrees(g)).filter((e) => e.branch === branch && canon(e.path) !== main).map((e) => e.path);
|
|
2286
2480
|
}
|
|
2287
2481
|
async function detachWorktree(path) {
|
|
2288
|
-
if (!
|
|
2482
|
+
if (!existsSync6(path))
|
|
2289
2483
|
return false;
|
|
2290
2484
|
const g = git(path);
|
|
2291
2485
|
let head;
|
|
@@ -2300,12 +2494,27 @@ async function detachWorktree(path) {
|
|
|
2300
2494
|
return true;
|
|
2301
2495
|
}
|
|
2302
2496
|
async function reattachWorktree(path, branch) {
|
|
2303
|
-
if (!
|
|
2497
|
+
if (!existsSync6(path))
|
|
2304
2498
|
return;
|
|
2305
2499
|
try {
|
|
2306
2500
|
await git(path).checkout(branch);
|
|
2307
2501
|
} catch {}
|
|
2308
2502
|
}
|
|
2503
|
+
function prepBranchName(unique) {
|
|
2504
|
+
return `${PREP_BRANCH_PREFIX}${unique}`;
|
|
2505
|
+
}
|
|
2506
|
+
async function readFileAtRef(repoPath, ref, path) {
|
|
2507
|
+
try {
|
|
2508
|
+
return await git(repoPath).raw(["show", `${ref}:${path}`]);
|
|
2509
|
+
} catch {
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
async function deletePrepBranch(repoPath, branch) {
|
|
2514
|
+
try {
|
|
2515
|
+
await deleteBranchDetachingWorktrees(git(repoPath), repoPath, branch);
|
|
2516
|
+
} catch {}
|
|
2517
|
+
}
|
|
2309
2518
|
function tempBranchSlugSegment(slug) {
|
|
2310
2519
|
return slug.slice(0, TEMP_BRANCH_SLUG_MAX).replace(/-+$/, "");
|
|
2311
2520
|
}
|
|
@@ -2323,14 +2532,14 @@ async function cleanupBurnWorktree(repoPath, branch, opts = {}) {
|
|
|
2323
2532
|
const attempts = Math.max(1, opts.attempts ?? 3);
|
|
2324
2533
|
const delayMs = opts.delayMs ?? 750;
|
|
2325
2534
|
const g = git(repoPath);
|
|
2326
|
-
for (let attempt = 1;attempt <= attempts &&
|
|
2535
|
+
for (let attempt = 1;attempt <= attempts && existsSync6(path); attempt++) {
|
|
2327
2536
|
try {
|
|
2328
2537
|
await g.raw(["worktree", "remove", "--force", path]);
|
|
2329
2538
|
} catch {}
|
|
2330
|
-
if (
|
|
2539
|
+
if (existsSync6(path) && attempt < attempts)
|
|
2331
2540
|
await sleep(delayMs);
|
|
2332
2541
|
}
|
|
2333
|
-
if (
|
|
2542
|
+
if (existsSync6(path)) {
|
|
2334
2543
|
try {
|
|
2335
2544
|
rmSync(path, { recursive: true, force: true });
|
|
2336
2545
|
} catch {}
|
|
@@ -2338,7 +2547,7 @@ async function cleanupBurnWorktree(repoPath, branch, opts = {}) {
|
|
|
2338
2547
|
try {
|
|
2339
2548
|
await g.raw(["worktree", "prune"]);
|
|
2340
2549
|
} catch {}
|
|
2341
|
-
return !
|
|
2550
|
+
return !existsSync6(path);
|
|
2342
2551
|
}
|
|
2343
2552
|
function sleep(ms) {
|
|
2344
2553
|
return new Promise((done) => setTimeout(done, ms));
|
|
@@ -2352,16 +2561,42 @@ async function branchCommitsAhead(repoPath, base, branch) {
|
|
|
2352
2561
|
return [];
|
|
2353
2562
|
}
|
|
2354
2563
|
}
|
|
2355
|
-
async function
|
|
2564
|
+
async function headSha(repoPath, ref) {
|
|
2356
2565
|
try {
|
|
2357
|
-
const out = await git(repoPath).
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2566
|
+
const out = (await git(repoPath).revparse([ref])).trim();
|
|
2567
|
+
return out.length > 0 ? out : undefined;
|
|
2568
|
+
} catch {
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
async function commitsSince(repoPath, sha, branch) {
|
|
2573
|
+
try {
|
|
2574
|
+
const out = (await git(repoPath).raw(["rev-list", "--count", `${sha}..${branch}`])).trim();
|
|
2575
|
+
const n = Number(out);
|
|
2576
|
+
return Number.isFinite(n) ? n : undefined;
|
|
2577
|
+
} catch {
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
async function diffPaths(repoPath, from, to) {
|
|
2582
|
+
try {
|
|
2583
|
+
const out = (await git(repoPath).raw(["diff", "--name-only", from, to])).trim();
|
|
2584
|
+
return out ? out.split(`
|
|
2585
|
+
`).map((s) => s.trim()).filter(Boolean) : [];
|
|
2586
|
+
} catch {
|
|
2587
|
+
return [];
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
async function commitSummaries(repoPath, base, branch, limit = 20) {
|
|
2591
|
+
try {
|
|
2592
|
+
const out = await git(repoPath).raw([
|
|
2593
|
+
"log",
|
|
2594
|
+
`--max-count=${limit}`,
|
|
2595
|
+
"--format=%h %s",
|
|
2596
|
+
`${base}..${branch}`
|
|
2597
|
+
]);
|
|
2598
|
+
return out.split(`
|
|
2599
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
2365
2600
|
} catch {
|
|
2366
2601
|
return [];
|
|
2367
2602
|
}
|
|
@@ -2547,7 +2782,7 @@ async function removeTalkWorktree(repoPath, worktreePath) {
|
|
|
2547
2782
|
try {
|
|
2548
2783
|
await g.raw(["worktree", "prune"]);
|
|
2549
2784
|
} catch {}
|
|
2550
|
-
if (
|
|
2785
|
+
if (existsSync6(worktreePath)) {
|
|
2551
2786
|
throw new InvalidInputError(`could not remove talk worktree at ${worktreePath} \u2014 a file may be locked; ` + "close anything using it and retry the delete");
|
|
2552
2787
|
}
|
|
2553
2788
|
}
|
|
@@ -2574,7 +2809,7 @@ async function deleteFeatureBranches(repoPath, slug) {
|
|
|
2574
2809
|
return { deleted, kept };
|
|
2575
2810
|
}
|
|
2576
2811
|
async function commitDocs(worktreePath, message) {
|
|
2577
|
-
if (!
|
|
2812
|
+
if (!existsSync6(resolve5(worktreePath, DOCS_PATHSPEC)))
|
|
2578
2813
|
return;
|
|
2579
2814
|
const g = git(worktreePath);
|
|
2580
2815
|
await g.add([DOCS_PATHSPEC]);
|
|
@@ -2619,6 +2854,8 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2619
2854
|
const devPaneId = testDriveState.devPaneId;
|
|
2620
2855
|
if (devPaneId)
|
|
2621
2856
|
stopDevPane(devPaneId);
|
|
2857
|
+
const teardownFailure = await runDriveHookStep(ctx, feature.id, project.repoPath, "teardown", project.driveStopCommand, driveEnvFor(ctx, project, feature, branch));
|
|
2858
|
+
const carriedChanges = await dirtyPaths(g);
|
|
2622
2859
|
await g.checkout(previousBranch2);
|
|
2623
2860
|
if (detachedWorktree2)
|
|
2624
2861
|
await reattachWorktree(detachedWorktree2, branch);
|
|
@@ -2628,7 +2865,21 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2628
2865
|
message: `test drive stopped \u2014 back on ${previousBranch2}`,
|
|
2629
2866
|
data: { branch: previousBranch2 }
|
|
2630
2867
|
});
|
|
2631
|
-
|
|
2868
|
+
if (carriedChanges.length > 0) {
|
|
2869
|
+
emit(ctx, feature.id, {
|
|
2870
|
+
type: "testdrive.carried_changes",
|
|
2871
|
+
message: `${carriedChanges.length} uncommitted file(s) came back with you onto ${previousBranch2}: ${carriedChanges.slice(0, 5).join(", ")}${carriedChanges.length > 5 ? ", \u2026" : ""}`,
|
|
2872
|
+
data: { branch: previousBranch2, files: carriedChanges }
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
const dbDrift = await detectDbDrift(ctx, project, feature, previousBranch2, branch);
|
|
2876
|
+
return {
|
|
2877
|
+
ok: true,
|
|
2878
|
+
branch: previousBranch2,
|
|
2879
|
+
...carriedChanges.length > 0 ? { carriedChanges } : {},
|
|
2880
|
+
...dbDrift ? { dbDrift } : {},
|
|
2881
|
+
...teardownFailure ? { hookFailure: teardownFailure } : {}
|
|
2882
|
+
};
|
|
2632
2883
|
}
|
|
2633
2884
|
const porcelain = (await g.raw(["status", "--porcelain"])).trim();
|
|
2634
2885
|
if (porcelain !== "")
|
|
@@ -2652,18 +2903,105 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2652
2903
|
message: `test driving ${branch} (was on ${previousBranch})`,
|
|
2653
2904
|
data: { branch, previousBranch }
|
|
2654
2905
|
});
|
|
2906
|
+
const driveEnv = driveEnvFor(ctx, project, feature, branch);
|
|
2907
|
+
const setupFailure = await runDriveHookStep(ctx, feature.id, project.repoPath, "setup", project.driveSetupCommand, driveEnv);
|
|
2655
2908
|
if (project.devCommand) {
|
|
2656
2909
|
const devPaneId = startDevPane({
|
|
2657
2910
|
ctx,
|
|
2658
2911
|
featureId: feature.id,
|
|
2659
2912
|
repoPath: project.repoPath,
|
|
2660
2913
|
devCommand: project.devCommand,
|
|
2914
|
+
env: driveEnv,
|
|
2661
2915
|
onUrl: (url) => recordDriveUrl(ctx, feature.id, url)
|
|
2662
2916
|
});
|
|
2663
2917
|
if (devPaneId)
|
|
2664
2918
|
testDriveState.devPaneId = devPaneId;
|
|
2665
2919
|
}
|
|
2666
|
-
return { ok: true, branch };
|
|
2920
|
+
return { ok: true, branch, ...setupFailure ? { hookFailure: setupFailure } : {} };
|
|
2921
|
+
}
|
|
2922
|
+
function driveEnvFor(ctx, project, feature, branch) {
|
|
2923
|
+
const { vars, unknown: unknown2 } = parseDriveEnv(project.driveEnv, { slug: feature.slug, branch });
|
|
2924
|
+
if (unknown2.length > 0) {
|
|
2925
|
+
emit(ctx, feature.id, {
|
|
2926
|
+
type: "testdrive.env_unknown_placeholder",
|
|
2927
|
+
message: `drive environment left ${unknown2.map((u) => `{{${u}}}`).join(", ")} unsubstituted \u2014 known variables are {{slug}}, {{branch}}, {{id}}`,
|
|
2928
|
+
data: { unknown: unknown2 }
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
if (Object.keys(vars).length > 0) {
|
|
2932
|
+
emit(ctx, feature.id, {
|
|
2933
|
+
type: "testdrive.env",
|
|
2934
|
+
message: describeDriveEnv(vars),
|
|
2935
|
+
data: { keys: Object.keys(vars) }
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
return driveProcessEnv(vars);
|
|
2939
|
+
}
|
|
2940
|
+
async function runDriveHookStep(ctx, featureId, repoPath, phase, command, env) {
|
|
2941
|
+
const cmd = command?.trim();
|
|
2942
|
+
if (!cmd)
|
|
2943
|
+
return;
|
|
2944
|
+
emit(ctx, featureId, {
|
|
2945
|
+
type: `testdrive.${phase}_started`,
|
|
2946
|
+
message: phase === "setup" ? `preparing environment: \`${cmd}\`` : `tearing down environment: \`${cmd}\``,
|
|
2947
|
+
data: { command: cmd }
|
|
2948
|
+
});
|
|
2949
|
+
const result = await runDriveHook(cmd, { cwd: repoPath, env });
|
|
2950
|
+
if (result.ok) {
|
|
2951
|
+
emit(ctx, featureId, {
|
|
2952
|
+
type: `testdrive.${phase}_ok`,
|
|
2953
|
+
message: describeHookResult(cmd, result),
|
|
2954
|
+
data: { command: cmd, durationMs: result.durationMs }
|
|
2955
|
+
});
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
emit(ctx, featureId, {
|
|
2959
|
+
type: `testdrive.${phase}_failed`,
|
|
2960
|
+
message: `${describeHookResult(cmd, result)}${result.output ? `
|
|
2961
|
+
${result.output}` : ""}`,
|
|
2962
|
+
data: {
|
|
2963
|
+
command: cmd,
|
|
2964
|
+
exitCode: result.exitCode,
|
|
2965
|
+
timedOut: result.timedOut,
|
|
2966
|
+
output: result.output
|
|
2967
|
+
}
|
|
2968
|
+
});
|
|
2969
|
+
return {
|
|
2970
|
+
phase,
|
|
2971
|
+
command: cmd,
|
|
2972
|
+
exitCode: result.exitCode,
|
|
2973
|
+
timedOut: result.timedOut,
|
|
2974
|
+
output: result.output
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
async function dirtyPaths(g) {
|
|
2978
|
+
try {
|
|
2979
|
+
const out = (await g.raw(["status", "--porcelain"])).trim();
|
|
2980
|
+
if (!out)
|
|
2981
|
+
return [];
|
|
2982
|
+
return out.split(`
|
|
2983
|
+
`).map((line) => line.slice(3).trim()).map((p) => p.includes(" -> ") ? p.split(" -> ").at(-1) ?? p : p).map((p) => p.replace(/^"|"$/g, "")).filter(Boolean);
|
|
2984
|
+
} catch {
|
|
2985
|
+
return [];
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
async function detectDbDrift(ctx, project, feature, previousBranch, driveBranch) {
|
|
2989
|
+
let files;
|
|
2990
|
+
try {
|
|
2991
|
+
files = migrationPaths(await diffPaths(project.repoPath, previousBranch, driveBranch));
|
|
2992
|
+
} catch {
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
if (files.length === 0)
|
|
2996
|
+
return;
|
|
2997
|
+
const resetCommand = project.dbResetCommand?.trim() || undefined;
|
|
2998
|
+
const drift = { files, ...resetCommand ? { resetCommand } : {} };
|
|
2999
|
+
emit(ctx, feature.id, {
|
|
3000
|
+
type: "testdrive.db_drift",
|
|
3001
|
+
message: `${driveBranch} and ${previousBranch} differ by ${files.length} migration file(s) \u2014 ` + "anything you migrated during the drive is still applied to your dev database, so the next " + `migrate on ${previousBranch} may report drift. ` + (resetCommand ? `Rebuild it with: ${resetCommand}` : 'Set a "Database reset command" in project settings to get a one-click fix here.'),
|
|
3002
|
+
data: { files, previousBranch, driveBranch, resetCommand: resetCommand ?? null }
|
|
3003
|
+
});
|
|
3004
|
+
return drift;
|
|
2667
3005
|
}
|
|
2668
3006
|
async function mergeFeature(project, feature) {
|
|
2669
3007
|
const target = feature.baseBranch ?? project.mainBranch;
|
|
@@ -2720,171 +3058,20 @@ async function mergeInProgress(g) {
|
|
|
2720
3058
|
return false;
|
|
2721
3059
|
}
|
|
2722
3060
|
}
|
|
2723
|
-
var DOCS_PATHSPEC = "docs/features", DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", TEMP_BRANCH_PREFIXES, TEMP_BRANCH_SLUG_MAX = 16, testDriveState;
|
|
3061
|
+
var DOCS_PATHSPEC = "docs/features", MIGRATION_DIR_RE, DRIZZLE_SQL_RE, DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", PREP_BRANCH_PREFIX = "runcastle/prep/", TEMP_BRANCH_PREFIXES, TEMP_BRANCH_SLUG_MAX = 16, testDriveState;
|
|
2724
3062
|
var init_git = __esm(() => {
|
|
2725
3063
|
init_paths();
|
|
2726
3064
|
init_errors();
|
|
2727
3065
|
init_dev_pane();
|
|
3066
|
+
init_drive_hooks();
|
|
3067
|
+
init_drive_env();
|
|
2728
3068
|
init_events();
|
|
2729
3069
|
init_repo();
|
|
3070
|
+
MIGRATION_DIR_RE = /(^|\/)(migrations|migrate)\//i;
|
|
3071
|
+
DRIZZLE_SQL_RE = /(^|\/)drizzle\/.*\.sql$/i;
|
|
2730
3072
|
TEMP_BRANCH_PREFIXES = [RESEARCH_BRANCH_PREFIX, TICKET_BRANCH_PREFIX];
|
|
2731
3073
|
});
|
|
2732
3074
|
|
|
2733
|
-
// src/services/knowledge.ts
|
|
2734
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync } from "fs";
|
|
2735
|
-
import { join as join7, relative, resolve as resolve4, sep } from "path";
|
|
2736
|
-
function scaffoldDocs(ctx, feature) {
|
|
2737
|
-
const project = projectForFeature(ctx, feature);
|
|
2738
|
-
const dir = featureDocsDir(project, feature);
|
|
2739
|
-
mkdirSync3(dir, { recursive: true });
|
|
2740
|
-
const briefPath = featureDocPath(project, feature, "brief.md");
|
|
2741
|
-
if (!existsSync8(briefPath)) {
|
|
2742
|
-
const created = new Date().toISOString();
|
|
2743
|
-
const brief = [
|
|
2744
|
-
`# ${feature.title}`,
|
|
2745
|
-
"",
|
|
2746
|
-
feature.oneLiner,
|
|
2747
|
-
"",
|
|
2748
|
-
`- Slug: ${feature.slug}`,
|
|
2749
|
-
`- Created: ${created}`,
|
|
2750
|
-
""
|
|
2751
|
-
].join(`
|
|
2752
|
-
`);
|
|
2753
|
-
writeFileSync(briefPath, brief, "utf8");
|
|
2754
|
-
}
|
|
2755
|
-
if (feature.mapped) {
|
|
2756
|
-
scaffoldMapDoc(project, feature);
|
|
2757
|
-
}
|
|
2758
|
-
emit(ctx, feature.id, {
|
|
2759
|
-
type: "docs.scaffolded",
|
|
2760
|
-
message: feature.mapped ? `scaffolded docs/features/${feature.slug}/{brief,map}.md` : `scaffolded docs/features/${feature.slug}/brief.md`
|
|
2761
|
-
});
|
|
2762
|
-
}
|
|
2763
|
-
function scaffoldMapDoc(project, feature, seed) {
|
|
2764
|
-
mkdirSync3(featureDocsDir(project, feature), { recursive: true });
|
|
2765
|
-
const mapPath = featureDocPath(project, feature, "map.md");
|
|
2766
|
-
if (existsSync8(mapPath))
|
|
2767
|
-
return;
|
|
2768
|
-
writeFileSync(mapPath, mapDocBody(feature, seed), "utf8");
|
|
2769
|
-
}
|
|
2770
|
-
function mapDocBody(feature, seed) {
|
|
2771
|
-
const seeded = {
|
|
2772
|
-
Destination: seed?.destination?.trim(),
|
|
2773
|
-
Notes: seed?.notes?.trim()
|
|
2774
|
-
};
|
|
2775
|
-
const lines = [`# ${feature.title} \u2014 map`, ""];
|
|
2776
|
-
for (const section of MAP_SECTIONS) {
|
|
2777
|
-
lines.push(`## ${section}`, "");
|
|
2778
|
-
const body = seeded[section];
|
|
2779
|
-
if (body)
|
|
2780
|
-
lines.push(body, "");
|
|
2781
|
-
}
|
|
2782
|
-
return lines.join(`
|
|
2783
|
-
`);
|
|
2784
|
-
}
|
|
2785
|
-
function listDocs(ctx, feature) {
|
|
2786
|
-
const project = projectForFeature(ctx, feature);
|
|
2787
|
-
const dir = featureDocsDir(project, feature);
|
|
2788
|
-
if (!existsSync8(dir))
|
|
2789
|
-
return [];
|
|
2790
|
-
return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join7(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
2791
|
-
}
|
|
2792
|
-
function readDoc(ctx, feature, relPath) {
|
|
2793
|
-
const project = projectForFeature(ctx, feature);
|
|
2794
|
-
const dir = featureDocsDir(project, feature);
|
|
2795
|
-
const root = resolve4(dir);
|
|
2796
|
-
const target = resolve4(dir, relPath);
|
|
2797
|
-
const rel = relative(root, target);
|
|
2798
|
-
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
2799
|
-
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
2800
|
-
}
|
|
2801
|
-
if (!existsSync8(target))
|
|
2802
|
-
throw new NotFoundError(`doc not found: ${relPath}`);
|
|
2803
|
-
return { content: readFileSync3(target, "utf8") };
|
|
2804
|
-
}
|
|
2805
|
-
function readTitle(path, fallback) {
|
|
2806
|
-
try {
|
|
2807
|
-
const firstLine = readFileSync3(path, "utf8").split(`
|
|
2808
|
-
`, 1)[0] ?? "";
|
|
2809
|
-
const heading = firstLine.match(/^#\s+(.+)$/);
|
|
2810
|
-
return heading ? heading[1].trim() : fallback;
|
|
2811
|
-
} catch {
|
|
2812
|
-
return fallback;
|
|
2813
|
-
}
|
|
2814
|
-
}
|
|
2815
|
-
var MAP_SECTIONS;
|
|
2816
|
-
var init_knowledge = __esm(() => {
|
|
2817
|
-
init_errors();
|
|
2818
|
-
init_events();
|
|
2819
|
-
init_feature_docs();
|
|
2820
|
-
init_repo();
|
|
2821
|
-
MAP_SECTIONS = [
|
|
2822
|
-
"Destination",
|
|
2823
|
-
"Notes",
|
|
2824
|
-
"Not yet specified",
|
|
2825
|
-
"Out of scope"
|
|
2826
|
-
];
|
|
2827
|
-
});
|
|
2828
|
-
|
|
2829
|
-
// src/pty/end-session.ts
|
|
2830
|
-
function endSession(ctx, sessionId) {
|
|
2831
|
-
const registry = ptyRegistry();
|
|
2832
|
-
const killed = registry.kill(sessionId);
|
|
2833
|
-
registry.remove(sessionId);
|
|
2834
|
-
const session = markSessionEnded(ctx, sessionId);
|
|
2835
|
-
if (session) {
|
|
2836
|
-
releaseForSession(ctx, sessionId);
|
|
2837
|
-
emit(ctx, session.featureId, {
|
|
2838
|
-
type: "session.ended",
|
|
2839
|
-
message: "session ended by user",
|
|
2840
|
-
data: { sessionId, killed }
|
|
2841
|
-
});
|
|
2842
|
-
} else {
|
|
2843
|
-
const existing = getSessionRow(ctx, sessionId);
|
|
2844
|
-
if (!existing)
|
|
2845
|
-
return { sessionId, ok: false, killed };
|
|
2846
|
-
}
|
|
2847
|
-
return { sessionId, ok: true, killed };
|
|
2848
|
-
}
|
|
2849
|
-
var init_end_session = __esm(() => {
|
|
2850
|
-
init_sessions();
|
|
2851
|
-
init_events();
|
|
2852
|
-
init_waypoints();
|
|
2853
|
-
init_registry();
|
|
2854
|
-
});
|
|
2855
|
-
|
|
2856
|
-
// src/launcher/skills-root.ts
|
|
2857
|
-
import { existsSync as existsSync9 } from "fs";
|
|
2858
|
-
import { dirname as dirname2, join as join8, resolve as resolve5 } from "path";
|
|
2859
|
-
function resolveSkillsRoot(fromDir) {
|
|
2860
|
-
const override = process.env[SKILLS_DIR_ENV];
|
|
2861
|
-
if (override) {
|
|
2862
|
-
const root = resolve5(override);
|
|
2863
|
-
if (existsSync9(join8(root, SKILLS_MARKER)))
|
|
2864
|
-
return root;
|
|
2865
|
-
throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
|
|
2866
|
-
}
|
|
2867
|
-
const searched = [];
|
|
2868
|
-
let dir = fromDir;
|
|
2869
|
-
for (let i = 0;i < 8; i += 1) {
|
|
2870
|
-
const candidate = join8(dir, "packages", "skills");
|
|
2871
|
-
searched.push(candidate);
|
|
2872
|
-
if (existsSync9(join8(candidate, SKILLS_MARKER)))
|
|
2873
|
-
return candidate;
|
|
2874
|
-
const parent = dirname2(dir);
|
|
2875
|
-
if (parent === dir)
|
|
2876
|
-
break;
|
|
2877
|
-
dir = parent;
|
|
2878
|
-
}
|
|
2879
|
-
throw new Error(`runcastle skills root not found; searched:
|
|
2880
|
-
${searched.join(`
|
|
2881
|
-
`)}`);
|
|
2882
|
-
}
|
|
2883
|
-
var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
|
|
2884
|
-
var init_skills_root = __esm(() => {
|
|
2885
|
-
SKILLS_MARKER = join8("packs", "runcastle");
|
|
2886
|
-
});
|
|
2887
|
-
|
|
2888
3075
|
// src/services/agent-stream.ts
|
|
2889
3076
|
function beginTranscript(ticketId) {
|
|
2890
3077
|
transcripts.set(ticketId, { live: true, chunks: [], nextIndex: 0, bytes: 0 });
|
|
@@ -3013,8 +3200,8 @@ var init_burn_guard = __esm(() => {
|
|
|
3013
3200
|
});
|
|
3014
3201
|
|
|
3015
3202
|
// src/workflows/ticket-burner.ts
|
|
3016
|
-
import { existsSync as
|
|
3017
|
-
import { dirname as dirname3, join as
|
|
3203
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
3204
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
3018
3205
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3019
3206
|
import { claudeCode, run } from "@ai-hero/sandcastle";
|
|
3020
3207
|
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
@@ -3063,7 +3250,7 @@ function detectCycle(tickets2) {
|
|
|
3063
3250
|
}
|
|
3064
3251
|
return null;
|
|
3065
3252
|
}
|
|
3066
|
-
function
|
|
3253
|
+
function renderTemplate2(template, values) {
|
|
3067
3254
|
let out = template;
|
|
3068
3255
|
for (const [key, value] of Object.entries(values)) {
|
|
3069
3256
|
out = out.split(`{{${key}}}`).join(value);
|
|
@@ -3071,7 +3258,7 @@ function renderTemplate(template, values) {
|
|
|
3071
3258
|
return out;
|
|
3072
3259
|
}
|
|
3073
3260
|
function renderTicketPrompt(template, values) {
|
|
3074
|
-
return
|
|
3261
|
+
return renderTemplate2(template, values);
|
|
3075
3262
|
}
|
|
3076
3263
|
function buildTicketJson(ticket) {
|
|
3077
3264
|
return JSON.stringify({
|
|
@@ -3130,13 +3317,13 @@ function parseEnvFile(content) {
|
|
|
3130
3317
|
if (line.length === 0 || line.startsWith("#"))
|
|
3131
3318
|
continue;
|
|
3132
3319
|
const body = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
3133
|
-
const
|
|
3134
|
-
if (
|
|
3320
|
+
const eq5 = body.indexOf("=");
|
|
3321
|
+
if (eq5 === -1)
|
|
3135
3322
|
continue;
|
|
3136
|
-
const key = body.slice(0,
|
|
3323
|
+
const key = body.slice(0, eq5).trim();
|
|
3137
3324
|
if (key.length === 0)
|
|
3138
3325
|
continue;
|
|
3139
|
-
let value = body.slice(
|
|
3326
|
+
let value = body.slice(eq5 + 1).trim();
|
|
3140
3327
|
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
3141
3328
|
value = value.slice(1, -1);
|
|
3142
3329
|
}
|
|
@@ -3159,6 +3346,9 @@ function detectPackageManager(tc) {
|
|
|
3159
3346
|
return "npm";
|
|
3160
3347
|
return tc.hasPackageJson ? "npm" : undefined;
|
|
3161
3348
|
}
|
|
3349
|
+
function orFallback(strict, permissive) {
|
|
3350
|
+
return `( ${strict} || ${permissive} )`;
|
|
3351
|
+
}
|
|
3162
3352
|
function resolveSetupCommand(tc, override) {
|
|
3163
3353
|
const trimmed = override?.trim();
|
|
3164
3354
|
if (trimmed)
|
|
@@ -3168,13 +3358,13 @@ function resolveSetupCommand(tc, override) {
|
|
|
3168
3358
|
return;
|
|
3169
3359
|
switch (pm) {
|
|
3170
3360
|
case "bun":
|
|
3171
|
-
return tc.lockfiles.bun ? "bun install --frozen-lockfile" : "bun install";
|
|
3361
|
+
return tc.lockfiles.bun ? orFallback("bun install --frozen-lockfile", "bun install") : "bun install";
|
|
3172
3362
|
case "pnpm":
|
|
3173
|
-
return tc.lockfiles.pnpm ? "corepack pnpm install --frozen-lockfile" : "corepack pnpm install";
|
|
3363
|
+
return tc.lockfiles.pnpm ? orFallback("corepack pnpm install --frozen-lockfile", "corepack pnpm install") : "corepack pnpm install";
|
|
3174
3364
|
case "yarn":
|
|
3175
|
-
return tc.lockfiles.yarn ? "corepack yarn install --frozen-lockfile" : "corepack yarn install";
|
|
3365
|
+
return tc.lockfiles.yarn ? orFallback("corepack yarn install --frozen-lockfile", "corepack yarn install") : "corepack yarn install";
|
|
3176
3366
|
case "npm":
|
|
3177
|
-
return tc.lockfiles.npm ? "npm ci" : "npm install";
|
|
3367
|
+
return tc.lockfiles.npm ? orFallback("npm ci", "npm install") : "npm install";
|
|
3178
3368
|
}
|
|
3179
3369
|
}
|
|
3180
3370
|
function cacheMountFor(pm, hostPath) {
|
|
@@ -3237,12 +3427,12 @@ function buildVerifyNotes(config2) {
|
|
|
3237
3427
|
`);
|
|
3238
3428
|
}
|
|
3239
3429
|
function readRepoToolchain(repoPath) {
|
|
3240
|
-
const pkgPath =
|
|
3241
|
-
const hasPackageJson =
|
|
3430
|
+
const pkgPath = join7(repoPath, "package.json");
|
|
3431
|
+
const hasPackageJson = existsSync7(pkgPath);
|
|
3242
3432
|
let packageManagerField;
|
|
3243
3433
|
if (hasPackageJson) {
|
|
3244
3434
|
try {
|
|
3245
|
-
const pkg = JSON.parse(
|
|
3435
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3246
3436
|
if (typeof pkg.packageManager === "string")
|
|
3247
3437
|
packageManagerField = pkg.packageManager;
|
|
3248
3438
|
} catch {}
|
|
@@ -3251,14 +3441,14 @@ function readRepoToolchain(repoPath) {
|
|
|
3251
3441
|
hasPackageJson,
|
|
3252
3442
|
...packageManagerField !== undefined ? { packageManagerField } : {},
|
|
3253
3443
|
lockfiles: {
|
|
3254
|
-
bun:
|
|
3255
|
-
pnpm:
|
|
3256
|
-
yarn:
|
|
3257
|
-
npm:
|
|
3444
|
+
bun: existsSync7(join7(repoPath, "bun.lock")) || existsSync7(join7(repoPath, "bun.lockb")),
|
|
3445
|
+
pnpm: existsSync7(join7(repoPath, "pnpm-lock.yaml")),
|
|
3446
|
+
yarn: existsSync7(join7(repoPath, "yarn.lock")),
|
|
3447
|
+
npm: existsSync7(join7(repoPath, "package-lock.json"))
|
|
3258
3448
|
}
|
|
3259
3449
|
};
|
|
3260
3450
|
}
|
|
3261
|
-
function createStreamThrottle(
|
|
3451
|
+
function createStreamThrottle(emit3, opts = {}) {
|
|
3262
3452
|
const intervalMs = opts.intervalMs ?? 2000;
|
|
3263
3453
|
const maxChars = opts.maxChars ?? 500;
|
|
3264
3454
|
const now = opts.now ?? Date.now;
|
|
@@ -3268,7 +3458,7 @@ function createStreamThrottle(emit2, opts = {}) {
|
|
|
3268
3458
|
const flushText = () => {
|
|
3269
3459
|
if (buffer.length === 0)
|
|
3270
3460
|
return;
|
|
3271
|
-
|
|
3461
|
+
emit3({ type: "burn.text", message: buffer, data: { iteration: lastIteration } });
|
|
3272
3462
|
buffer = "";
|
|
3273
3463
|
lastFlush = now();
|
|
3274
3464
|
};
|
|
@@ -3283,7 +3473,7 @@ function createStreamThrottle(emit2, opts = {}) {
|
|
|
3283
3473
|
flushText();
|
|
3284
3474
|
const rawArgs = event.formattedArgs ?? "";
|
|
3285
3475
|
const args = rawArgs.length > 200 ? `${rawArgs.slice(0, 200)}\u2026` : rawArgs;
|
|
3286
|
-
|
|
3476
|
+
emit3({
|
|
3287
3477
|
type: "burn.tool",
|
|
3288
3478
|
message: args.length > 0 ? `${event.name} ${args}` : event.name,
|
|
3289
3479
|
data: { name: event.name, args: rawArgs, iteration: event.iteration }
|
|
@@ -3429,38 +3619,38 @@ function createSerialQueue() {
|
|
|
3429
3619
|
};
|
|
3430
3620
|
}
|
|
3431
3621
|
async function landWithResolve(branch, deps) {
|
|
3432
|
-
let
|
|
3622
|
+
let current = branch;
|
|
3433
3623
|
for (let attempt = 1;; attempt++) {
|
|
3434
|
-
const merge = await deps.merge(
|
|
3624
|
+
const merge = await deps.merge(current);
|
|
3435
3625
|
if (merge.ok)
|
|
3436
|
-
return { status: "landed", branch:
|
|
3626
|
+
return { status: "landed", branch: current };
|
|
3437
3627
|
const detail = merge.error ?? "merge failed";
|
|
3438
3628
|
if (!merge.conflict)
|
|
3439
|
-
return { status: "failed", branch:
|
|
3629
|
+
return { status: "failed", branch: current, error: detail };
|
|
3440
3630
|
const files = merge.files ?? [];
|
|
3441
3631
|
if (attempt > deps.maxResolveAttempts) {
|
|
3442
|
-
return { status: "conflict", branch:
|
|
3632
|
+
return { status: "conflict", branch: current, files, error: detail };
|
|
3443
3633
|
}
|
|
3444
3634
|
const where = files.length > 0 ? `${files.length} file(s)` : "the merge";
|
|
3445
3635
|
deps.emit({
|
|
3446
3636
|
type: "merge.conflict.resolving",
|
|
3447
3637
|
message: `${deps.label}: conflicts with ${deps.featureBranch} on ${where} \u2014 resolving with an agent (pass ${attempt}/${deps.maxResolveAttempts})`,
|
|
3448
|
-
data: { branch:
|
|
3638
|
+
data: { branch: current, files, attempt, maxAttempts: deps.maxResolveAttempts }
|
|
3449
3639
|
});
|
|
3450
3640
|
const resolved = await deps.resolve({
|
|
3451
|
-
branch:
|
|
3641
|
+
branch: current,
|
|
3452
3642
|
files,
|
|
3453
3643
|
attempt,
|
|
3454
3644
|
maxAttempts: deps.maxResolveAttempts
|
|
3455
3645
|
});
|
|
3456
|
-
|
|
3646
|
+
current = resolved.branch;
|
|
3457
3647
|
if (!resolved.ok) {
|
|
3458
|
-
return { status: "conflict", branch:
|
|
3648
|
+
return { status: "conflict", branch: current, files, error: resolved.error ?? detail };
|
|
3459
3649
|
}
|
|
3460
3650
|
deps.emit({
|
|
3461
3651
|
type: "merge.conflict.resolved",
|
|
3462
|
-
message: `${deps.label}: conflict resolved on ${
|
|
3463
|
-
data: { branch:
|
|
3652
|
+
message: `${deps.label}: conflict resolved on ${current} \u2014 landing`,
|
|
3653
|
+
data: { branch: current, files, attempt }
|
|
3464
3654
|
});
|
|
3465
3655
|
}
|
|
3466
3656
|
}
|
|
@@ -3618,26 +3808,26 @@ function resolverTemplatePath() {
|
|
|
3618
3808
|
}
|
|
3619
3809
|
function burnerAssetPath(file) {
|
|
3620
3810
|
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
3621
|
-
return
|
|
3811
|
+
return join7(resolveSkillsRoot(here), "burner", file);
|
|
3622
3812
|
}
|
|
3623
3813
|
function readDocsDigestFromDisk(projectId, slug) {
|
|
3624
3814
|
const worktree = worktreeDir(projectId, slug);
|
|
3625
|
-
if (!
|
|
3815
|
+
if (!existsSync7(worktree))
|
|
3626
3816
|
return "_No talk worktree on disk \u2014 docs digest skipped._";
|
|
3627
|
-
const docsDir =
|
|
3628
|
-
if (!
|
|
3817
|
+
const docsDir = join7(worktree, ...featureDocsRel(slug).split("/"));
|
|
3818
|
+
if (!existsSync7(docsDir))
|
|
3629
3819
|
return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
|
|
3630
|
-
const files =
|
|
3820
|
+
const files = readdirSync2(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync3(join7(docsDir, e.name), "utf8") }));
|
|
3631
3821
|
return buildDocsDigest(files);
|
|
3632
3822
|
}
|
|
3633
3823
|
function readBlockedFile(dirs) {
|
|
3634
3824
|
for (const dir of dirs) {
|
|
3635
3825
|
if (!dir)
|
|
3636
3826
|
continue;
|
|
3637
|
-
const p =
|
|
3638
|
-
if (
|
|
3827
|
+
const p = join7(dir, "BLOCKED.md");
|
|
3828
|
+
if (existsSync7(p)) {
|
|
3639
3829
|
try {
|
|
3640
|
-
return
|
|
3830
|
+
return readFileSync3(p, "utf8");
|
|
3641
3831
|
} catch {}
|
|
3642
3832
|
}
|
|
3643
3833
|
}
|
|
@@ -3690,8 +3880,9 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3690
3880
|
const featureBrief = buildFeatureBrief(feature);
|
|
3691
3881
|
const docsDigest = readDocsDigestFromDisk(project.id, feature.slug);
|
|
3692
3882
|
const workspaceNotes = buildWorkspaceNotes(workspaceMode);
|
|
3693
|
-
const
|
|
3694
|
-
const
|
|
3883
|
+
const prepared = resolvePreparedSettings(config2, project);
|
|
3884
|
+
const verifyNotes = buildVerifyNotes(prepared);
|
|
3885
|
+
const basePrompt = renderTicketPrompt(readFileSync3(burnerTemplatePath(), "utf8"), {
|
|
3695
3886
|
TICKET_JSON: ticketJson,
|
|
3696
3887
|
FEATURE_BRIEF: featureBrief,
|
|
3697
3888
|
DOCS_DIGEST: docsDigest,
|
|
@@ -3701,19 +3892,19 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3701
3892
|
});
|
|
3702
3893
|
const toolchain = readRepoToolchain(project.repoPath);
|
|
3703
3894
|
const pm = detectPackageManager(toolchain);
|
|
3704
|
-
const setupCommand = resolveSetupCommand(toolchain,
|
|
3895
|
+
const setupCommand = resolveSetupCommand(toolchain, prepared.setupCommand);
|
|
3705
3896
|
const mounts = [];
|
|
3706
3897
|
if (config2.sandbox !== "noSandbox" && pm) {
|
|
3707
3898
|
const mount = cacheMountFor(pm, burnCacheDir(pm));
|
|
3708
3899
|
if (mount) {
|
|
3709
|
-
|
|
3900
|
+
mkdirSync3(mount.hostPath, { recursive: true });
|
|
3710
3901
|
mounts.push(mount);
|
|
3711
3902
|
}
|
|
3712
3903
|
}
|
|
3713
3904
|
const guardInstall = config2.burnGuard && config2.sandbox !== "noSandbox" ? buildGuardInstallCommand() : undefined;
|
|
3714
3905
|
const withGuard = (setup) => guardInstall === undefined ? setup : setup ? `${guardInstall} && ${setup}` : guardInstall;
|
|
3715
|
-
|
|
3716
|
-
const logFilePath =
|
|
3906
|
+
mkdirSync3(logsDir(), { recursive: true });
|
|
3907
|
+
const logFilePath = join7(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
|
|
3717
3908
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
3718
3909
|
beginTranscript(ticket.id);
|
|
3719
3910
|
const timer = createToolTimer();
|
|
@@ -3759,7 +3950,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3759
3950
|
const runResolver = async (input) => {
|
|
3760
3951
|
const resolveBranch = ticketBranchName(feature.slug, ticket.seq, newId("r").slice(2, 10));
|
|
3761
3952
|
const otherSide = await commitSummaries(project.repoPath, input.branch, feature.branch);
|
|
3762
|
-
const prompt =
|
|
3953
|
+
const prompt = renderTemplate2(readFileSync3(resolverTemplatePath(), "utf8"), {
|
|
3763
3954
|
TICKET_JSON: ticketJson,
|
|
3764
3955
|
FEATURE_BRIEF: featureBrief,
|
|
3765
3956
|
DOCS_DIGEST: docsDigest,
|
|
@@ -4062,9 +4253,9 @@ function resolveBurnDeps(ctx) {
|
|
|
4062
4253
|
}
|
|
4063
4254
|
function readTokenFromEnvFile(path) {
|
|
4064
4255
|
let fromFile;
|
|
4065
|
-
if (
|
|
4256
|
+
if (existsSync7(path)) {
|
|
4066
4257
|
try {
|
|
4067
|
-
fromFile = parseEnvFile(
|
|
4258
|
+
fromFile = parseEnvFile(readFileSync3(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
|
|
4068
4259
|
} catch {
|
|
4069
4260
|
fromFile = undefined;
|
|
4070
4261
|
}
|
|
@@ -4135,19 +4326,933 @@ var init_ticket_burner = __esm(() => {
|
|
|
4135
4326
|
/\bapi error\b/i,
|
|
4136
4327
|
/session capture failed/i
|
|
4137
4328
|
];
|
|
4138
|
-
activeTicketAborts = new Map;
|
|
4139
|
-
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
4140
|
-
ticketBurner = {
|
|
4141
|
-
id: "ticket-burner",
|
|
4142
|
-
run: (ctx) => burnRun(ctx, resolveBurnDeps(ctx))
|
|
4143
|
-
};
|
|
4329
|
+
activeTicketAborts = new Map;
|
|
4330
|
+
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
4331
|
+
ticketBurner = {
|
|
4332
|
+
id: "ticket-burner",
|
|
4333
|
+
run: (ctx) => burnRun(ctx, resolveBurnDeps(ctx))
|
|
4334
|
+
};
|
|
4335
|
+
});
|
|
4336
|
+
|
|
4337
|
+
// src/workflows/project-prep.ts
|
|
4338
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "fs";
|
|
4339
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
4340
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4341
|
+
import { run as run2 } from "@ai-hero/sandcastle";
|
|
4342
|
+
import * as z3 from "zod";
|
|
4343
|
+
function parsePrepFindings(raw) {
|
|
4344
|
+
let json;
|
|
4345
|
+
try {
|
|
4346
|
+
json = JSON.parse(raw);
|
|
4347
|
+
} catch (e) {
|
|
4348
|
+
throw new Error(`findings file is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
4349
|
+
}
|
|
4350
|
+
const outer = RawFindings.safeParse(json);
|
|
4351
|
+
if (!outer.success)
|
|
4352
|
+
throw new Error("findings file is not a JSON object");
|
|
4353
|
+
const warnings = [];
|
|
4354
|
+
const values = {};
|
|
4355
|
+
const known = new Set(PREPARED_KEYS);
|
|
4356
|
+
for (const [key, rawValue] of Object.entries(outer.data)) {
|
|
4357
|
+
if (key === "notes")
|
|
4358
|
+
continue;
|
|
4359
|
+
if (!known.has(key)) {
|
|
4360
|
+
warnings.push(`ignored unknown key "${key}"`);
|
|
4361
|
+
continue;
|
|
4362
|
+
}
|
|
4363
|
+
const parsed = RawFinding.safeParse(rawValue);
|
|
4364
|
+
if (!parsed.success) {
|
|
4365
|
+
warnings.push(`ignored "${key}": not a string or {value, evidence} object`);
|
|
4366
|
+
continue;
|
|
4367
|
+
}
|
|
4368
|
+
const entry = parsed.data;
|
|
4369
|
+
const value = (typeof entry === "string" ? entry : entry.value ?? "").trim();
|
|
4370
|
+
if (value === "")
|
|
4371
|
+
continue;
|
|
4372
|
+
const evidence = typeof entry === "string" ? undefined : entry.evidence?.trim();
|
|
4373
|
+
values[key] = { value, ...evidence ? { evidence } : {} };
|
|
4374
|
+
}
|
|
4375
|
+
const notes = outer.data.notes?.trim();
|
|
4376
|
+
return { findings: { values, ...notes ? { notes } : {} }, warnings };
|
|
4377
|
+
}
|
|
4378
|
+
function renderPrepPrompt(template, values) {
|
|
4379
|
+
let out = template;
|
|
4380
|
+
for (const key of PLACEHOLDERS) {
|
|
4381
|
+
out = out.split(`{{${key}}}`).join(values[key]);
|
|
4382
|
+
}
|
|
4383
|
+
return out;
|
|
4384
|
+
}
|
|
4385
|
+
function buildRequestedKeysBlock(keys) {
|
|
4386
|
+
if (keys.length === 0) {
|
|
4387
|
+
return "_Everything is already established. Report an empty findings object and stop._";
|
|
4388
|
+
}
|
|
4389
|
+
const order = new Map(PREPARED_KEYS.map((k, i) => [k, i]));
|
|
4390
|
+
return [...keys].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0)).map((k) => `- ${KEY_BRIEF[k]}`).join(`
|
|
4391
|
+
`);
|
|
4392
|
+
}
|
|
4393
|
+
async function prepRun(ctx, deps) {
|
|
4394
|
+
if (ctx.keys.length === 0) {
|
|
4395
|
+
ctx.emitEvent({
|
|
4396
|
+
type: "prep.skipped",
|
|
4397
|
+
message: "nothing to prepare \u2014 every field is already set"
|
|
4398
|
+
});
|
|
4399
|
+
return { status: "succeeded", summary: "nothing to prepare" };
|
|
4400
|
+
}
|
|
4401
|
+
if (deps.config.sandbox !== "noSandbox" && !deps.hasAuthToken) {
|
|
4402
|
+
ctx.emitEvent({ type: AUTH_MISSING_EVENT2, message: AUTH_MISSING_MESSAGE2 });
|
|
4403
|
+
return { status: "failed", summary: "preparation aborted: auth token missing" };
|
|
4404
|
+
}
|
|
4405
|
+
ctx.emitEvent({
|
|
4406
|
+
type: "prep.started",
|
|
4407
|
+
message: `preparing ${ctx.project.name} \u2014 establishing ${ctx.keys.join(", ")}`,
|
|
4408
|
+
data: { keys: [...ctx.keys] }
|
|
4409
|
+
});
|
|
4410
|
+
const outcome = await deps.executePrepRun(ctx);
|
|
4411
|
+
if (outcome.status === "failed") {
|
|
4412
|
+
ctx.emitEvent({
|
|
4413
|
+
type: "prep.failed",
|
|
4414
|
+
message: `preparation failed: ${errorHeadline2(outcome.error)}`,
|
|
4415
|
+
data: { error: outcome.error }
|
|
4416
|
+
});
|
|
4417
|
+
return { status: "failed", summary: outcome.error };
|
|
4418
|
+
}
|
|
4419
|
+
for (const warning of outcome.warnings) {
|
|
4420
|
+
ctx.emitEvent({ type: "prep.warning", message: `preparation: ${warning}` });
|
|
4421
|
+
}
|
|
4422
|
+
const found = Object.keys(outcome.findings.values);
|
|
4423
|
+
const missed = ctx.keys.filter((k) => !found.includes(k));
|
|
4424
|
+
const summary = found.length === 0 ? "preparation established nothing" : `established ${found.join(", ")}${missed.length > 0 ? ` (could not establish ${missed.join(", ")})` : ""}`;
|
|
4425
|
+
return { status: "succeeded", summary, findings: outcome.findings };
|
|
4426
|
+
}
|
|
4427
|
+
function errorHeadline2(s) {
|
|
4428
|
+
const lines = s.split(`
|
|
4429
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
4430
|
+
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
4431
|
+
return causes.at(-1) ?? lines[0] ?? "";
|
|
4432
|
+
}
|
|
4433
|
+
function createPrepStreamThrottle(emit3, opts = {}) {
|
|
4434
|
+
return createStreamThrottle((e) => emit3({ ...e, type: e.type.replace(/^burn\./, "prep.") }), opts);
|
|
4435
|
+
}
|
|
4436
|
+
function nonFatalSetup(setupCommand) {
|
|
4437
|
+
return `( ${setupCommand} || true )`;
|
|
4438
|
+
}
|
|
4439
|
+
function prepTemplatePath() {
|
|
4440
|
+
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
4441
|
+
return join8(resolveSkillsRoot(here), "burner", "prepare-project.md");
|
|
4442
|
+
}
|
|
4443
|
+
async function realExecutePrepRun(ctx, config2, token, model) {
|
|
4444
|
+
const { project } = ctx;
|
|
4445
|
+
const branch = prepBranchName(newId("b").slice(2, 10));
|
|
4446
|
+
const toolchain = readRepoToolchain(project.repoPath);
|
|
4447
|
+
const pm = detectPackageManager(toolchain);
|
|
4448
|
+
const prepared = resolvePreparedSettings(config2, project);
|
|
4449
|
+
const setupCommand = resolveSetupCommand(toolchain, prepared.setupCommand);
|
|
4450
|
+
const workspaceMode = resolveBurnWorkspaceMode(config2);
|
|
4451
|
+
const mounts = [];
|
|
4452
|
+
if (config2.sandbox !== "noSandbox" && pm) {
|
|
4453
|
+
const mount = cacheMountFor(pm, burnCacheDir(pm));
|
|
4454
|
+
if (mount) {
|
|
4455
|
+
mkdirSync4(mount.hostPath, { recursive: true });
|
|
4456
|
+
mounts.push(mount);
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
const template = readFileSync4(prepTemplatePath(), "utf8");
|
|
4460
|
+
const prompt = renderPrepPrompt(template, {
|
|
4461
|
+
REQUESTED_KEYS: buildRequestedKeysBlock(ctx.keys),
|
|
4462
|
+
SETUP_COMMAND: setupCommand ?? "(no install step detected)"
|
|
4463
|
+
});
|
|
4464
|
+
mkdirSync4(logsDir(), { recursive: true });
|
|
4465
|
+
const logFilePath = join8(logsDir(), `prep-${project.id}.log`);
|
|
4466
|
+
const throttle = createPrepStreamThrottle((e) => ctx.emitEvent(e));
|
|
4467
|
+
if (setupCommand) {
|
|
4468
|
+
ctx.emitEvent({
|
|
4469
|
+
type: "prep.setup",
|
|
4470
|
+
message: `attempting a dependency install before the agent starts (best-effort): ${setupCommand}`
|
|
4471
|
+
});
|
|
4472
|
+
}
|
|
4473
|
+
const prepSetupCommand = setupCommand ? nonFatalSetup(setupCommand) : undefined;
|
|
4474
|
+
const hookCommand = workspaceMode === "isolated" ? buildIsolatedSetupCommand(branch, prepSetupCommand, pm) : prepSetupCommand;
|
|
4475
|
+
const runOptions = {
|
|
4476
|
+
agent: buildBurnAgent(config2, token, model),
|
|
4477
|
+
sandbox: selectSandbox(config2, mounts),
|
|
4478
|
+
cwd: project.repoPath,
|
|
4479
|
+
prompt,
|
|
4480
|
+
branchStrategy: { type: "branch", branch, baseBranch: project.mainBranch },
|
|
4481
|
+
signal: ctx.signal,
|
|
4482
|
+
name: "prepare",
|
|
4483
|
+
maxIterations: 1,
|
|
4484
|
+
...hookCommand ? {
|
|
4485
|
+
hooks: {
|
|
4486
|
+
sandbox: {
|
|
4487
|
+
onSandboxReady: [{ command: hookCommand, timeoutMs: PREP_SETUP_TIMEOUT_MS }]
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
} : {},
|
|
4491
|
+
logging: { type: "file", path: logFilePath, onAgentStreamEvent: throttle.onEvent }
|
|
4492
|
+
};
|
|
4493
|
+
try {
|
|
4494
|
+
let result;
|
|
4495
|
+
try {
|
|
4496
|
+
result = await run2(runOptions);
|
|
4497
|
+
} catch (err) {
|
|
4498
|
+
throttle.flush();
|
|
4499
|
+
if (ctx.signal.aborted)
|
|
4500
|
+
throw err;
|
|
4501
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4502
|
+
const salvaged = await readFindings(project.repoPath, branch);
|
|
4503
|
+
if (!salvaged)
|
|
4504
|
+
return { status: "failed", error: msg };
|
|
4505
|
+
ctx.emitEvent({
|
|
4506
|
+
type: "prep.salvaged",
|
|
4507
|
+
message: `the agent finished but sandcastle errored on teardown (${errorHeadline2(msg)}) \u2014 reading the findings it committed anyway`
|
|
4508
|
+
});
|
|
4509
|
+
return salvaged;
|
|
4510
|
+
}
|
|
4511
|
+
throttle.flush();
|
|
4512
|
+
if (result.commits.length === 0) {
|
|
4513
|
+
return {
|
|
4514
|
+
status: "failed",
|
|
4515
|
+
error: `agent produced no commits \u2014 no ${PREP_FINDINGS_PATH} landed`
|
|
4516
|
+
};
|
|
4517
|
+
}
|
|
4518
|
+
const read = await readFindings(project.repoPath, branch);
|
|
4519
|
+
return read ?? {
|
|
4520
|
+
status: "failed",
|
|
4521
|
+
error: `agent committed, but ${PREP_FINDINGS_PATH} is missing from ${branch}`
|
|
4522
|
+
};
|
|
4523
|
+
} finally {
|
|
4524
|
+
await deletePrepBranch(project.repoPath, branch);
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
async function readFindings(repoPath, branch) {
|
|
4528
|
+
const raw = await readFileAtRef(repoPath, branch, PREP_FINDINGS_PATH);
|
|
4529
|
+
if (raw === undefined)
|
|
4530
|
+
return;
|
|
4531
|
+
try {
|
|
4532
|
+
const { findings, warnings } = parsePrepFindings(raw);
|
|
4533
|
+
return { status: "done", findings, warnings };
|
|
4534
|
+
} catch (e) {
|
|
4535
|
+
return { status: "failed", error: e instanceof Error ? e.message : String(e) };
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
function resolvePrepDeps(project) {
|
|
4539
|
+
const config2 = loadConfig();
|
|
4540
|
+
const token = readTokenFromEnvFile(envPath());
|
|
4541
|
+
const model = resolveModel("prepare", config2, project);
|
|
4542
|
+
return {
|
|
4543
|
+
config: config2,
|
|
4544
|
+
hasAuthToken: token !== undefined,
|
|
4545
|
+
executePrepRun: (c) => realExecutePrepRun(c, config2, token, model)
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
4548
|
+
async function prepHeadSha(project) {
|
|
4549
|
+
return existsSync8(project.repoPath) ? headSha(project.repoPath, project.mainBranch) : undefined;
|
|
4550
|
+
}
|
|
4551
|
+
var AUTH_MISSING_EVENT2 = "auth.missing", AUTH_MISSING_MESSAGE2 = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PREP_FINDINGS_PATH = ".runcastle/prep.json", PREP_SETUP_TIMEOUT_MS, RawFinding, RawFindings, PLACEHOLDERS, KEY_BRIEF;
|
|
4552
|
+
var init_project_prep = __esm(() => {
|
|
4553
|
+
init_src();
|
|
4554
|
+
init_config_load();
|
|
4555
|
+
init_paths();
|
|
4556
|
+
init_skills_root();
|
|
4557
|
+
init_git();
|
|
4558
|
+
init_ticket_burner();
|
|
4559
|
+
PREP_SETUP_TIMEOUT_MS = 15 * 60000;
|
|
4560
|
+
RawFinding = z3.union([
|
|
4561
|
+
z3.string(),
|
|
4562
|
+
z3.object({
|
|
4563
|
+
value: z3.string().nullish(),
|
|
4564
|
+
evidence: z3.string().nullish()
|
|
4565
|
+
})
|
|
4566
|
+
]);
|
|
4567
|
+
RawFindings = z3.object({ notes: z3.string().nullish() }).catchall(z3.unknown());
|
|
4568
|
+
PLACEHOLDERS = ["REQUESTED_KEYS", "SETUP_COMMAND"];
|
|
4569
|
+
KEY_BRIEF = {
|
|
4570
|
+
setupCommand: "**`setupCommand`** \u2014 the command that takes a clean checkout to a state where the code builds and tests run: dependency install, plus any codegen or contract build that every task would otherwise discover it needed mid-flight (ORM client generation, protobuf, a `contracts:build` step). Run it and report what worked.",
|
|
4571
|
+
verifyCommands: "**`verifyCommands`** \u2014 the exact typecheck / test / lint commands for this repo, one per line. In a monorepo these must include the correct workspace filter names. Run each one and confirm it executes the thing it claims to.",
|
|
4572
|
+
knownFailures: '**`knownFailures`** \u2014 which tests are ALREADY failing on this checkout, before anything is changed. Run the full suite once and report the count plus the failing suite names. "0 known failures" is a real, useful answer.',
|
|
4573
|
+
devCommand: "**`devCommand`** \u2014 the command that starts this project's dev server on a developer's own machine. Read it from config; do not run it here.",
|
|
4574
|
+
driveSetupCommand: "**`driveSetupCommand`** \u2014 a single shell command that takes this repo from a fresh checkout to \"the dev server would actually work\": backing services up, schema applied, whatever this project needs. Chain steps with `&&`. Read it from the repo's own config (compose file, Makefile, package scripts, README); do not run it here. Omit it if the dev command alone is enough \u2014 an empty answer is better than a guessed one, because this runs on a developer's real machine.",
|
|
4575
|
+
driveStopCommand: "**`driveStopCommand`** \u2014 the matching teardown for `driveSetupCommand`, if this project has one (`docker compose down`, a stop script). Read it from config; do not run it here. Omit it if setup leaves nothing that needs stopping.",
|
|
4576
|
+
driveEnv: "**`driveEnv`** \u2014 `KEY=VALUE` lines (one per line) overlaid on the dev server's environment during a test drive, with `{{slug}}`, `{{branch}}` and `{{id}}` (an identifier-safe slug) rendered per drive. Its purpose is giving a branch its own database, e.g. `DATABASE_URL=postgres://localhost:5432/myapp_{{id}}` paired with a `driveSetupCommand` that creates it. Report it ONLY if you can read the real connection variable's name and shape out of the repo's own config or `.env.example`; a connection string you assembled from defaults points at the wrong database convincingly, which is worse than omitting the key. Omit it if the project has no database or you had to guess the host, port or credentials.",
|
|
4577
|
+
dbResetCommand: "**`dbResetCommand`** \u2014 the command that rebuilds the dev database from the migrations currently in the working tree. Read it from config; do not run it here. Omit it entirely if this repo has no database."
|
|
4578
|
+
};
|
|
4579
|
+
});
|
|
4580
|
+
|
|
4581
|
+
// src/services/findings.ts
|
|
4582
|
+
import { and as and5, eq as eq5 } from "drizzle-orm";
|
|
4583
|
+
function isPreparedKey(key) {
|
|
4584
|
+
return PREPARED_SET.has(key);
|
|
4585
|
+
}
|
|
4586
|
+
function findingRow(ctx, projectId, key) {
|
|
4587
|
+
return ctx.db.select({
|
|
4588
|
+
source: projectFindings.source,
|
|
4589
|
+
evidence: projectFindings.evidence,
|
|
4590
|
+
establishedAt: projectFindings.establishedAt,
|
|
4591
|
+
establishedSha: projectFindings.establishedSha
|
|
4592
|
+
}).from(projectFindings).where(and5(eq5(projectFindings.projectId, projectId), eq5(projectFindings.key, key))).get();
|
|
4593
|
+
}
|
|
4594
|
+
function isOverwritable(ctx, projectId, key) {
|
|
4595
|
+
return findingRow(ctx, projectId, key)?.source !== "human";
|
|
4596
|
+
}
|
|
4597
|
+
function recordFinding(ctx, projectId, input) {
|
|
4598
|
+
const { key, value, source, evidence, establishedSha } = input;
|
|
4599
|
+
ctx.db.transaction((tx) => {
|
|
4600
|
+
tx.update(projects).set({ [COLUMN_NAME[key]]: value }).where(eq5(projects.id, projectId)).run();
|
|
4601
|
+
if (value === null) {
|
|
4602
|
+
tx.delete(projectFindings).where(and5(eq5(projectFindings.projectId, projectId), eq5(projectFindings.key, key))).run();
|
|
4603
|
+
return;
|
|
4604
|
+
}
|
|
4605
|
+
tx.insert(projectFindings).values({
|
|
4606
|
+
projectId,
|
|
4607
|
+
key,
|
|
4608
|
+
source,
|
|
4609
|
+
evidence: evidence ?? null,
|
|
4610
|
+
establishedAt: Date.now(),
|
|
4611
|
+
establishedSha: establishedSha ?? null
|
|
4612
|
+
}).onConflictDoUpdate({
|
|
4613
|
+
target: [projectFindings.projectId, projectFindings.key],
|
|
4614
|
+
set: {
|
|
4615
|
+
source,
|
|
4616
|
+
evidence: evidence ?? null,
|
|
4617
|
+
establishedAt: Date.now(),
|
|
4618
|
+
establishedSha: establishedSha ?? null
|
|
4619
|
+
}
|
|
4620
|
+
}).run();
|
|
4621
|
+
});
|
|
4622
|
+
}
|
|
4623
|
+
function recordHuman(ctx, projectId, key, value) {
|
|
4624
|
+
if (!isPreparedKey(key))
|
|
4625
|
+
return;
|
|
4626
|
+
const existing = findingRow(ctx, projectId, key);
|
|
4627
|
+
if (value === null) {
|
|
4628
|
+
if (existing) {
|
|
4629
|
+
ctx.db.delete(projectFindings).where(and5(eq5(projectFindings.projectId, projectId), eq5(projectFindings.key, key))).run();
|
|
4630
|
+
}
|
|
4631
|
+
return;
|
|
4632
|
+
}
|
|
4633
|
+
ctx.db.insert(projectFindings).values({
|
|
4634
|
+
projectId,
|
|
4635
|
+
key,
|
|
4636
|
+
source: "human",
|
|
4637
|
+
evidence: null,
|
|
4638
|
+
establishedAt: Date.now(),
|
|
4639
|
+
establishedSha: null
|
|
4640
|
+
}).onConflictDoUpdate({
|
|
4641
|
+
target: [projectFindings.projectId, projectFindings.key],
|
|
4642
|
+
set: { source: "human", evidence: null, establishedAt: Date.now(), establishedSha: null }
|
|
4643
|
+
}).run();
|
|
4644
|
+
}
|
|
4645
|
+
async function listFindings(ctx, project) {
|
|
4646
|
+
const rows = ctx.db.select().from(projectFindings).where(eq5(projectFindings.projectId, project.id)).all();
|
|
4647
|
+
const distances = new Map;
|
|
4648
|
+
for (const row of rows) {
|
|
4649
|
+
const sha = row.establishedSha;
|
|
4650
|
+
if (!sha || distances.has(sha))
|
|
4651
|
+
continue;
|
|
4652
|
+
distances.set(sha, await commitsSince(project.repoPath, sha, project.mainBranch));
|
|
4653
|
+
}
|
|
4654
|
+
const order = new Map(PREPARED_KEYS.map((k, i) => [k, i]));
|
|
4655
|
+
return rows.filter((row) => isPreparedKey(row.key)).sort((a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0)).map((row) => {
|
|
4656
|
+
const staleCommits = row.establishedSha ? distances.get(row.establishedSha) : undefined;
|
|
4657
|
+
return {
|
|
4658
|
+
key: row.key,
|
|
4659
|
+
source: row.source,
|
|
4660
|
+
...row.evidence ? { evidence: row.evidence } : {},
|
|
4661
|
+
establishedAt: row.establishedAt,
|
|
4662
|
+
...row.establishedSha ? { establishedSha: row.establishedSha } : {},
|
|
4663
|
+
...staleCommits !== undefined ? { staleCommits } : {}
|
|
4664
|
+
};
|
|
4665
|
+
});
|
|
4666
|
+
}
|
|
4667
|
+
function unsetPreparedKeys(project) {
|
|
4668
|
+
return PREPARED_KEYS.filter((key) => {
|
|
4669
|
+
const value = project[key];
|
|
4670
|
+
return value === undefined || value.trim() === "";
|
|
4671
|
+
});
|
|
4672
|
+
}
|
|
4673
|
+
var VALUE_COLUMN, COLUMN_NAME, PREPARED_SET;
|
|
4674
|
+
var init_findings = __esm(() => {
|
|
4675
|
+
init_src();
|
|
4676
|
+
init_schema();
|
|
4677
|
+
init_git();
|
|
4678
|
+
VALUE_COLUMN = {
|
|
4679
|
+
setupCommand: projects.setupCommand,
|
|
4680
|
+
verifyCommands: projects.verifyCommands,
|
|
4681
|
+
knownFailures: projects.knownFailures,
|
|
4682
|
+
devCommand: projects.devCommand,
|
|
4683
|
+
driveSetupCommand: projects.driveSetupCommand,
|
|
4684
|
+
driveStopCommand: projects.driveStopCommand,
|
|
4685
|
+
driveEnv: projects.driveEnv,
|
|
4686
|
+
dbResetCommand: projects.dbResetCommand
|
|
4687
|
+
};
|
|
4688
|
+
COLUMN_NAME = {
|
|
4689
|
+
setupCommand: "setupCommand",
|
|
4690
|
+
verifyCommands: "verifyCommands",
|
|
4691
|
+
knownFailures: "knownFailures",
|
|
4692
|
+
devCommand: "devCommand",
|
|
4693
|
+
driveSetupCommand: "driveSetupCommand",
|
|
4694
|
+
driveStopCommand: "driveStopCommand",
|
|
4695
|
+
driveEnv: "driveEnv",
|
|
4696
|
+
dbResetCommand: "dbResetCommand"
|
|
4697
|
+
};
|
|
4698
|
+
PREPARED_SET = new Set(PREPARED_KEYS);
|
|
4699
|
+
});
|
|
4700
|
+
|
|
4701
|
+
// src/services/prep.ts
|
|
4702
|
+
import { and as and6, desc as desc3, eq as eq6 } from "drizzle-orm";
|
|
4703
|
+
function isPreparing(projectId) {
|
|
4704
|
+
return controllers.has(projectId);
|
|
4705
|
+
}
|
|
4706
|
+
function keysToPrepare(ctx, project, opts = {}) {
|
|
4707
|
+
const unset = new Set(unsetPreparedKeys(project));
|
|
4708
|
+
const candidates = opts.keys ?? (opts.refresh ? PREPARED_KEYS : [...unset]);
|
|
4709
|
+
return candidates.filter((key) => (opts.refresh || unset.has(key)) && isOverwritable(ctx, project.id, key));
|
|
4710
|
+
}
|
|
4711
|
+
function shouldAutoPrepare(ctx, project) {
|
|
4712
|
+
if (!ctx.config.autoPrepare)
|
|
4713
|
+
return false;
|
|
4714
|
+
if (latestPrep(ctx, project.id))
|
|
4715
|
+
return false;
|
|
4716
|
+
return keysToPrepare(ctx, project).length > 0;
|
|
4717
|
+
}
|
|
4718
|
+
async function startPrep(ctx, projectId, opts = {}) {
|
|
4719
|
+
const project = requireProjectById(ctx, projectId);
|
|
4720
|
+
if (controllers.has(projectId)) {
|
|
4721
|
+
throw new InvalidInputError(`${project.name} is already being prepared`);
|
|
4722
|
+
}
|
|
4723
|
+
const keys = keysToPrepare(ctx, project, opts);
|
|
4724
|
+
const prepId = newId("prep");
|
|
4725
|
+
const headSha2 = await prepHeadSha(project);
|
|
4726
|
+
ctx.db.insert(projectPreps).values({
|
|
4727
|
+
id: prepId,
|
|
4728
|
+
projectId,
|
|
4729
|
+
status: "running",
|
|
4730
|
+
startedAt: Date.now(),
|
|
4731
|
+
endedAt: null,
|
|
4732
|
+
summary: null,
|
|
4733
|
+
headSha: headSha2 ?? null
|
|
4734
|
+
}).run();
|
|
4735
|
+
emitProject(ctx, projectId, {
|
|
4736
|
+
type: "prep.run.started",
|
|
4737
|
+
message: keys.length === 0 ? `preparing ${project.name} \u2014 nothing left to establish` : `preparing ${project.name} \u2014 ${keys.length} field(s) to establish`,
|
|
4738
|
+
data: { prepId, keys }
|
|
4739
|
+
});
|
|
4740
|
+
const controller = new AbortController;
|
|
4741
|
+
controllers.set(projectId, { runId: prepId, controller });
|
|
4742
|
+
const prepCtx = {
|
|
4743
|
+
project,
|
|
4744
|
+
keys,
|
|
4745
|
+
emitEvent: (e) => {
|
|
4746
|
+
emitProject(ctx, projectId, { type: e.type, message: e.message, data: e.data });
|
|
4747
|
+
},
|
|
4748
|
+
signal: controller.signal
|
|
4749
|
+
};
|
|
4750
|
+
const deps = opts.deps ?? resolvePrepDeps(project);
|
|
4751
|
+
const done = finalize(ctx, project, prepId, prepCtx, deps, controller, headSha2);
|
|
4752
|
+
return { prepId, keys, done };
|
|
4753
|
+
}
|
|
4754
|
+
function cancelPrep(projectId) {
|
|
4755
|
+
controllers.get(projectId)?.controller.abort();
|
|
4756
|
+
}
|
|
4757
|
+
async function finalize(ctx, project, prepId, prepCtx, deps, controller, headSha2) {
|
|
4758
|
+
let status = "failed";
|
|
4759
|
+
let summary = "preparation failed";
|
|
4760
|
+
let findings;
|
|
4761
|
+
try {
|
|
4762
|
+
const result = await prepRun(prepCtx, deps);
|
|
4763
|
+
status = result.status === "succeeded" ? "succeeded" : "failed";
|
|
4764
|
+
summary = result.summary;
|
|
4765
|
+
findings = result.findings;
|
|
4766
|
+
} catch (e) {
|
|
4767
|
+
if (controller.signal.aborted) {
|
|
4768
|
+
status = "cancelled";
|
|
4769
|
+
summary = "preparation cancelled";
|
|
4770
|
+
} else {
|
|
4771
|
+
status = "failed";
|
|
4772
|
+
summary = e instanceof Error ? e.message : "preparation failed";
|
|
4773
|
+
}
|
|
4774
|
+
} finally {
|
|
4775
|
+
controllers.delete(project.id);
|
|
4776
|
+
}
|
|
4777
|
+
const applied = findings ? applyFindings(ctx, project, findings, headSha2) : [];
|
|
4778
|
+
ctx.db.update(projectPreps).set({ status, endedAt: Date.now(), summary }).where(eq6(projectPreps.id, prepId)).run();
|
|
4779
|
+
emitProject(ctx, project.id, {
|
|
4780
|
+
type: "prep.run.finished",
|
|
4781
|
+
message: `preparation ${status}: ${summary}`,
|
|
4782
|
+
data: { prepId, status, summary, applied }
|
|
4783
|
+
});
|
|
4784
|
+
}
|
|
4785
|
+
function applyFindings(ctx, project, findings, headSha2) {
|
|
4786
|
+
const applied = [];
|
|
4787
|
+
for (const [key, finding] of Object.entries(findings.values)) {
|
|
4788
|
+
if (!isOverwritable(ctx, project.id, key)) {
|
|
4789
|
+
emitProject(ctx, project.id, {
|
|
4790
|
+
type: "prep.skipped_field",
|
|
4791
|
+
message: `kept your ${key} \u2014 preparation proposed "${truncate(finding.value)}" but you set this by hand`,
|
|
4792
|
+
data: { key, proposed: finding.value }
|
|
4793
|
+
});
|
|
4794
|
+
continue;
|
|
4795
|
+
}
|
|
4796
|
+
recordFinding(ctx, project.id, {
|
|
4797
|
+
key,
|
|
4798
|
+
value: finding.value,
|
|
4799
|
+
source: "prep",
|
|
4800
|
+
...finding.evidence ? { evidence: finding.evidence } : {},
|
|
4801
|
+
...headSha2 ? { establishedSha: headSha2 } : {}
|
|
4802
|
+
});
|
|
4803
|
+
applied.push(key);
|
|
4804
|
+
emitProject(ctx, project.id, {
|
|
4805
|
+
type: "prep.established",
|
|
4806
|
+
message: `${key}: ${truncate(finding.value)}`,
|
|
4807
|
+
data: { key, value: finding.value, evidence: finding.evidence }
|
|
4808
|
+
});
|
|
4809
|
+
}
|
|
4810
|
+
if (findings.notes) {
|
|
4811
|
+
emitProject(ctx, project.id, {
|
|
4812
|
+
type: "prep.notes",
|
|
4813
|
+
message: `preparation notes: ${findings.notes}`,
|
|
4814
|
+
data: { notes: findings.notes }
|
|
4815
|
+
});
|
|
4816
|
+
}
|
|
4817
|
+
return applied;
|
|
4818
|
+
}
|
|
4819
|
+
function truncate(value, max = 120) {
|
|
4820
|
+
const oneLine = value.replace(/\s*\n\s*/g, " \xB7 ").trim();
|
|
4821
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}\u2026` : oneLine;
|
|
4822
|
+
}
|
|
4823
|
+
function latestPrep(ctx, projectId) {
|
|
4824
|
+
const row = ctx.db.select().from(projectPreps).where(eq6(projectPreps.projectId, projectId)).orderBy(desc3(projectPreps.startedAt)).get();
|
|
4825
|
+
if (!row)
|
|
4826
|
+
return null;
|
|
4827
|
+
return {
|
|
4828
|
+
id: row.id,
|
|
4829
|
+
projectId: row.projectId,
|
|
4830
|
+
status: row.status,
|
|
4831
|
+
startedAt: row.startedAt,
|
|
4832
|
+
...row.endedAt !== null ? { endedAt: row.endedAt } : {},
|
|
4833
|
+
...row.summary !== null ? { summary: row.summary } : {},
|
|
4834
|
+
...row.headSha !== null ? { headSha: row.headSha } : {}
|
|
4835
|
+
};
|
|
4836
|
+
}
|
|
4837
|
+
function latestPrepNotes(ctx, projectId) {
|
|
4838
|
+
const row = ctx.db.select({ data: events.data }).from(events).where(and6(eq6(events.projectId, projectId), eq6(events.type, "prep.notes"))).orderBy(desc3(events.id)).get();
|
|
4839
|
+
const notes = row?.data?.notes;
|
|
4840
|
+
return typeof notes === "string" && notes.trim() !== "" ? notes : undefined;
|
|
4841
|
+
}
|
|
4842
|
+
function reconcilePreps(ctx) {
|
|
4843
|
+
const stale = ctx.db.select({ id: projectPreps.id, projectId: projectPreps.projectId }).from(projectPreps).where(eq6(projectPreps.status, "running")).all().filter((row) => !controllers.has(row.projectId));
|
|
4844
|
+
for (const row of stale) {
|
|
4845
|
+
ctx.db.update(projectPreps).set({ status: "failed", endedAt: Date.now(), summary: "server stopped while preparing" }).where(eq6(projectPreps.id, row.id)).run();
|
|
4846
|
+
}
|
|
4847
|
+
return stale.length;
|
|
4848
|
+
}
|
|
4849
|
+
var controllers;
|
|
4850
|
+
var init_prep = __esm(() => {
|
|
4851
|
+
init_src();
|
|
4852
|
+
init_schema();
|
|
4853
|
+
init_errors();
|
|
4854
|
+
init_project_prep();
|
|
4855
|
+
init_events();
|
|
4856
|
+
init_findings();
|
|
4857
|
+
init_repo();
|
|
4858
|
+
controllers = new Map;
|
|
4859
|
+
});
|
|
4860
|
+
|
|
4861
|
+
// src/launcher/runtime.ts
|
|
4862
|
+
function setRuntimeCtx(ctx) {
|
|
4863
|
+
current = ctx;
|
|
4864
|
+
}
|
|
4865
|
+
async function getRuntimeCtx() {
|
|
4866
|
+
if (current)
|
|
4867
|
+
return current;
|
|
4868
|
+
const [{ createDb: createDb2 }, { dbPath: dbPath2 }, { loadConfig: loadConfig2 }] = await Promise.all([
|
|
4869
|
+
Promise.resolve().then(() => (init_client(), exports_client)),
|
|
4870
|
+
Promise.resolve().then(() => (init_paths(), exports_paths)),
|
|
4871
|
+
Promise.resolve().then(() => (init_config_load(), exports_config_load))
|
|
4872
|
+
]);
|
|
4873
|
+
const db = createDb2(dbPath2());
|
|
4874
|
+
current = { db, config: loadConfig2() };
|
|
4875
|
+
return current;
|
|
4876
|
+
}
|
|
4877
|
+
var current = null;
|
|
4878
|
+
|
|
4879
|
+
// src/services/feature-docs.ts
|
|
4880
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4881
|
+
import { join as join9 } from "path";
|
|
4882
|
+
function featureDocsDir(project, feature) {
|
|
4883
|
+
const relSegments = featureDocsRel(feature.slug).split("/");
|
|
4884
|
+
const worktree = worktreeDir(project.id, feature.slug);
|
|
4885
|
+
const base = existsSync9(worktree) ? worktree : project.repoPath;
|
|
4886
|
+
return join9(base, ...relSegments);
|
|
4887
|
+
}
|
|
4888
|
+
function featureDocPath(project, feature, fileName) {
|
|
4889
|
+
return join9(featureDocsDir(project, feature), fileName);
|
|
4890
|
+
}
|
|
4891
|
+
var init_feature_docs = __esm(() => {
|
|
4892
|
+
init_paths();
|
|
4893
|
+
});
|
|
4894
|
+
|
|
4895
|
+
// src/services/tickets.ts
|
|
4896
|
+
import { and as and7, asc as asc3, eq as eq7 } from "drizzle-orm";
|
|
4897
|
+
function rowToTicket(row) {
|
|
4898
|
+
return {
|
|
4899
|
+
id: row.id,
|
|
4900
|
+
featureId: row.featureId,
|
|
4901
|
+
seq: row.seq,
|
|
4902
|
+
title: row.title,
|
|
4903
|
+
goal: row.goal,
|
|
4904
|
+
context: row.context,
|
|
4905
|
+
acceptanceCriteria: row.acceptanceCriteria,
|
|
4906
|
+
seams: row.seams,
|
|
4907
|
+
blockedBy: row.blockedBy,
|
|
4908
|
+
status: row.status,
|
|
4909
|
+
commits: row.commits,
|
|
4910
|
+
error: row.error ?? undefined,
|
|
4911
|
+
attemptBranch: row.attemptBranch ?? undefined,
|
|
4912
|
+
conflictFiles: row.conflictFiles ?? undefined
|
|
4913
|
+
};
|
|
4914
|
+
}
|
|
4915
|
+
function resolveBlocking(inputs, startSeq) {
|
|
4916
|
+
try {
|
|
4917
|
+
return resolveBatchBlocking(inputs, { startSeq });
|
|
4918
|
+
} catch (e) {
|
|
4919
|
+
if (e instanceof BlockingEdgeError)
|
|
4920
|
+
throw new InvalidInputError(e.message);
|
|
4921
|
+
throw e;
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
function listByFeature2(ctx, featureId) {
|
|
4925
|
+
return ctx.db.select().from(tickets).where(eq7(tickets.featureId, featureId)).orderBy(asc3(tickets.seq)).all().map(rowToTicket);
|
|
4926
|
+
}
|
|
4927
|
+
function storeTickets(ctx, featureId, inputs) {
|
|
4928
|
+
if (inputs.length === 0)
|
|
4929
|
+
return [];
|
|
4930
|
+
const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq7(tickets.featureId, featureId)).all();
|
|
4931
|
+
const startSeq = existing.reduce((max, r) => Math.max(max, r.seq), 0) + 1;
|
|
4932
|
+
const resolved = resolveBlocking(inputs, startSeq);
|
|
4933
|
+
const rows = inputs.map((t, i) => ({
|
|
4934
|
+
id: newId("tkt"),
|
|
4935
|
+
featureId,
|
|
4936
|
+
seq: resolved[i].seq,
|
|
4937
|
+
title: t.title,
|
|
4938
|
+
goal: t.goal,
|
|
4939
|
+
context: t.context,
|
|
4940
|
+
acceptanceCriteria: t.acceptanceCriteria,
|
|
4941
|
+
seams: t.seams,
|
|
4942
|
+
blockedBy: resolved[i].blockedBy,
|
|
4943
|
+
status: "pending",
|
|
4944
|
+
commits: [],
|
|
4945
|
+
error: null,
|
|
4946
|
+
attemptBranch: null,
|
|
4947
|
+
conflictFiles: null
|
|
4948
|
+
}));
|
|
4949
|
+
ctx.db.insert(tickets).values(rows).run();
|
|
4950
|
+
emit(ctx, featureId, {
|
|
4951
|
+
type: "tickets.stored",
|
|
4952
|
+
message: `${rows.length} ticket(s) stored`,
|
|
4953
|
+
data: { count: rows.length, seqs: rows.map((r) => r.seq) }
|
|
4954
|
+
});
|
|
4955
|
+
return rows.map(rowToTicket);
|
|
4956
|
+
}
|
|
4957
|
+
function getTicket(ctx, id) {
|
|
4958
|
+
const row = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
4959
|
+
if (!row)
|
|
4960
|
+
throw new NotFoundError(`ticket ${id} not found`);
|
|
4961
|
+
return rowToTicket(row);
|
|
4962
|
+
}
|
|
4963
|
+
function assertMutable(t, verb) {
|
|
4964
|
+
if (!MUTABLE_STATUSES.has(t.status)) {
|
|
4965
|
+
throw new InvalidInputError(`cannot ${verb} ticket ${t.seq} \u2014 it is ${t.status}; only pending or failed tickets can be ${verb}${verb.endsWith("l") ? "led" : "ed"}`);
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
function editTicket(ctx, id, patch) {
|
|
4969
|
+
const current2 = getTicket(ctx, id);
|
|
4970
|
+
assertMutable(current2, "edit");
|
|
4971
|
+
const set = {};
|
|
4972
|
+
if (patch.title !== undefined)
|
|
4973
|
+
set.title = patch.title;
|
|
4974
|
+
if (patch.goal !== undefined)
|
|
4975
|
+
set.goal = patch.goal;
|
|
4976
|
+
if (patch.context !== undefined)
|
|
4977
|
+
set.context = patch.context;
|
|
4978
|
+
if (patch.acceptanceCriteria !== undefined)
|
|
4979
|
+
set.acceptanceCriteria = patch.acceptanceCriteria;
|
|
4980
|
+
if (patch.seams !== undefined)
|
|
4981
|
+
set.seams = patch.seams;
|
|
4982
|
+
if (Object.keys(set).length === 0) {
|
|
4983
|
+
throw new InvalidInputError("nothing to edit \u2014 pass at least one content field");
|
|
4984
|
+
}
|
|
4985
|
+
ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
|
|
4986
|
+
emit(ctx, current2.featureId, {
|
|
4987
|
+
type: "ticket.edited",
|
|
4988
|
+
message: `ticket ${current2.seq} content edited (${Object.keys(set).join(", ")})`,
|
|
4989
|
+
ticketId: id,
|
|
4990
|
+
data: { fields: Object.keys(set) }
|
|
4991
|
+
});
|
|
4992
|
+
return getTicket(ctx, id);
|
|
4993
|
+
}
|
|
4994
|
+
function cancelTicket(ctx, id, reason) {
|
|
4995
|
+
const current2 = getTicket(ctx, id);
|
|
4996
|
+
assertMutable(current2, "cancel");
|
|
4997
|
+
ctx.db.update(tickets).set({ status: "cancelled", error: reason?.trim() ? reason.trim() : null }).where(eq7(tickets.id, id)).run();
|
|
4998
|
+
emit(ctx, current2.featureId, {
|
|
4999
|
+
type: "ticket.cancelled",
|
|
5000
|
+
message: `ticket ${current2.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
|
|
5001
|
+
ticketId: id,
|
|
5002
|
+
data: { reason: reason ?? null }
|
|
5003
|
+
});
|
|
5004
|
+
return getTicket(ctx, id);
|
|
5005
|
+
}
|
|
5006
|
+
function sweepOrphanedBurning(ctx, featureId, reason) {
|
|
5007
|
+
const orphaned = ctx.db.select().from(tickets).where(and7(eq7(tickets.featureId, featureId), eq7(tickets.status, "burning"))).all().map(rowToTicket);
|
|
5008
|
+
for (const t of orphaned) {
|
|
5009
|
+
updateTicket(ctx, t.id, { status: "failed", error: reason });
|
|
5010
|
+
emit(ctx, featureId, {
|
|
5011
|
+
type: "ticket.failed",
|
|
5012
|
+
message: `ticket ${t.seq} failed: ${reason}`,
|
|
5013
|
+
ticketId: t.id,
|
|
5014
|
+
data: { error: reason, orphaned: true }
|
|
5015
|
+
});
|
|
5016
|
+
}
|
|
5017
|
+
return orphaned.map((t) => ({ ...t, status: "failed", error: reason }));
|
|
5018
|
+
}
|
|
5019
|
+
function updateTicket(ctx, id, patch) {
|
|
5020
|
+
const current2 = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
5021
|
+
if (!current2)
|
|
5022
|
+
throw new NotFoundError(`ticket ${id} not found`);
|
|
5023
|
+
const set = {};
|
|
5024
|
+
if (patch.status !== undefined)
|
|
5025
|
+
set.status = patch.status;
|
|
5026
|
+
if (patch.commits !== undefined)
|
|
5027
|
+
set.commits = patch.commits;
|
|
5028
|
+
if (patch.error !== undefined)
|
|
5029
|
+
set.error = patch.error;
|
|
5030
|
+
if (patch.attemptBranch !== undefined)
|
|
5031
|
+
set.attemptBranch = patch.attemptBranch;
|
|
5032
|
+
if (patch.conflictFiles !== undefined)
|
|
5033
|
+
set.conflictFiles = patch.conflictFiles;
|
|
5034
|
+
ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
|
|
5035
|
+
emit(ctx, current2.featureId, {
|
|
5036
|
+
type: "ticket.updated",
|
|
5037
|
+
message: `ticket ${current2.seq} \u2192 ${patch.status ?? current2.status}`,
|
|
5038
|
+
ticketId: id,
|
|
5039
|
+
data: patch
|
|
5040
|
+
});
|
|
5041
|
+
const updated = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
5042
|
+
return rowToTicket(updated);
|
|
5043
|
+
}
|
|
5044
|
+
var MUTABLE_STATUSES;
|
|
5045
|
+
var init_tickets = __esm(() => {
|
|
5046
|
+
init_src();
|
|
5047
|
+
init_schema();
|
|
5048
|
+
init_errors();
|
|
5049
|
+
init_events();
|
|
5050
|
+
MUTABLE_STATUSES = new Set(["pending", "failed"]);
|
|
5051
|
+
});
|
|
5052
|
+
|
|
5053
|
+
// src/services/gates.ts
|
|
5054
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5055
|
+
function checkGate(ctx, check, feature) {
|
|
5056
|
+
switch (check) {
|
|
5057
|
+
case "decisions-file-exists":
|
|
5058
|
+
return fileGate(ctx, feature, "decisions.md", "run the ideation session to capture decisions first");
|
|
5059
|
+
case "all-waypoints-terminal": {
|
|
5060
|
+
const wps = listByFeature(ctx, feature.id);
|
|
5061
|
+
if (wps.length === 0) {
|
|
5062
|
+
return { satisfied: false, reason: "no waypoints charted yet \u2014 chart the map before converging" };
|
|
5063
|
+
}
|
|
5064
|
+
const open = wps.filter((w) => w.status !== "resolved" && w.status !== "dropped");
|
|
5065
|
+
return open.length === 0 ? { satisfied: true } : {
|
|
5066
|
+
satisfied: false,
|
|
5067
|
+
reason: notYetTerminal("waypoint", open.map((w) => w.status), ["open", "claimed"])
|
|
5068
|
+
};
|
|
5069
|
+
}
|
|
5070
|
+
case "spec-file-exists":
|
|
5071
|
+
return fileGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
|
|
5072
|
+
case "tickets-approved": {
|
|
5073
|
+
const count = listByFeature2(ctx, feature.id).filter((t) => t.status !== "cancelled").length;
|
|
5074
|
+
return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
|
|
5075
|
+
}
|
|
5076
|
+
case "all-tickets-terminal": {
|
|
5077
|
+
const tickets2 = listByFeature2(ctx, feature.id);
|
|
5078
|
+
if (tickets2.length === 0)
|
|
5079
|
+
return { satisfied: false, reason: "no tickets have run yet" };
|
|
5080
|
+
const open = tickets2.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
5081
|
+
return open.length === 0 ? { satisfied: true } : {
|
|
5082
|
+
satisfied: false,
|
|
5083
|
+
reason: notYetTerminal("ticket", open.map((t) => t.status), ["pending", "burning"])
|
|
5084
|
+
};
|
|
5085
|
+
}
|
|
5086
|
+
case "human-merge":
|
|
5087
|
+
return { satisfied: false, reason: "use the Merge button to ship" };
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
function notYetTerminal(noun, statuses, order) {
|
|
5091
|
+
const counts = new Map;
|
|
5092
|
+
for (const s of statuses)
|
|
5093
|
+
counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
5094
|
+
const ordered = [
|
|
5095
|
+
...order.filter((s) => counts.has(s)),
|
|
5096
|
+
...[...counts.keys()].filter((s) => !order.includes(s))
|
|
5097
|
+
];
|
|
5098
|
+
const breakdown = ordered.map((s) => `${counts.get(s)} ${s}`).join(", ");
|
|
5099
|
+
return `${statuses.length} ${statuses.length === 1 ? noun : `${noun}s`} not yet terminal (${breakdown})`;
|
|
5100
|
+
}
|
|
5101
|
+
function fileGate(ctx, feature, fileName, reason) {
|
|
5102
|
+
const project = projectForFeature(ctx, feature);
|
|
5103
|
+
return existsSync10(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
|
|
5104
|
+
}
|
|
5105
|
+
function overrideGate(ctx, featureId, gate, reason) {
|
|
5106
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
5107
|
+
ctx.db.insert(gateOverrides).values({ featureId, gate, reason, ts: Date.now() }).run();
|
|
5108
|
+
emit(ctx, featureId, {
|
|
5109
|
+
type: "gate.overridden",
|
|
5110
|
+
message: `gate ${gate} overridden: ${reason}`,
|
|
5111
|
+
data: { gate, reason }
|
|
5112
|
+
});
|
|
5113
|
+
const next = nextPhase(feature);
|
|
5114
|
+
if (next)
|
|
5115
|
+
return setPhase(ctx, featureId, next, "phase.advanced", `advanced to ${next} (override)`);
|
|
5116
|
+
return feature;
|
|
5117
|
+
}
|
|
5118
|
+
var init_gates = __esm(() => {
|
|
5119
|
+
init_src();
|
|
5120
|
+
init_schema();
|
|
5121
|
+
init_events();
|
|
5122
|
+
init_feature_docs();
|
|
5123
|
+
init_repo();
|
|
5124
|
+
init_tickets();
|
|
5125
|
+
init_waypoints();
|
|
5126
|
+
});
|
|
5127
|
+
|
|
5128
|
+
// src/services/knowledge.ts
|
|
5129
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, writeFileSync } from "fs";
|
|
5130
|
+
import { join as join10, relative, resolve as resolve6, sep } from "path";
|
|
5131
|
+
function scaffoldDocs(ctx, feature) {
|
|
5132
|
+
const project = projectForFeature(ctx, feature);
|
|
5133
|
+
const dir = featureDocsDir(project, feature);
|
|
5134
|
+
mkdirSync5(dir, { recursive: true });
|
|
5135
|
+
const briefPath = featureDocPath(project, feature, "brief.md");
|
|
5136
|
+
if (!existsSync11(briefPath)) {
|
|
5137
|
+
const created = new Date().toISOString();
|
|
5138
|
+
const brief = [
|
|
5139
|
+
`# ${feature.title}`,
|
|
5140
|
+
"",
|
|
5141
|
+
feature.oneLiner,
|
|
5142
|
+
"",
|
|
5143
|
+
`- Slug: ${feature.slug}`,
|
|
5144
|
+
`- Created: ${created}`,
|
|
5145
|
+
""
|
|
5146
|
+
].join(`
|
|
5147
|
+
`);
|
|
5148
|
+
writeFileSync(briefPath, brief, "utf8");
|
|
5149
|
+
}
|
|
5150
|
+
if (feature.mapped) {
|
|
5151
|
+
scaffoldMapDoc(project, feature);
|
|
5152
|
+
}
|
|
5153
|
+
emit(ctx, feature.id, {
|
|
5154
|
+
type: "docs.scaffolded",
|
|
5155
|
+
message: feature.mapped ? `scaffolded docs/features/${feature.slug}/{brief,map}.md` : `scaffolded docs/features/${feature.slug}/brief.md`
|
|
5156
|
+
});
|
|
5157
|
+
}
|
|
5158
|
+
function scaffoldMapDoc(project, feature, seed) {
|
|
5159
|
+
mkdirSync5(featureDocsDir(project, feature), { recursive: true });
|
|
5160
|
+
const mapPath = featureDocPath(project, feature, "map.md");
|
|
5161
|
+
if (existsSync11(mapPath))
|
|
5162
|
+
return;
|
|
5163
|
+
writeFileSync(mapPath, mapDocBody(feature, seed), "utf8");
|
|
5164
|
+
}
|
|
5165
|
+
function mapDocBody(feature, seed) {
|
|
5166
|
+
const seeded = {
|
|
5167
|
+
Destination: seed?.destination?.trim(),
|
|
5168
|
+
Notes: seed?.notes?.trim()
|
|
5169
|
+
};
|
|
5170
|
+
const lines = [`# ${feature.title} \u2014 map`, ""];
|
|
5171
|
+
for (const section of MAP_SECTIONS) {
|
|
5172
|
+
lines.push(`## ${section}`, "");
|
|
5173
|
+
const body = seeded[section];
|
|
5174
|
+
if (body)
|
|
5175
|
+
lines.push(body, "");
|
|
5176
|
+
}
|
|
5177
|
+
return lines.join(`
|
|
5178
|
+
`);
|
|
5179
|
+
}
|
|
5180
|
+
function listDocs(ctx, feature) {
|
|
5181
|
+
const project = projectForFeature(ctx, feature);
|
|
5182
|
+
const dir = featureDocsDir(project, feature);
|
|
5183
|
+
if (!existsSync11(dir))
|
|
5184
|
+
return [];
|
|
5185
|
+
return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join10(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
5186
|
+
}
|
|
5187
|
+
function readDoc(ctx, feature, relPath) {
|
|
5188
|
+
const project = projectForFeature(ctx, feature);
|
|
5189
|
+
const dir = featureDocsDir(project, feature);
|
|
5190
|
+
const root = resolve6(dir);
|
|
5191
|
+
const target = resolve6(dir, relPath);
|
|
5192
|
+
const rel = relative(root, target);
|
|
5193
|
+
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
5194
|
+
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
5195
|
+
}
|
|
5196
|
+
if (!existsSync11(target))
|
|
5197
|
+
throw new NotFoundError(`doc not found: ${relPath}`);
|
|
5198
|
+
return { content: readFileSync5(target, "utf8") };
|
|
5199
|
+
}
|
|
5200
|
+
function readTitle(path, fallback) {
|
|
5201
|
+
try {
|
|
5202
|
+
const firstLine = readFileSync5(path, "utf8").split(`
|
|
5203
|
+
`, 1)[0] ?? "";
|
|
5204
|
+
const heading = firstLine.match(/^#\s+(.+)$/);
|
|
5205
|
+
return heading ? heading[1].trim() : fallback;
|
|
5206
|
+
} catch {
|
|
5207
|
+
return fallback;
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
var MAP_SECTIONS;
|
|
5211
|
+
var init_knowledge = __esm(() => {
|
|
5212
|
+
init_errors();
|
|
5213
|
+
init_events();
|
|
5214
|
+
init_feature_docs();
|
|
5215
|
+
init_repo();
|
|
5216
|
+
MAP_SECTIONS = [
|
|
5217
|
+
"Destination",
|
|
5218
|
+
"Notes",
|
|
5219
|
+
"Not yet specified",
|
|
5220
|
+
"Out of scope"
|
|
5221
|
+
];
|
|
5222
|
+
});
|
|
5223
|
+
|
|
5224
|
+
// src/pty/end-session.ts
|
|
5225
|
+
function endSession(ctx, sessionId) {
|
|
5226
|
+
const registry = ptyRegistry();
|
|
5227
|
+
const killed = registry.kill(sessionId);
|
|
5228
|
+
registry.remove(sessionId);
|
|
5229
|
+
const session = markSessionEnded(ctx, sessionId);
|
|
5230
|
+
if (session) {
|
|
5231
|
+
releaseForSession(ctx, sessionId);
|
|
5232
|
+
emitForSession(ctx, session, {
|
|
5233
|
+
type: "session.ended",
|
|
5234
|
+
message: "session ended by user",
|
|
5235
|
+
data: { sessionId, killed }
|
|
5236
|
+
});
|
|
5237
|
+
} else {
|
|
5238
|
+
const existing = getSessionRow(ctx, sessionId);
|
|
5239
|
+
if (!existing)
|
|
5240
|
+
return { sessionId, ok: false, killed };
|
|
5241
|
+
}
|
|
5242
|
+
return { sessionId, ok: true, killed };
|
|
5243
|
+
}
|
|
5244
|
+
var init_end_session = __esm(() => {
|
|
5245
|
+
init_sessions();
|
|
5246
|
+
init_events();
|
|
5247
|
+
init_waypoints();
|
|
5248
|
+
init_registry();
|
|
4144
5249
|
});
|
|
4145
5250
|
|
|
4146
5251
|
// src/workflows/research.ts
|
|
4147
|
-
import { existsSync as
|
|
4148
|
-
import { dirname as
|
|
4149
|
-
import { fileURLToPath as
|
|
4150
|
-
import { run as
|
|
5252
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readdirSync as readdirSync4 } from "fs";
|
|
5253
|
+
import { dirname as dirname5, join as join11 } from "path";
|
|
5254
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
5255
|
+
import { run as run3 } from "@ai-hero/sandcastle";
|
|
4151
5256
|
import { docker as docker2 } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
4152
5257
|
import { noSandbox as noSandbox2 } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
|
|
4153
5258
|
function waypointSlug(waypoint) {
|
|
@@ -4164,7 +5269,7 @@ async function researchRun(ctx, deps) {
|
|
|
4164
5269
|
return { status: "failed", summary: "no waypoint to research" };
|
|
4165
5270
|
}
|
|
4166
5271
|
if (deps.config.sandbox === "docker" && !deps.hasAuthToken) {
|
|
4167
|
-
ctx.emitEvent({ type:
|
|
5272
|
+
ctx.emitEvent({ type: AUTH_MISSING_EVENT3, message: AUTH_MISSING_MESSAGE3 });
|
|
4168
5273
|
return { status: "failed", summary: "research aborted: auth token missing" };
|
|
4169
5274
|
}
|
|
4170
5275
|
ctx.emitEvent({
|
|
@@ -4185,23 +5290,23 @@ async function researchRun(ctx, deps) {
|
|
|
4185
5290
|
}
|
|
4186
5291
|
ctx.emitEvent({
|
|
4187
5292
|
type: "research.failed",
|
|
4188
|
-
message: `waypoint ${waypoint.seq} research failed: ${
|
|
5293
|
+
message: `waypoint ${waypoint.seq} research failed: ${errorHeadline3(outcome.error)}`,
|
|
4189
5294
|
data: { waypointId: waypoint.id, error: outcome.error }
|
|
4190
5295
|
});
|
|
4191
5296
|
return { status: "failed", summary: outcome.error };
|
|
4192
5297
|
}
|
|
4193
|
-
function
|
|
5298
|
+
function errorHeadline3(s) {
|
|
4194
5299
|
const lines = s.split(`
|
|
4195
5300
|
`).map((l) => l.trim()).filter(Boolean);
|
|
4196
5301
|
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
4197
5302
|
return causes.at(-1) ?? lines[0] ?? "";
|
|
4198
5303
|
}
|
|
4199
|
-
function createResearchStreamThrottle(
|
|
4200
|
-
return createStreamThrottle((e) =>
|
|
5304
|
+
function createResearchStreamThrottle(emit3, opts = {}) {
|
|
5305
|
+
return createStreamThrottle((e) => emit3({ ...e, type: e.type.replace(/^burn\./, "research.") }), opts);
|
|
4201
5306
|
}
|
|
4202
5307
|
function renderResearchPrompt(template, values) {
|
|
4203
5308
|
let out = template;
|
|
4204
|
-
for (const key of
|
|
5309
|
+
for (const key of PLACEHOLDERS2) {
|
|
4205
5310
|
out = out.split(`{{${key}}}`).join(values[key]);
|
|
4206
5311
|
}
|
|
4207
5312
|
return out;
|
|
@@ -4216,32 +5321,32 @@ function interpretResearchResult(result, docRelPath) {
|
|
|
4216
5321
|
return { status: "failed", error: "agent produced no commits \u2014 no research summary landed" };
|
|
4217
5322
|
}
|
|
4218
5323
|
function researchTemplatePath() {
|
|
4219
|
-
const here =
|
|
4220
|
-
return
|
|
5324
|
+
const here = dirname5(fileURLToPath4(import.meta.url));
|
|
5325
|
+
return join11(resolveSkillsRoot(here), "burner", "research-waypoint.md");
|
|
4221
5326
|
}
|
|
4222
5327
|
function readDocsDigestFromDisk2(projectId, slug) {
|
|
4223
5328
|
const worktree = worktreeDir(projectId, slug);
|
|
4224
|
-
if (!
|
|
5329
|
+
if (!existsSync12(worktree))
|
|
4225
5330
|
return "_No talk worktree on disk \u2014 docs digest skipped._";
|
|
4226
|
-
const docsDir =
|
|
4227
|
-
if (!
|
|
5331
|
+
const docsDir = join11(worktree, ...featureDocsRel(slug).split("/"));
|
|
5332
|
+
if (!existsSync12(docsDir))
|
|
4228
5333
|
return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
|
|
4229
|
-
const files = readdirSync4(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content:
|
|
5334
|
+
const files = readdirSync4(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync6(join11(docsDir, e.name), "utf8") }));
|
|
4230
5335
|
return buildDocsDigest(files);
|
|
4231
5336
|
}
|
|
4232
5337
|
async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
4233
5338
|
const { project, feature } = ctx;
|
|
4234
5339
|
const docRelPath = researchDocRel(feature.slug, waypoint);
|
|
4235
5340
|
const tempBranch = researchBranchName(feature.slug, waypoint.seq, newId("b").slice(2, 10));
|
|
4236
|
-
const template =
|
|
5341
|
+
const template = readFileSync6(researchTemplatePath(), "utf8");
|
|
4237
5342
|
const prompt = renderResearchPrompt(template, {
|
|
4238
5343
|
WAYPOINT_JSON: buildWaypointJson(waypoint),
|
|
4239
5344
|
FEATURE_BRIEF: buildFeatureBrief(feature),
|
|
4240
5345
|
DOCS_DIGEST: readDocsDigestFromDisk2(project.id, feature.slug),
|
|
4241
5346
|
RESEARCH_DOC_PATH: docRelPath
|
|
4242
5347
|
});
|
|
4243
|
-
|
|
4244
|
-
const logFilePath =
|
|
5348
|
+
mkdirSync6(logsDir(), { recursive: true });
|
|
5349
|
+
const logFilePath = join11(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
|
|
4245
5350
|
const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
|
|
4246
5351
|
const runOptions = {
|
|
4247
5352
|
agent: buildBurnAgent(config2, token, model),
|
|
@@ -4256,7 +5361,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
4256
5361
|
let result;
|
|
4257
5362
|
let salvaged = [];
|
|
4258
5363
|
try {
|
|
4259
|
-
result = await
|
|
5364
|
+
result = await run3(runOptions);
|
|
4260
5365
|
} catch (err) {
|
|
4261
5366
|
throttle.flush();
|
|
4262
5367
|
if (ctx.signal.aborted)
|
|
@@ -4268,7 +5373,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
4268
5373
|
const removed = await cleanupBurnWorktree(project.repoPath, tempBranch);
|
|
4269
5374
|
ctx.emitEvent({
|
|
4270
5375
|
type: "burn.worktree.teardown-failed",
|
|
4271
|
-
message: `waypoint ${waypoint.seq}: agent finished but sandcastle could not remove its worktree (${
|
|
5376
|
+
message: `waypoint ${waypoint.seq}: agent finished but sandcastle could not remove its worktree (${errorHeadline3(msg)})${removed ? " \u2014 cleaned up" : " \u2014 left on disk"}; landing the ${salvaged.length} commit(s) anyway`,
|
|
4272
5377
|
data: { tempBranch, error: msg, cleanedUp: removed }
|
|
4273
5378
|
});
|
|
4274
5379
|
}
|
|
@@ -4281,7 +5386,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
4281
5386
|
const detail = merge.error ?? "merge failed";
|
|
4282
5387
|
ctx.emitEvent({
|
|
4283
5388
|
type: merge.conflict ? "merge.conflict.needs-human" : "research.merge_failed",
|
|
4284
|
-
message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${
|
|
5389
|
+
message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${errorHeadline3(detail)} \u2014 the branch is preserved for manual recovery`,
|
|
4285
5390
|
data: { tempBranch, featureBranch: feature.branch, conflict: merge.conflict ?? false, error: detail }
|
|
4286
5391
|
});
|
|
4287
5392
|
return {
|
|
@@ -4303,9 +5408,9 @@ function resolveResearchDeps(ctx) {
|
|
|
4303
5408
|
}
|
|
4304
5409
|
function readTokenFromEnvFile2(path) {
|
|
4305
5410
|
let fromFile;
|
|
4306
|
-
if (
|
|
5411
|
+
if (existsSync12(path)) {
|
|
4307
5412
|
try {
|
|
4308
|
-
fromFile = parseEnvFile(
|
|
5413
|
+
fromFile = parseEnvFile(readFileSync6(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
|
|
4309
5414
|
} catch {
|
|
4310
5415
|
fromFile = undefined;
|
|
4311
5416
|
}
|
|
@@ -4313,7 +5418,7 @@ function readTokenFromEnvFile2(path) {
|
|
|
4313
5418
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
4314
5419
|
return token && token.length > 0 ? token : undefined;
|
|
4315
5420
|
}
|
|
4316
|
-
var
|
|
5421
|
+
var AUTH_MISSING_EVENT3 = "auth.missing", AUTH_MISSING_MESSAGE3 = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PLACEHOLDERS2, research;
|
|
4317
5422
|
var init_research = __esm(() => {
|
|
4318
5423
|
init_src();
|
|
4319
5424
|
init_config_load();
|
|
@@ -4321,7 +5426,7 @@ var init_research = __esm(() => {
|
|
|
4321
5426
|
init_skills_root();
|
|
4322
5427
|
init_git();
|
|
4323
5428
|
init_ticket_burner();
|
|
4324
|
-
|
|
5429
|
+
PLACEHOLDERS2 = ["WAYPOINT_JSON", "FEATURE_BRIEF", "DOCS_DIGEST", "RESEARCH_DOC_PATH"];
|
|
4325
5430
|
research = {
|
|
4326
5431
|
id: "research",
|
|
4327
5432
|
run: (ctx) => researchRun(ctx, resolveResearchDeps(ctx))
|
|
@@ -4343,12 +5448,12 @@ var init_registry2 = __esm(() => {
|
|
|
4343
5448
|
});
|
|
4344
5449
|
|
|
4345
5450
|
// src/workflows/runner.ts
|
|
4346
|
-
import { eq as
|
|
5451
|
+
import { eq as eq8 } from "drizzle-orm";
|
|
4347
5452
|
function workflowClaimsFeatureBranch(workflowId) {
|
|
4348
5453
|
return BRANCH_CLAIMING.has(workflowId);
|
|
4349
5454
|
}
|
|
4350
5455
|
function isRunActive(runId) {
|
|
4351
|
-
return
|
|
5456
|
+
return controllers2.has(runId);
|
|
4352
5457
|
}
|
|
4353
5458
|
async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
4354
5459
|
const feature = getFeatureRow(ctx, featureId);
|
|
@@ -4372,7 +5477,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
4372
5477
|
claim(ctx, opts.claimWaypointId, runId);
|
|
4373
5478
|
} catch (e) {
|
|
4374
5479
|
const summary = e instanceof Error ? e.message : "claim failed";
|
|
4375
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(
|
|
5480
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(eq8(runs.id, runId)).run();
|
|
4376
5481
|
throw e;
|
|
4377
5482
|
}
|
|
4378
5483
|
}
|
|
@@ -4383,7 +5488,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
4383
5488
|
data: { workflow: workflowId }
|
|
4384
5489
|
});
|
|
4385
5490
|
const controller = new AbortController;
|
|
4386
|
-
|
|
5491
|
+
controllers2.set(runId, controller);
|
|
4387
5492
|
const talkWorktree = worktreeDir(project.id, feature.slug);
|
|
4388
5493
|
const talkDetached = workflowClaimsFeatureBranch(workflowId) ? await detachWorktree(talkWorktree) : false;
|
|
4389
5494
|
const wctx = {
|
|
@@ -4405,7 +5510,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
4405
5510
|
input: opts.input,
|
|
4406
5511
|
modelOverride: opts.modelOverride,
|
|
4407
5512
|
resolveWaypoint: (id, disposition, summary) => {
|
|
4408
|
-
|
|
5513
|
+
resolve3(ctx, id, disposition, summary);
|
|
4409
5514
|
},
|
|
4410
5515
|
signal: controller.signal
|
|
4411
5516
|
};
|
|
@@ -4416,7 +5521,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
4416
5521
|
return { runId, done };
|
|
4417
5522
|
}
|
|
4418
5523
|
function cancelRun(runId) {
|
|
4419
|
-
|
|
5524
|
+
controllers2.get(runId)?.abort();
|
|
4420
5525
|
}
|
|
4421
5526
|
async function executeRun(ctx, runId, featureId, workflow2, runPromise, controller, cleanup) {
|
|
4422
5527
|
let status = "failed";
|
|
@@ -4435,9 +5540,9 @@ async function executeRun(ctx, runId, featureId, workflow2, runPromise, controll
|
|
|
4435
5540
|
}
|
|
4436
5541
|
emit(ctx, featureId, { type: "run.error", message: summary, runId });
|
|
4437
5542
|
} finally {
|
|
4438
|
-
|
|
5543
|
+
controllers2.delete(runId);
|
|
4439
5544
|
}
|
|
4440
|
-
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary }).where(
|
|
5545
|
+
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary }).where(eq8(runs.id, runId)).run();
|
|
4441
5546
|
releaseForSession(ctx, runId);
|
|
4442
5547
|
if (workflowClaimsFeatureBranch(workflow2)) {
|
|
4443
5548
|
sweepOrphanedBurning(ctx, featureId, `orphaned \u2014 the run ended (${status}) while it was burning`);
|
|
@@ -4467,7 +5572,7 @@ function maybeAutoAdvance(ctx, featureId) {
|
|
|
4467
5572
|
if (next)
|
|
4468
5573
|
setPhase(ctx, featureId, next, "phase.advanced", "auto-advanced to review after successful run");
|
|
4469
5574
|
}
|
|
4470
|
-
var
|
|
5575
|
+
var controllers2, BRANCH_CLAIMING;
|
|
4471
5576
|
var init_runner = __esm(() => {
|
|
4472
5577
|
init_src();
|
|
4473
5578
|
init_paths();
|
|
@@ -4480,12 +5585,12 @@ var init_runner = __esm(() => {
|
|
|
4480
5585
|
init_tickets();
|
|
4481
5586
|
init_waypoints();
|
|
4482
5587
|
init_registry2();
|
|
4483
|
-
|
|
5588
|
+
controllers2 = new Map;
|
|
4484
5589
|
BRANCH_CLAIMING = new Set(["ticket-burner"]);
|
|
4485
5590
|
});
|
|
4486
5591
|
|
|
4487
5592
|
// src/services/features.ts
|
|
4488
|
-
import { desc as
|
|
5593
|
+
import { desc as desc4, eq as eq9 } from "drizzle-orm";
|
|
4489
5594
|
import { rmSync as rmSync2 } from "fs";
|
|
4490
5595
|
async function createFeature(ctx, input) {
|
|
4491
5596
|
const project = requireProjectById(ctx, input.projectId);
|
|
@@ -4548,7 +5653,7 @@ function getFeatureFull(ctx, id) {
|
|
|
4548
5653
|
};
|
|
4549
5654
|
}
|
|
4550
5655
|
function list(ctx, projectId) {
|
|
4551
|
-
const rows = ctx.db.select().from(features).where(
|
|
5656
|
+
const rows = ctx.db.select().from(features).where(eq9(features.projectId, projectId)).orderBy(desc4(features.createdAt)).all();
|
|
4552
5657
|
return rows.map((row) => {
|
|
4553
5658
|
const feature = rowToFeature(row);
|
|
4554
5659
|
const tickets2 = listByFeature2(ctx, feature.id);
|
|
@@ -4693,7 +5798,7 @@ function escalateToMap(ctx, featureId, input) {
|
|
|
4693
5798
|
return { ok: true, warning: `feature ${feature.slug} is already mapped \u2014 no changes made` };
|
|
4694
5799
|
}
|
|
4695
5800
|
const project = projectForFeature(ctx, feature);
|
|
4696
|
-
ctx.db.update(features).set({ mapped: true }).where(
|
|
5801
|
+
ctx.db.update(features).set({ mapped: true }).where(eq9(features.id, featureId)).run();
|
|
4697
5802
|
scaffoldMapDoc(project, { ...feature, mapped: true }, input);
|
|
4698
5803
|
emit(ctx, featureId, {
|
|
4699
5804
|
type: "feature.escalated",
|
|
@@ -4710,7 +5815,7 @@ function archiveFeature(ctx, featureId) {
|
|
|
4710
5815
|
const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
|
|
4711
5816
|
if (live)
|
|
4712
5817
|
endSession(ctx, live.id);
|
|
4713
|
-
ctx.db.update(features).set({ status: "archived" }).where(
|
|
5818
|
+
ctx.db.update(features).set({ status: "archived" }).where(eq9(features.id, featureId)).run();
|
|
4714
5819
|
emit(ctx, featureId, {
|
|
4715
5820
|
type: "feature.archived",
|
|
4716
5821
|
message: `feature ${feature.slug} archived`,
|
|
@@ -4724,7 +5829,7 @@ function unarchiveFeature(ctx, featureId) {
|
|
|
4724
5829
|
throw new GateError(`feature ${feature.slug} is not archived`);
|
|
4725
5830
|
}
|
|
4726
5831
|
const restored = feature.phase === "shipped" ? "shipped" : "active";
|
|
4727
|
-
ctx.db.update(features).set({ status: restored }).where(
|
|
5832
|
+
ctx.db.update(features).set({ status: restored }).where(eq9(features.id, featureId)).run();
|
|
4728
5833
|
emit(ctx, featureId, {
|
|
4729
5834
|
type: "feature.unarchived",
|
|
4730
5835
|
message: `feature ${feature.slug} unarchived (${restored})`,
|
|
@@ -4738,9 +5843,9 @@ async function deleteFeature(ctx, featureId) {
|
|
|
4738
5843
|
throw new GateError(`feature ${feature.slug} is shipped \u2014 archive it instead (delete is for non-shipped features)`);
|
|
4739
5844
|
}
|
|
4740
5845
|
const project = projectForFeature(ctx, feature);
|
|
4741
|
-
for (const
|
|
4742
|
-
if (
|
|
4743
|
-
cancelRun(
|
|
5846
|
+
for (const run4 of listRunsByFeature(ctx, featureId)) {
|
|
5847
|
+
if (run4.status === "running")
|
|
5848
|
+
cancelRun(run4.id);
|
|
4744
5849
|
}
|
|
4745
5850
|
const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
|
|
4746
5851
|
if (live)
|
|
@@ -4764,13 +5869,13 @@ async function deleteFeature(ctx, featureId) {
|
|
|
4764
5869
|
return { ok: true, slug: feature.slug };
|
|
4765
5870
|
}
|
|
4766
5871
|
function deleteFeatureRows(ctx, featureId) {
|
|
4767
|
-
ctx.db.delete(tickets).where(
|
|
4768
|
-
ctx.db.delete(sessions).where(
|
|
4769
|
-
ctx.db.delete(runs).where(
|
|
4770
|
-
ctx.db.delete(events).where(
|
|
4771
|
-
ctx.db.delete(gateOverrides).where(
|
|
4772
|
-
ctx.db.delete(waypoints).where(
|
|
4773
|
-
ctx.db.delete(features).where(
|
|
5872
|
+
ctx.db.delete(tickets).where(eq9(tickets.featureId, featureId)).run();
|
|
5873
|
+
ctx.db.delete(sessions).where(eq9(sessions.featureId, featureId)).run();
|
|
5874
|
+
ctx.db.delete(runs).where(eq9(runs.featureId, featureId)).run();
|
|
5875
|
+
ctx.db.delete(events).where(eq9(events.featureId, featureId)).run();
|
|
5876
|
+
ctx.db.delete(gateOverrides).where(eq9(gateOverrides.featureId, featureId)).run();
|
|
5877
|
+
ctx.db.delete(waypoints).where(eq9(waypoints.featureId, featureId)).run();
|
|
5878
|
+
ctx.db.delete(features).where(eq9(features.id, featureId)).run();
|
|
4774
5879
|
}
|
|
4775
5880
|
function gateState(ctx, feature) {
|
|
4776
5881
|
const gate = nextGate(feature);
|
|
@@ -4781,7 +5886,7 @@ function gateState(ctx, feature) {
|
|
|
4781
5886
|
}
|
|
4782
5887
|
function uniqueSlug(ctx, projectId, title) {
|
|
4783
5888
|
const base = slugify(title);
|
|
4784
|
-
const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(
|
|
5889
|
+
const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(eq9(features.projectId, projectId)).all().map((r) => r.slug));
|
|
4785
5890
|
if (!existing.has(base))
|
|
4786
5891
|
return base;
|
|
4787
5892
|
let n = 2;
|
|
@@ -4813,7 +5918,7 @@ var init_features = __esm(() => {
|
|
|
4813
5918
|
import { StreamableHTTPTransport } from "@hono/mcp";
|
|
4814
5919
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4815
5920
|
import { Hono } from "hono";
|
|
4816
|
-
import * as
|
|
5921
|
+
import * as z4 from "zod";
|
|
4817
5922
|
function headerSessionId(extra) {
|
|
4818
5923
|
const headers = extra.requestInfo?.headers;
|
|
4819
5924
|
if (!headers)
|
|
@@ -4830,8 +5935,14 @@ function resolveSession(ctx, sessionId) {
|
|
|
4830
5935
|
}
|
|
4831
5936
|
return mostRecentLiveSession(ctx);
|
|
4832
5937
|
}
|
|
5938
|
+
function requireFeatureId(session) {
|
|
5939
|
+
if (!session.featureId) {
|
|
5940
|
+
throw new GateError(`this tool needs a feature, and a ${session.kind} session is project-scoped. ` + "Use record_finding to store what you establish about the project.");
|
|
5941
|
+
}
|
|
5942
|
+
return session.featureId;
|
|
5943
|
+
}
|
|
4833
5944
|
function toolGetFeatureContext(ctx, session) {
|
|
4834
|
-
const feature = getFeatureRow(ctx, session
|
|
5945
|
+
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
4835
5946
|
const docs = listDocs(ctx, feature).map((d) => {
|
|
4836
5947
|
try {
|
|
4837
5948
|
return { relPath: d.relPath, content: readDoc(ctx, feature, d.relPath).content };
|
|
@@ -4853,17 +5964,17 @@ function toolGetFeatureContext(ctx, session) {
|
|
|
4853
5964
|
return context;
|
|
4854
5965
|
}
|
|
4855
5966
|
function toolResolveWaypoint(ctx, _session, input) {
|
|
4856
|
-
|
|
5967
|
+
resolve3(ctx, input.id, input.disposition, input.summary);
|
|
4857
5968
|
return { ok: true };
|
|
4858
5969
|
}
|
|
4859
5970
|
function toolEmitTickets(ctx, session, input) {
|
|
4860
|
-
const feature = getFeatureRow(ctx, session
|
|
5971
|
+
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
4861
5972
|
const stored = storeTickets(ctx, feature.id, input.tickets);
|
|
4862
5973
|
return { stored: stored.length, ids: stored.map((t) => t.id) };
|
|
4863
5974
|
}
|
|
4864
5975
|
function requireOwnTicket(ctx, session, ticketId) {
|
|
4865
5976
|
const ticket = getTicket(ctx, ticketId);
|
|
4866
|
-
if (ticket.featureId !== session
|
|
5977
|
+
if (ticket.featureId !== requireFeatureId(session)) {
|
|
4867
5978
|
throw new GateError(`ticket ${ticketId} does not belong to this session's feature`);
|
|
4868
5979
|
}
|
|
4869
5980
|
return ticket;
|
|
@@ -4878,10 +5989,10 @@ function toolCancelTicket(ctx, session, input) {
|
|
|
4878
5989
|
return { ok: true, ticket: cancelTicket(ctx, input.id, input.reason) };
|
|
4879
5990
|
}
|
|
4880
5991
|
function toolEscalateToMap(ctx, session, input) {
|
|
4881
|
-
return escalateToMap(ctx, session
|
|
5992
|
+
return escalateToMap(ctx, requireFeatureId(session), input);
|
|
4882
5993
|
}
|
|
4883
5994
|
function toolEmitWaypoints(ctx, session, input) {
|
|
4884
|
-
const feature = getFeatureRow(ctx, session
|
|
5995
|
+
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
4885
5996
|
if (!feature.mapped) {
|
|
4886
5997
|
throw new GateError("feature is not mapped \u2014 call escalate_to_map before emitting waypoints");
|
|
4887
5998
|
}
|
|
@@ -4889,7 +6000,7 @@ function toolEmitWaypoints(ctx, session, input) {
|
|
|
4889
6000
|
return { stored: stored.length, ids: stored.map((w) => w.id) };
|
|
4890
6001
|
}
|
|
4891
6002
|
function toolRecordEvent(ctx, session, input) {
|
|
4892
|
-
emit(ctx, session
|
|
6003
|
+
emit(ctx, requireFeatureId(session), {
|
|
4893
6004
|
type: input.type,
|
|
4894
6005
|
message: input.message,
|
|
4895
6006
|
data: { source: "mcp" }
|
|
@@ -4897,7 +6008,7 @@ function toolRecordEvent(ctx, session, input) {
|
|
|
4897
6008
|
return { ok: true };
|
|
4898
6009
|
}
|
|
4899
6010
|
function toolCompletePhase(ctx, session, input) {
|
|
4900
|
-
const feature = getFeatureRow(ctx, session
|
|
6011
|
+
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
4901
6012
|
emit(ctx, feature.id, {
|
|
4902
6013
|
type: "phase.complete_requested",
|
|
4903
6014
|
message: `session marked phase '${input.phase}' complete`,
|
|
@@ -4926,13 +6037,46 @@ async function commitDocsCheckpoint(ctx, session, message) {
|
|
|
4926
6037
|
try {
|
|
4927
6038
|
await commitDocs(session.worktreePath, message);
|
|
4928
6039
|
} catch (e) {
|
|
4929
|
-
emit(ctx, session
|
|
6040
|
+
emit(ctx, requireFeatureId(session), {
|
|
4930
6041
|
type: "git.commit_pending",
|
|
4931
6042
|
message: isNotImplemented(e) ? "docs checkpoint skipped (git service pending)" : `docs checkpoint failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
4932
6043
|
data: { message }
|
|
4933
6044
|
});
|
|
4934
6045
|
}
|
|
4935
6046
|
}
|
|
6047
|
+
function requireProject(ctx, session) {
|
|
6048
|
+
if (!session.projectId) {
|
|
6049
|
+
throw new GateError(`record_finding is for a project-scoped preparation session; this is a ${session.kind} session.`);
|
|
6050
|
+
}
|
|
6051
|
+
const project = getProjectById(ctx, session.projectId);
|
|
6052
|
+
if (!project)
|
|
6053
|
+
throw new GateError(`project ${session.projectId} not found`);
|
|
6054
|
+
return project;
|
|
6055
|
+
}
|
|
6056
|
+
function toolRecordFinding(ctx, session, input) {
|
|
6057
|
+
const project = requireProject(ctx, session);
|
|
6058
|
+
const source = input.userSupplied ? "human" : "session";
|
|
6059
|
+
if (!isOverwritable(ctx, project.id, input.key) && !input.userSupplied) {
|
|
6060
|
+
return {
|
|
6061
|
+
ok: true,
|
|
6062
|
+
key: input.key,
|
|
6063
|
+
source,
|
|
6064
|
+
skipped: `${input.key} was set by a human and is not auto-overwritable \u2014 ask them to clear it first if it should change`
|
|
6065
|
+
};
|
|
6066
|
+
}
|
|
6067
|
+
recordFinding(ctx, project.id, {
|
|
6068
|
+
key: input.key,
|
|
6069
|
+
value: input.value,
|
|
6070
|
+
source,
|
|
6071
|
+
...input.evidence ? { evidence: input.evidence } : {}
|
|
6072
|
+
});
|
|
6073
|
+
emitProject(ctx, project.id, {
|
|
6074
|
+
type: "prep.finding_recorded",
|
|
6075
|
+
message: `preparation session established ${input.key} (${source})`,
|
|
6076
|
+
data: { key: input.key, source, sessionId: session.id }
|
|
6077
|
+
});
|
|
6078
|
+
return { ok: true, key: input.key, source };
|
|
6079
|
+
}
|
|
4936
6080
|
function ok(result) {
|
|
4937
6081
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4938
6082
|
}
|
|
@@ -4954,6 +6098,21 @@ async function resolveCtxSession(extra) {
|
|
|
4954
6098
|
}
|
|
4955
6099
|
function buildMcpServer() {
|
|
4956
6100
|
const server = new McpServer({ name: "runcastle", version: "0.1.0" });
|
|
6101
|
+
server.registerTool("record_finding", {
|
|
6102
|
+
title: "Record a preparation finding",
|
|
6103
|
+
description: "Store one established project setting from a preparation conversation. " + "Set userSupplied: true ONLY when the human gave you this value or confirmed it verbatim \u2014 " + "that marks it as theirs and stops any future automatic run from overwriting it. " + "Anything you worked out yourself must leave it false, even if they were watching. " + "Always include evidence: what you ran, or what the human told you.",
|
|
6104
|
+
inputSchema: {
|
|
6105
|
+
key: PreparedKey,
|
|
6106
|
+
value: z4.string(),
|
|
6107
|
+
evidence: z4.string().optional(),
|
|
6108
|
+
userSupplied: z4.boolean().optional()
|
|
6109
|
+
}
|
|
6110
|
+
}, async (args, extra) => {
|
|
6111
|
+
const rs = await resolveCtxSession(extra);
|
|
6112
|
+
if (!rs)
|
|
6113
|
+
return noSession();
|
|
6114
|
+
return ok(toolRecordFinding(rs.ctx, rs.session, args));
|
|
6115
|
+
});
|
|
4957
6116
|
server.registerTool("get_feature_context", {
|
|
4958
6117
|
title: "Get feature context",
|
|
4959
6118
|
description: "Full context for the current feature: the feature row, its phase, all docs/features/<slug>/*.md contents, and its tickets.",
|
|
@@ -4967,7 +6126,7 @@ function buildMcpServer() {
|
|
|
4967
6126
|
server.registerTool("emit_tickets", {
|
|
4968
6127
|
title: "Emit tickets",
|
|
4969
6128
|
description: "Store the ideation session's ticket batch. Each ticket: title, goal, context, acceptanceCriteria[], seams[], blockedBy[] (1-based positions within THIS batch).",
|
|
4970
|
-
inputSchema: { tickets:
|
|
6129
|
+
inputSchema: { tickets: z4.array(TicketInput) }
|
|
4971
6130
|
}, async (args, extra) => {
|
|
4972
6131
|
const rs = await resolveCtxSession(extra);
|
|
4973
6132
|
if (!rs)
|
|
@@ -4980,12 +6139,12 @@ function buildMcpServer() {
|
|
|
4980
6139
|
title: "Update ticket",
|
|
4981
6140
|
description: "Rewrite a stored ticket's content \u2014 title, goal, context, acceptanceCriteria, seams (any subset). Only pending or failed tickets can be edited; done/cancelled tickets are history and burning tickets are already running. Use during a revisit when a decision change makes a ticket stale. Get ids from get_feature_context.",
|
|
4982
6141
|
inputSchema: {
|
|
4983
|
-
id:
|
|
4984
|
-
title:
|
|
4985
|
-
goal:
|
|
4986
|
-
context:
|
|
4987
|
-
acceptanceCriteria:
|
|
4988
|
-
seams:
|
|
6142
|
+
id: z4.string(),
|
|
6143
|
+
title: z4.string().optional(),
|
|
6144
|
+
goal: z4.string().optional(),
|
|
6145
|
+
context: z4.string().optional(),
|
|
6146
|
+
acceptanceCriteria: z4.array(z4.string()).optional(),
|
|
6147
|
+
seams: z4.array(z4.string()).optional()
|
|
4989
6148
|
}
|
|
4990
6149
|
}, async (args, extra) => {
|
|
4991
6150
|
const rs = await resolveCtxSession(extra);
|
|
@@ -4998,7 +6157,7 @@ function buildMcpServer() {
|
|
|
4998
6157
|
server.registerTool("cancel_ticket", {
|
|
4999
6158
|
title: "Cancel ticket",
|
|
5000
6159
|
description: "Cancel a ticket that is no longer needed (terminal state; only pending or failed tickets). The burner skips cancelled tickets, and tickets blocked by a cancelled one still burn \u2014 cancellation counts as satisfied. Pass a short reason so the timeline explains why.",
|
|
5001
|
-
inputSchema: { id:
|
|
6160
|
+
inputSchema: { id: z4.string(), reason: z4.string().optional() }
|
|
5002
6161
|
}, async (args, extra) => {
|
|
5003
6162
|
const rs = await resolveCtxSession(extra);
|
|
5004
6163
|
if (!rs)
|
|
@@ -5010,7 +6169,7 @@ function buildMcpServer() {
|
|
|
5010
6169
|
server.registerTool("escalate_to_map", {
|
|
5011
6170
|
title: "Escalate to map",
|
|
5012
6171
|
description: "Escalate this grilling session into a map when the feature outgrows one context window. Flips the feature to mapped and scaffolds docs/features/<slug>/map.md, seeding Destination + Notes from your arguments (Not-yet-specified and Out-of-scope start empty). Idempotent: calling it on an already-mapped feature warns and changes nothing.",
|
|
5013
|
-
inputSchema: { destination:
|
|
6172
|
+
inputSchema: { destination: z4.string(), notes: z4.string().optional() }
|
|
5014
6173
|
}, async (args, extra) => {
|
|
5015
6174
|
const rs = await resolveCtxSession(extra);
|
|
5016
6175
|
if (!rs)
|
|
@@ -5024,7 +6183,7 @@ function buildMcpServer() {
|
|
|
5024
6183
|
server.registerTool("emit_waypoints", {
|
|
5025
6184
|
title: "Emit waypoints",
|
|
5026
6185
|
description: "Batch-create waypoints on the map (the feature must already be mapped \u2014 call escalate_to_map first). Each waypoint: title, type (grilling|research|prototype|task), question, blockedBy[] (1-based positions within THIS batch, and/or ids of already-stored waypoints). Available from any session once mapped \u2014 any session may branch the map.",
|
|
5027
|
-
inputSchema: { waypoints:
|
|
6186
|
+
inputSchema: { waypoints: z4.array(WaypointInput) }
|
|
5028
6187
|
}, async (args, extra) => {
|
|
5029
6188
|
const rs = await resolveCtxSession(extra);
|
|
5030
6189
|
if (!rs)
|
|
@@ -5034,7 +6193,7 @@ function buildMcpServer() {
|
|
|
5034
6193
|
server.registerTool("resolve_waypoint", {
|
|
5035
6194
|
title: "Resolve waypoint",
|
|
5036
6195
|
description: "End the current waypoint: `resolved` (its question is answered) or `dropped` (no longer needed). Write the decision prose to decisions.md (or the gist to map.md Out-of-scope for a drop) FIRST \u2014 this tool flips machinery only (marks the waypoint terminal, frees any dependents on the frontier). `summary` is the one-line gist shown in the UI. Call this exactly once, as the last thing you do.",
|
|
5037
|
-
inputSchema: { id:
|
|
6196
|
+
inputSchema: { id: z4.string(), disposition: WaypointDisposition, summary: z4.string() }
|
|
5038
6197
|
}, async (args, extra) => {
|
|
5039
6198
|
const rs = await resolveCtxSession(extra);
|
|
5040
6199
|
if (!rs)
|
|
@@ -5046,7 +6205,7 @@ function buildMcpServer() {
|
|
|
5046
6205
|
server.registerTool("record_event", {
|
|
5047
6206
|
title: "Record event",
|
|
5048
6207
|
description: "Add a note to the feature timeline (decisions recorded, spec saved, milestones).",
|
|
5049
|
-
inputSchema: { type:
|
|
6208
|
+
inputSchema: { type: z4.string(), message: z4.string() }
|
|
5050
6209
|
}, async (args, extra) => {
|
|
5051
6210
|
const rs = await resolveCtxSession(extra);
|
|
5052
6211
|
if (!rs)
|
|
@@ -5076,6 +6235,7 @@ var init_server = __esm(() => {
|
|
|
5076
6235
|
init_sessions();
|
|
5077
6236
|
init_features();
|
|
5078
6237
|
init_events();
|
|
6238
|
+
init_findings();
|
|
5079
6239
|
init_git();
|
|
5080
6240
|
init_knowledge();
|
|
5081
6241
|
init_repo();
|
|
@@ -5169,12 +6329,16 @@ import { Hono as Hono2 } from "hono";
|
|
|
5169
6329
|
function handleSessionStart(ctx, sessionId, feature, payload) {
|
|
5170
6330
|
const ccSessionId = typeof payload?.session_id === "string" ? payload.session_id : undefined;
|
|
5171
6331
|
const transcriptPath = typeof payload?.transcript_path === "string" ? payload.transcript_path : undefined;
|
|
6332
|
+
const source = typeof payload?.source === "string" ? payload.source : undefined;
|
|
6333
|
+
const wasLive = getSessionRow(ctx, sessionId)?.status === "live";
|
|
5172
6334
|
markSessionLive(ctx, sessionId, { ccSessionId, transcriptPath });
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
6335
|
+
if (!wasLive) {
|
|
6336
|
+
emit(ctx, feature.id, {
|
|
6337
|
+
type: "session.started",
|
|
6338
|
+
message: source === "resume" ? "session live (conversation resumed)" : "session live",
|
|
6339
|
+
data: { sessionId, ccSessionId, transcriptPath, source: source ?? null }
|
|
6340
|
+
});
|
|
6341
|
+
}
|
|
5178
6342
|
return {
|
|
5179
6343
|
hookSpecificOutput: {
|
|
5180
6344
|
hookEventName: "SessionStart",
|
|
@@ -5182,7 +6346,60 @@ function handleSessionStart(ctx, sessionId, feature, payload) {
|
|
|
5182
6346
|
}
|
|
5183
6347
|
};
|
|
5184
6348
|
}
|
|
5185
|
-
function
|
|
6349
|
+
function handlePrepareSessionStart(ctx, session, payload) {
|
|
6350
|
+
const ccSessionId = typeof payload?.session_id === "string" ? payload.session_id : undefined;
|
|
6351
|
+
const transcriptPath = typeof payload?.transcript_path === "string" ? payload.transcript_path : undefined;
|
|
6352
|
+
const source = typeof payload?.source === "string" ? payload.source : undefined;
|
|
6353
|
+
const wasLive = getSessionRow(ctx, session.id)?.status === "live";
|
|
6354
|
+
markSessionLive(ctx, session.id, { ccSessionId, transcriptPath });
|
|
6355
|
+
if (!wasLive) {
|
|
6356
|
+
emitForSession(ctx, session, {
|
|
6357
|
+
type: "session.started",
|
|
6358
|
+
message: source === "resume" ? "preparation session live (resumed)" : "preparation session live",
|
|
6359
|
+
data: { sessionId: session.id, ccSessionId, transcriptPath, source: source ?? null }
|
|
6360
|
+
});
|
|
6361
|
+
}
|
|
6362
|
+
return {
|
|
6363
|
+
hookSpecificOutput: {
|
|
6364
|
+
hookEventName: "SessionStart",
|
|
6365
|
+
additionalContext: prepareStartContext(ctx, session)
|
|
6366
|
+
}
|
|
6367
|
+
};
|
|
6368
|
+
}
|
|
6369
|
+
function handlePrepareUserPrompt(ctx, session, payload) {
|
|
6370
|
+
const prompt = typeof payload?.prompt === "string" ? payload.prompt : undefined;
|
|
6371
|
+
noteKickoffPrompt(ctx, session.id, prompt);
|
|
6372
|
+
return {
|
|
6373
|
+
hookSpecificOutput: {
|
|
6374
|
+
hookEventName: "UserPromptSubmit",
|
|
6375
|
+
additionalContext: `[runcastle] preparation session (project-scoped, no feature)`
|
|
6376
|
+
}
|
|
6377
|
+
};
|
|
6378
|
+
}
|
|
6379
|
+
function handlePrepareSessionEnd(ctx, session) {
|
|
6380
|
+
markSessionEnded(ctx, session.id);
|
|
6381
|
+
emitForSession(ctx, session, {
|
|
6382
|
+
type: "session.ended",
|
|
6383
|
+
message: "preparation session ended",
|
|
6384
|
+
data: { sessionId: session.id }
|
|
6385
|
+
});
|
|
6386
|
+
return {};
|
|
6387
|
+
}
|
|
6388
|
+
function prepareStartContext(ctx, session) {
|
|
6389
|
+
const project = session.projectId ? getProjectById(ctx, session.projectId) : null;
|
|
6390
|
+
if (!project)
|
|
6391
|
+
return "[runcastle] preparation session \u2014 project not found";
|
|
6392
|
+
const remaining = keysToPrepare(ctx, project);
|
|
6393
|
+
return [
|
|
6394
|
+
`[runcastle] preparation session for ${project.name} (${project.repoPath}).`,
|
|
6395
|
+
remaining.length > 0 ? `Still unestablished: ${remaining.join(", ")}.` : "Every prepared field currently has a value.",
|
|
6396
|
+
"Record what you establish with the `record_finding` MCP tool \u2014 a value the user",
|
|
6397
|
+
"gives you or confirms verbatim is theirs (userSupplied: true), anything you measured is not."
|
|
6398
|
+
].join(" ");
|
|
6399
|
+
}
|
|
6400
|
+
function handleUserPrompt(ctx, sessionId, feature, payload) {
|
|
6401
|
+
const prompt = typeof payload?.prompt === "string" ? payload.prompt : undefined;
|
|
6402
|
+
noteKickoffPrompt(ctx, sessionId, prompt);
|
|
5186
6403
|
const tickets2 = listByFeature2(ctx, feature.id).length;
|
|
5187
6404
|
return {
|
|
5188
6405
|
hookSpecificOutput: {
|
|
@@ -5221,6 +6438,7 @@ var hooks, hooks_default;
|
|
|
5221
6438
|
var init_hooks = __esm(() => {
|
|
5222
6439
|
init_sessions();
|
|
5223
6440
|
init_events();
|
|
6441
|
+
init_prep();
|
|
5224
6442
|
init_repo();
|
|
5225
6443
|
init_tickets();
|
|
5226
6444
|
init_waypoints();
|
|
@@ -5242,6 +6460,18 @@ var init_hooks = __esm(() => {
|
|
|
5242
6460
|
const session = getSessionRow(ctx, sessionId);
|
|
5243
6461
|
if (!session)
|
|
5244
6462
|
return c.json({});
|
|
6463
|
+
if (!session.featureId) {
|
|
6464
|
+
switch (event) {
|
|
6465
|
+
case "session-start":
|
|
6466
|
+
return c.json(handlePrepareSessionStart(ctx, session, body.payload));
|
|
6467
|
+
case "user-prompt":
|
|
6468
|
+
return c.json(handlePrepareUserPrompt(ctx, session, body.payload));
|
|
6469
|
+
case "session-end":
|
|
6470
|
+
return c.json(handlePrepareSessionEnd(ctx, session));
|
|
6471
|
+
default:
|
|
6472
|
+
return c.json({});
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
5245
6475
|
const feature = tryGetFeature(ctx, session.featureId);
|
|
5246
6476
|
if (!feature)
|
|
5247
6477
|
return c.json({});
|
|
@@ -5249,7 +6479,7 @@ var init_hooks = __esm(() => {
|
|
|
5249
6479
|
case "session-start":
|
|
5250
6480
|
return c.json(handleSessionStart(ctx, sessionId, feature, body.payload));
|
|
5251
6481
|
case "user-prompt":
|
|
5252
|
-
return c.json(handleUserPrompt(ctx, feature));
|
|
6482
|
+
return c.json(handleUserPrompt(ctx, sessionId, feature, body.payload));
|
|
5253
6483
|
case "session-end":
|
|
5254
6484
|
return c.json(handleSessionEnd(ctx, sessionId, feature));
|
|
5255
6485
|
default:
|
|
@@ -5314,9 +6544,9 @@ var init_stream = __esm(() => {
|
|
|
5314
6544
|
});
|
|
5315
6545
|
|
|
5316
6546
|
// src/routes/web.ts
|
|
5317
|
-
import { existsSync as
|
|
6547
|
+
import { existsSync as existsSync13, statSync } from "fs";
|
|
5318
6548
|
import { readFile } from "fs/promises";
|
|
5319
|
-
import { extname, join as
|
|
6549
|
+
import { extname, join as join12, normalize, resolve as resolve7, sep as sep2 } from "path";
|
|
5320
6550
|
function contentType(path) {
|
|
5321
6551
|
return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
5322
6552
|
}
|
|
@@ -5334,7 +6564,7 @@ function safeJoin(root, pathname) {
|
|
|
5334
6564
|
} catch {
|
|
5335
6565
|
return null;
|
|
5336
6566
|
}
|
|
5337
|
-
const joined = normalize(
|
|
6567
|
+
const joined = normalize(join12(root, decoded));
|
|
5338
6568
|
if (joined !== root && !joined.startsWith(root + sep2))
|
|
5339
6569
|
return null;
|
|
5340
6570
|
return joined;
|
|
@@ -5342,12 +6572,12 @@ function safeJoin(root, pathname) {
|
|
|
5342
6572
|
function resolveWebDist() {
|
|
5343
6573
|
const override = process.env.RUNCASTLE_WEB_DIST;
|
|
5344
6574
|
if (override)
|
|
5345
|
-
return
|
|
5346
|
-
return
|
|
6575
|
+
return resolve7(override);
|
|
6576
|
+
return resolve7(import.meta.dirname, "..", "..", "..", "..", "apps", "web", "dist");
|
|
5347
6577
|
}
|
|
5348
6578
|
function mountWebApp(app, distDir) {
|
|
5349
|
-
const root =
|
|
5350
|
-
const indexHtml =
|
|
6579
|
+
const root = resolve7(distDir);
|
|
6580
|
+
const indexHtml = join12(root, "index.html");
|
|
5351
6581
|
app.get("*", async (c, next) => {
|
|
5352
6582
|
const { pathname } = new URL(c.req.url);
|
|
5353
6583
|
if (/^\/(api|mcp|ws|health)(\/|$)/.test(pathname))
|
|
@@ -5367,7 +6597,7 @@ function mountWebApp(app, distDir) {
|
|
|
5367
6597
|
}
|
|
5368
6598
|
function mountWebAppIfBuilt(app) {
|
|
5369
6599
|
const dist = resolveWebDist();
|
|
5370
|
-
if (
|
|
6600
|
+
if (existsSync13(join12(dist, "index.html")))
|
|
5371
6601
|
mountWebApp(app, dist);
|
|
5372
6602
|
}
|
|
5373
6603
|
var MIME;
|
|
@@ -5414,14 +6644,14 @@ var init_context = __esm(() => {
|
|
|
5414
6644
|
});
|
|
5415
6645
|
|
|
5416
6646
|
// src/trpc/routers/docs.ts
|
|
5417
|
-
import * as
|
|
6647
|
+
import * as z5 from "zod";
|
|
5418
6648
|
var docsRouter;
|
|
5419
6649
|
var init_docs = __esm(() => {
|
|
5420
6650
|
init_knowledge();
|
|
5421
6651
|
init_repo();
|
|
5422
6652
|
init_context();
|
|
5423
6653
|
docsRouter = router({
|
|
5424
|
-
read: publicProcedure.input(
|
|
6654
|
+
read: publicProcedure.input(z5.object({ featureId: z5.string(), relPath: z5.string() })).query(({ ctx, input }) => {
|
|
5425
6655
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
5426
6656
|
return readDoc(ctx, feature, input.relPath);
|
|
5427
6657
|
})
|
|
@@ -5429,23 +6659,23 @@ var init_docs = __esm(() => {
|
|
|
5429
6659
|
});
|
|
5430
6660
|
|
|
5431
6661
|
// src/trpc/routers/events.ts
|
|
5432
|
-
import * as
|
|
6662
|
+
import * as z6 from "zod";
|
|
5433
6663
|
var eventsRouter;
|
|
5434
6664
|
var init_events2 = __esm(() => {
|
|
5435
6665
|
init_events();
|
|
5436
6666
|
init_context();
|
|
5437
6667
|
eventsRouter = router({
|
|
5438
|
-
list: publicProcedure.input(
|
|
5439
|
-
listByProject: publicProcedure.input(
|
|
6668
|
+
list: publicProcedure.input(z6.object({ featureId: z6.string(), afterId: z6.number().optional() })).query(({ ctx, input }) => listAfter(ctx, input.featureId, input.afterId)),
|
|
6669
|
+
listByProject: publicProcedure.input(z6.object({ projectId: z6.string(), afterId: z6.number().optional() })).query(({ ctx, input }) => listByProject(ctx, input.projectId, input.afterId))
|
|
5440
6670
|
});
|
|
5441
6671
|
});
|
|
5442
6672
|
|
|
5443
6673
|
// src/launcher/artifacts.ts
|
|
5444
|
-
import { mkdirSync as
|
|
5445
|
-
import { fileURLToPath as
|
|
5446
|
-
import { join as
|
|
6674
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
6675
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
6676
|
+
import { join as join13 } from "path";
|
|
5447
6677
|
function hookClientPath() {
|
|
5448
|
-
return resolveAsset(ASSET_ENV.hookClient,
|
|
6678
|
+
return resolveAsset(ASSET_ENV.hookClient, fileURLToPath5(new URL("./hook-client.ts", import.meta.url)));
|
|
5449
6679
|
}
|
|
5450
6680
|
function serverUrlFor(config2) {
|
|
5451
6681
|
return `http://localhost:${config2.serverPort}`;
|
|
@@ -5637,6 +6867,69 @@ function renderRevisitPrompt(feature) {
|
|
|
5637
6867
|
].join(`
|
|
5638
6868
|
`);
|
|
5639
6869
|
}
|
|
6870
|
+
function renderPreparePrompt(brief) {
|
|
6871
|
+
const { project, remainingKeys, established, notes } = brief;
|
|
6872
|
+
return [
|
|
6873
|
+
`# runcastle \u2014 preparing ${project.name}`,
|
|
6874
|
+
"",
|
|
6875
|
+
"This is a **preparation session**: a project-scoped conversation whose job is to",
|
|
6876
|
+
"establish the settings every later agent depends on, and to record them with evidence.",
|
|
6877
|
+
"There is no feature here and no pipeline to advance.",
|
|
6878
|
+
"",
|
|
6879
|
+
"## Where this runs \u2014 and why that matters",
|
|
6880
|
+
`You are on the developer's own machine, in \`${project.repoPath}\`, NOT in a sandbox.`,
|
|
6881
|
+
"A headless preparation run measures the repo in a throwaway container, which means it",
|
|
6882
|
+
"can never verify anything about *this* machine: the dev server, the local database,",
|
|
6883
|
+
"docker, credentials. Those keys \u2014 `devCommand`, `driveSetupCommand`, `driveStopCommand`,",
|
|
6884
|
+
"`driveEnv`, `dbResetCommand` \u2014 are exactly what it has to leave as proposals.",
|
|
6885
|
+
"You can actually run them. That is the point of this session.",
|
|
6886
|
+
"",
|
|
6887
|
+
"The same access is why you must **ask before you act**. Anything that starts or stops a",
|
|
6888
|
+
"service, creates or migrates a database, or writes outside the repo needs the human to",
|
|
6889
|
+
"agree first \u2014 say what you are about to run and why, then wait.",
|
|
6890
|
+
"",
|
|
6891
|
+
...established.length > 0 ? [
|
|
6892
|
+
"## Already established \u2014 do not re-derive",
|
|
6893
|
+
"These are recorded findings. Treat them as true unless the human says otherwise;",
|
|
6894
|
+
"replacing a measured value with a fresh guess makes preparation worse, not better.",
|
|
6895
|
+
"",
|
|
6896
|
+
...established.map((f) => `- \`${f.key}\` (${f.source})${f.evidence ? ` \u2014 ${f.evidence}` : ""}`),
|
|
6897
|
+
""
|
|
6898
|
+
] : [],
|
|
6899
|
+
...notes ? [
|
|
6900
|
+
"## What the last run could not settle",
|
|
6901
|
+
"Its own words. Where it says it declined to guess, it was right to \u2014 that is the",
|
|
6902
|
+
"gap you are here to close, and closing it usually means asking one direct question.",
|
|
6903
|
+
"",
|
|
6904
|
+
notes,
|
|
6905
|
+
""
|
|
6906
|
+
] : [],
|
|
6907
|
+
"## Still open",
|
|
6908
|
+
remainingKeys.length > 0 ? remainingKeys.map((k) => `- \`${k}\``).join(`
|
|
6909
|
+
`) : "_Nothing is unset. Confirm the existing values still hold, then say so and stop._",
|
|
6910
|
+
"",
|
|
6911
|
+
"## Recording what you establish",
|
|
6912
|
+
"- `record_finding({ key, value, evidence, userSupplied })` \u2014 one call per key.",
|
|
6913
|
+
"- `userSupplied: true` means the human GAVE you this value or confirmed it verbatim.",
|
|
6914
|
+
" That marks it as theirs and permanently stops automatic runs from overwriting it.",
|
|
6915
|
+
"- Leave it false for anything you worked out yourself, even with them watching \u2014",
|
|
6916
|
+
" that stays improvable by a later run. Getting this backwards silently retires a",
|
|
6917
|
+
" field from preparation forever, and the only way back is the human clearing it.",
|
|
6918
|
+
"- `evidence` is not optional in spirit: record what you ran and what it printed, or",
|
|
6919
|
+
" what the human told you. A value with no account of itself is a guess with a source field.",
|
|
6920
|
+
"",
|
|
6921
|
+
"## Secrets",
|
|
6922
|
+
"This is a development environment and the human has agreed to supply real connection",
|
|
6923
|
+
"strings and credentials here. Store them as given. Do not paste a secret into a timeline",
|
|
6924
|
+
"note or a commit message \u2014 `record_finding` is the only place a value belongs.",
|
|
6925
|
+
"",
|
|
6926
|
+
"## Your task",
|
|
6927
|
+
"Open by telling the human which fields are still open and what you need from them for",
|
|
6928
|
+
"each. Work them one at a time: propose, ask, run it if they agree, then record it.",
|
|
6929
|
+
""
|
|
6930
|
+
].join(`
|
|
6931
|
+
`);
|
|
6932
|
+
}
|
|
5640
6933
|
function renderSettings(hookClient) {
|
|
5641
6934
|
const cmd = (event) => ({
|
|
5642
6935
|
type: "command",
|
|
@@ -5646,7 +6939,10 @@ function renderSettings(hookClient) {
|
|
|
5646
6939
|
return {
|
|
5647
6940
|
permissions: { allow: [...RUNCASTLE_MCP_ALLOW_RULES, ...SESSION_BASH_ALLOW_RULES] },
|
|
5648
6941
|
hooks: {
|
|
5649
|
-
SessionStart:
|
|
6942
|
+
SessionStart: SESSION_START_SOURCES.map((source) => ({
|
|
6943
|
+
matcher: source,
|
|
6944
|
+
hooks: [cmd("session-start")]
|
|
6945
|
+
})),
|
|
5650
6946
|
UserPromptSubmit: [{ hooks: [cmd("user-prompt")] }],
|
|
5651
6947
|
SessionEnd: [{ hooks: [cmd("session-end")] }]
|
|
5652
6948
|
}
|
|
@@ -5664,18 +6960,21 @@ function renderMcpConfig(session, config2) {
|
|
|
5664
6960
|
};
|
|
5665
6961
|
}
|
|
5666
6962
|
async function writeSessionArtifacts(input) {
|
|
5667
|
-
const { session, feature, config: config2, waypoint } = input;
|
|
6963
|
+
const { session, feature, config: config2, waypoint, prepare } = input;
|
|
5668
6964
|
const dir = sessionDir(session.id);
|
|
5669
|
-
|
|
5670
|
-
const systemPromptPath =
|
|
5671
|
-
const settingsPath =
|
|
5672
|
-
const mcpConfigPath =
|
|
5673
|
-
|
|
6965
|
+
mkdirSync7(dir, { recursive: true });
|
|
6966
|
+
const systemPromptPath = join13(dir, "system-prompt.md");
|
|
6967
|
+
const settingsPath = join13(dir, "settings.json");
|
|
6968
|
+
const mcpConfigPath = join13(dir, "mcp.json");
|
|
6969
|
+
const systemPrompt = feature ? renderSystemPrompt(feature, session.kind, waypoint) : prepare ? renderPreparePrompt(prepare) : (() => {
|
|
6970
|
+
throw new Error(`session ${session.id} has neither a feature nor a preparation brief`);
|
|
6971
|
+
})();
|
|
6972
|
+
writeFileSync2(systemPromptPath, systemPrompt, "utf8");
|
|
5674
6973
|
writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath()), null, 2), "utf8");
|
|
5675
6974
|
writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
|
|
5676
6975
|
return { systemPromptPath, settingsPath, mcpConfigPath };
|
|
5677
6976
|
}
|
|
5678
|
-
var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_ALLOW_RULES;
|
|
6977
|
+
var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_ALLOW_RULES, SESSION_START_SOURCES;
|
|
5679
6978
|
var init_artifacts = __esm(() => {
|
|
5680
6979
|
init_paths();
|
|
5681
6980
|
init_asset_paths();
|
|
@@ -5688,7 +6987,8 @@ var init_artifacts = __esm(() => {
|
|
|
5688
6987
|
"mcp__runcastle__complete_phase",
|
|
5689
6988
|
"mcp__runcastle__escalate_to_map",
|
|
5690
6989
|
"mcp__runcastle__emit_waypoints",
|
|
5691
|
-
"mcp__runcastle__resolve_waypoint"
|
|
6990
|
+
"mcp__runcastle__resolve_waypoint",
|
|
6991
|
+
"mcp__runcastle__record_finding"
|
|
5692
6992
|
];
|
|
5693
6993
|
SESSION_BASH_ALLOW_RULES = [
|
|
5694
6994
|
"Bash(git status:*)",
|
|
@@ -5700,13 +7000,14 @@ var init_artifacts = __esm(() => {
|
|
|
5700
7000
|
"Bash(git branch:*)",
|
|
5701
7001
|
"Bash(git show:*)"
|
|
5702
7002
|
];
|
|
7003
|
+
SESSION_START_SOURCES = ["startup", "resume", "clear", "compact", "fork"];
|
|
5703
7004
|
});
|
|
5704
7005
|
|
|
5705
7006
|
// src/launcher/launcher.ts
|
|
5706
|
-
import { existsSync as
|
|
5707
|
-
import { dirname as
|
|
5708
|
-
import { fileURLToPath as
|
|
5709
|
-
import { and as
|
|
7007
|
+
import { existsSync as existsSync14 } from "fs";
|
|
7008
|
+
import { dirname as dirname6, join as join14, resolve as resolve8 } from "path";
|
|
7009
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
7010
|
+
import { and as and8, eq as eq10 } from "drizzle-orm";
|
|
5710
7011
|
function ptyExitMessage(exitCode) {
|
|
5711
7012
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
5712
7013
|
return `terminal exited (code ${label})`;
|
|
@@ -5720,7 +7021,7 @@ function buildClaudeArgs(input) {
|
|
|
5720
7021
|
input.settingsPath,
|
|
5721
7022
|
"--mcp-config",
|
|
5722
7023
|
input.mcpConfigPath,
|
|
5723
|
-
"--strict-mcp-config",
|
|
7024
|
+
...input.strictMcp ? ["--strict-mcp-config"] : [],
|
|
5724
7025
|
"--plugin-dir",
|
|
5725
7026
|
input.pluginDir,
|
|
5726
7027
|
"--append-system-prompt-file",
|
|
@@ -5737,23 +7038,23 @@ function resolveClaudeExecutable() {
|
|
|
5737
7038
|
function claudeSpawnTarget(claudeArgs) {
|
|
5738
7039
|
return spawnTargetFor(resolveClaudeExecutable(), claudeArgs);
|
|
5739
7040
|
}
|
|
5740
|
-
function resolvePluginDir(fromDir =
|
|
5741
|
-
const rel =
|
|
7041
|
+
function resolvePluginDir(fromDir = dirname6(fileURLToPath6(import.meta.url))) {
|
|
7042
|
+
const rel = join14("packages", "skills", "packs", "runcastle");
|
|
5742
7043
|
const override = process.env[SKILLS_DIR_ENV];
|
|
5743
7044
|
if (override) {
|
|
5744
|
-
const dir2 =
|
|
5745
|
-
if (
|
|
7045
|
+
const dir2 = join14(resolve8(override), "packs", "runcastle");
|
|
7046
|
+
if (existsSync14(dir2))
|
|
5746
7047
|
return dir2;
|
|
5747
7048
|
throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
|
|
5748
7049
|
}
|
|
5749
7050
|
const searched = [];
|
|
5750
7051
|
let dir = fromDir;
|
|
5751
7052
|
for (let i = 0;i < 8; i += 1) {
|
|
5752
|
-
const candidate =
|
|
7053
|
+
const candidate = join14(dir, rel);
|
|
5753
7054
|
searched.push(candidate);
|
|
5754
|
-
if (
|
|
7055
|
+
if (existsSync14(candidate))
|
|
5755
7056
|
return candidate;
|
|
5756
|
-
const parent =
|
|
7057
|
+
const parent = dirname6(dir);
|
|
5757
7058
|
if (parent === dir)
|
|
5758
7059
|
break;
|
|
5759
7060
|
dir = parent;
|
|
@@ -5763,7 +7064,7 @@ function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
|
|
|
5763
7064
|
`)}`);
|
|
5764
7065
|
}
|
|
5765
7066
|
function activeRunFor(ctx, featureId) {
|
|
5766
|
-
const row = ctx.db.select().from(runs).where(
|
|
7067
|
+
const row = ctx.db.select().from(runs).where(and8(eq10(runs.featureId, featureId), eq10(runs.status, "running"))).limit(1).get();
|
|
5767
7068
|
return row ? rowToRun(row) : null;
|
|
5768
7069
|
}
|
|
5769
7070
|
function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
@@ -5776,6 +7077,29 @@ function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
|
5776
7077
|
throw new GateError(`a ${running.workflow} run is in progress on ${feature.slug} \u2014 it holds the feature branch; terminals are available when it finishes`);
|
|
5777
7078
|
}
|
|
5778
7079
|
}
|
|
7080
|
+
function sessionFinished(ctx, feature, session) {
|
|
7081
|
+
if (session.kind !== "waypoint")
|
|
7082
|
+
return feature.mapped;
|
|
7083
|
+
const own = listByFeature(ctx, feature.id).find((w) => w.lastSessionId === session.id);
|
|
7084
|
+
return !!own && (own.status === "resolved" || own.status === "dropped");
|
|
7085
|
+
}
|
|
7086
|
+
function sweepActiveSessions(ctx, feature, endLive) {
|
|
7087
|
+
for (const session of activeSessionsForFeature(ctx, feature.id)) {
|
|
7088
|
+
const finished = sessionFinished(ctx, feature, session);
|
|
7089
|
+
if (!finished && !endLive)
|
|
7090
|
+
continue;
|
|
7091
|
+
endSession(ctx, session.id);
|
|
7092
|
+
emit(ctx, feature.id, {
|
|
7093
|
+
type: "session.auto_ended",
|
|
7094
|
+
message: finished ? `ended the finished ${session.kind} session to work the next waypoint` : `abandoned the in-flight ${session.kind} session to work the next waypoint`,
|
|
7095
|
+
data: {
|
|
7096
|
+
sessionId: session.id,
|
|
7097
|
+
kind: session.kind,
|
|
7098
|
+
reason: finished ? "finished" : "abandoned"
|
|
7099
|
+
}
|
|
7100
|
+
});
|
|
7101
|
+
}
|
|
7102
|
+
}
|
|
5779
7103
|
async function ensureWorktree(ctx, project, feature) {
|
|
5780
7104
|
try {
|
|
5781
7105
|
return await ensureTalkWorktree(project, feature);
|
|
@@ -5883,7 +7207,8 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
5883
7207
|
mcpConfigPath: artifacts.mcpConfigPath,
|
|
5884
7208
|
systemPromptPath: artifacts.systemPromptPath,
|
|
5885
7209
|
model: resolveModel(input.kind, ctx.config, project),
|
|
5886
|
-
resumeSessionId
|
|
7210
|
+
resumeSessionId,
|
|
7211
|
+
strictMcp: ctx.config.sessionMcp === "runcastleOnly"
|
|
5887
7212
|
};
|
|
5888
7213
|
if (opts.spawn === false) {
|
|
5889
7214
|
emit(ctx, feature.id, {
|
|
@@ -5903,6 +7228,75 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
5903
7228
|
});
|
|
5904
7229
|
return { sessionId: session.id };
|
|
5905
7230
|
}
|
|
7231
|
+
async function launchPrepareSession(ctx, input, opts = {}) {
|
|
7232
|
+
const project = requireProjectById(ctx, input.projectId);
|
|
7233
|
+
const session = createSessionRow(ctx, {
|
|
7234
|
+
projectId: project.id,
|
|
7235
|
+
kind: "prepare",
|
|
7236
|
+
worktreePath: project.repoPath
|
|
7237
|
+
});
|
|
7238
|
+
const resumeSessionId = mostRecentResumableProjectSession(ctx, project.id, "prepare")?.ccSessionId;
|
|
7239
|
+
emitProject(ctx, project.id, {
|
|
7240
|
+
type: "session.launching",
|
|
7241
|
+
message: "launching preparation session",
|
|
7242
|
+
data: { sessionId: session.id, kind: "prepare", worktreePath: project.repoPath }
|
|
7243
|
+
});
|
|
7244
|
+
if (resumeSessionId) {
|
|
7245
|
+
emitProject(ctx, project.id, {
|
|
7246
|
+
type: "session.resumed",
|
|
7247
|
+
message: "resuming the previous preparation conversation",
|
|
7248
|
+
data: { sessionId: session.id, resumeSessionId }
|
|
7249
|
+
});
|
|
7250
|
+
}
|
|
7251
|
+
const artifacts = await writeSessionArtifacts({
|
|
7252
|
+
session,
|
|
7253
|
+
project,
|
|
7254
|
+
config: ctx.config,
|
|
7255
|
+
prepare: await buildPrepareBrief(ctx, project)
|
|
7256
|
+
});
|
|
7257
|
+
const serverUrl = serverUrlFor(ctx.config);
|
|
7258
|
+
const buildInput = {
|
|
7259
|
+
sessionId: session.id,
|
|
7260
|
+
serverUrl,
|
|
7261
|
+
featureTitle: `preparing ${project.name}`,
|
|
7262
|
+
worktreePath: project.repoPath,
|
|
7263
|
+
pluginDir: resolvePluginDir(),
|
|
7264
|
+
settingsPath: artifacts.settingsPath,
|
|
7265
|
+
mcpConfigPath: artifacts.mcpConfigPath,
|
|
7266
|
+
systemPromptPath: artifacts.systemPromptPath,
|
|
7267
|
+
model: resolveModel("prepare", ctx.config, project),
|
|
7268
|
+
resumeSessionId,
|
|
7269
|
+
strictMcp: ctx.config.sessionMcp === "runcastleOnly"
|
|
7270
|
+
};
|
|
7271
|
+
if (opts.spawn === false) {
|
|
7272
|
+
emitProject(ctx, project.id, {
|
|
7273
|
+
type: "session.launched",
|
|
7274
|
+
message: "preparation session prepared (terminal spawn skipped)",
|
|
7275
|
+
data: {
|
|
7276
|
+
sessionId: session.id,
|
|
7277
|
+
command: ["claude", ...buildClaudeArgs(buildInput)].join(" "),
|
|
7278
|
+
spawned: false
|
|
7279
|
+
}
|
|
7280
|
+
});
|
|
7281
|
+
return { sessionId: session.id };
|
|
7282
|
+
}
|
|
7283
|
+
spawnEmbeddedPty(ctx, undefined, session, project.repoPath, serverUrl, buildClaudeArgs(buildInput), { resumeSessionId });
|
|
7284
|
+
return { sessionId: session.id };
|
|
7285
|
+
}
|
|
7286
|
+
async function buildPrepareBrief(ctx, project) {
|
|
7287
|
+
const findings = await listFindings(ctx, project);
|
|
7288
|
+
const notes = latestPrepNotes(ctx, project.id);
|
|
7289
|
+
return {
|
|
7290
|
+
project,
|
|
7291
|
+
remainingKeys: keysToPrepare(ctx, project),
|
|
7292
|
+
established: findings.map((f) => ({
|
|
7293
|
+
key: f.key,
|
|
7294
|
+
source: f.source,
|
|
7295
|
+
...f.evidence ? { evidence: f.evidence } : {}
|
|
7296
|
+
})),
|
|
7297
|
+
...notes ? { notes } : {}
|
|
7298
|
+
};
|
|
7299
|
+
}
|
|
5906
7300
|
async function workWaypoint(ctx, input, opts = {}) {
|
|
5907
7301
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
5908
7302
|
if (!feature.mapped) {
|
|
@@ -5919,6 +7313,7 @@ async function workWaypoint(ctx, input, opts = {}) {
|
|
|
5919
7313
|
});
|
|
5920
7314
|
return { runId };
|
|
5921
7315
|
}
|
|
7316
|
+
sweepActiveSessions(ctx, feature, input.endLive === true);
|
|
5922
7317
|
assertSpawnable(ctx, feature);
|
|
5923
7318
|
return launchSession(ctx, { featureId: feature.id, kind: "waypoint", waypointId: wp.id }, opts);
|
|
5924
7319
|
}
|
|
@@ -5975,7 +7370,7 @@ function handlePtyExit(ctx, feature, session, meta, exitCode) {
|
|
|
5975
7370
|
releaseForSession(ctx, session.id);
|
|
5976
7371
|
if (diedBeforeLive && meta.resumeSessionId) {
|
|
5977
7372
|
const label = meta.waypoint ? `waypoint ${meta.waypoint.seq} (${meta.waypoint.title})` : session.kind;
|
|
5978
|
-
|
|
7373
|
+
emitForSession(ctx, session, {
|
|
5979
7374
|
type: "session.resume_failed",
|
|
5980
7375
|
message: `resume failed for ${label} \u2014 the session exited before starting (code ${exitCode ?? "unknown"}); the previous conversation is still resumable`,
|
|
5981
7376
|
data: {
|
|
@@ -5986,7 +7381,7 @@ function handlePtyExit(ctx, feature, session, meta, exitCode) {
|
|
|
5986
7381
|
}
|
|
5987
7382
|
});
|
|
5988
7383
|
}
|
|
5989
|
-
|
|
7384
|
+
emitForSession(ctx, session, {
|
|
5990
7385
|
type: "session.pty_exited",
|
|
5991
7386
|
message: ptyExitMessage(exitCode),
|
|
5992
7387
|
data: { sessionId: session.id, exitCode: exitCode ?? null }
|
|
@@ -6009,15 +7404,16 @@ function spawnEmbeddedPty(ctx, feature, session, worktreePath, serverUrl, claude
|
|
|
6009
7404
|
opts: { cwd: worktreePath, env, cols: 80, rows: 24, useConpty: true },
|
|
6010
7405
|
onExit: ({ exitCode }) => handlePtyExit(ctx, feature, session, meta, exitCode)
|
|
6011
7406
|
});
|
|
6012
|
-
|
|
7407
|
+
emitForSession(ctx, session, {
|
|
6013
7408
|
type: "session.launched",
|
|
6014
7409
|
message: "embedded terminal spawned",
|
|
6015
7410
|
data: { sessionId: session.id, mode: "embedded", pid: entry.pty.pid }
|
|
6016
7411
|
});
|
|
7412
|
+
armSessionReadyWatchdog(ctx, session);
|
|
6017
7413
|
} catch (err) {
|
|
6018
7414
|
markSessionEnded(ctx, session.id);
|
|
6019
7415
|
releaseForSession(ctx, session.id);
|
|
6020
|
-
|
|
7416
|
+
emitForSession(ctx, session, {
|
|
6021
7417
|
type: "session.spawn_failed",
|
|
6022
7418
|
message: `failed to spawn embedded terminal: ${err instanceof Error ? err.message : String(err)}`,
|
|
6023
7419
|
data: { sessionId: session.id, mode: "embedded" }
|
|
@@ -6032,6 +7428,7 @@ var init_launcher = __esm(() => {
|
|
|
6032
7428
|
init_skills_root();
|
|
6033
7429
|
init_schema();
|
|
6034
7430
|
init_errors();
|
|
7431
|
+
init_end_session();
|
|
6035
7432
|
init_registry();
|
|
6036
7433
|
init_events();
|
|
6037
7434
|
init_gates();
|
|
@@ -6040,8 +7437,11 @@ var init_launcher = __esm(() => {
|
|
|
6040
7437
|
init_tickets();
|
|
6041
7438
|
init_waypoints();
|
|
6042
7439
|
init_runner();
|
|
7440
|
+
init_findings();
|
|
7441
|
+
init_prep();
|
|
6043
7442
|
init_artifacts();
|
|
6044
7443
|
init_sessions();
|
|
7444
|
+
init_sessions();
|
|
6045
7445
|
init_end_session();
|
|
6046
7446
|
CC_NESTING_ENV = [
|
|
6047
7447
|
"CLAUDE_CODE_CHILD_SESSION",
|
|
@@ -6056,7 +7456,7 @@ var init_launcher = __esm(() => {
|
|
|
6056
7456
|
});
|
|
6057
7457
|
|
|
6058
7458
|
// src/trpc/routers/feature.ts
|
|
6059
|
-
import * as
|
|
7459
|
+
import * as z7 from "zod";
|
|
6060
7460
|
var gateId, featureRouter;
|
|
6061
7461
|
var init_feature = __esm(() => {
|
|
6062
7462
|
init_src();
|
|
@@ -6067,37 +7467,38 @@ var init_feature = __esm(() => {
|
|
|
6067
7467
|
init_git();
|
|
6068
7468
|
init_repo();
|
|
6069
7469
|
init_context();
|
|
6070
|
-
gateId =
|
|
7470
|
+
gateId = z7.enum(["G1", "G2", "G3", "G4", "G5"]);
|
|
6071
7471
|
featureRouter = router({
|
|
6072
|
-
create: publicProcedure.input(
|
|
6073
|
-
projectId:
|
|
6074
|
-
title:
|
|
6075
|
-
oneLiner:
|
|
6076
|
-
baseBranch:
|
|
7472
|
+
create: publicProcedure.input(z7.object({
|
|
7473
|
+
projectId: z7.string(),
|
|
7474
|
+
title: z7.string().min(1),
|
|
7475
|
+
oneLiner: z7.string(),
|
|
7476
|
+
baseBranch: z7.string().optional()
|
|
6077
7477
|
})).mutation(({ ctx, input }) => createFeature(ctx, input)),
|
|
6078
|
-
list: publicProcedure.input(
|
|
6079
|
-
get: publicProcedure.input(
|
|
6080
|
-
launchSession: publicProcedure.input(
|
|
6081
|
-
featureId:
|
|
7478
|
+
list: publicProcedure.input(z7.object({ projectId: z7.string() })).query(({ ctx, input }) => list(ctx, input.projectId)),
|
|
7479
|
+
get: publicProcedure.input(z7.object({ id: z7.string() })).query(({ ctx, input }) => getFeatureFull(ctx, input.id)),
|
|
7480
|
+
launchSession: publicProcedure.input(z7.object({
|
|
7481
|
+
featureId: z7.string(),
|
|
6082
7482
|
kind: SessionKind,
|
|
6083
|
-
kickoffLine:
|
|
7483
|
+
kickoffLine: z7.string().min(1).optional()
|
|
6084
7484
|
})).mutation(({ ctx, input }) => launchSession(ctx, input)),
|
|
6085
|
-
workWaypoint: publicProcedure.input(
|
|
6086
|
-
converge: publicProcedure.input(
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
7485
|
+
workWaypoint: publicProcedure.input(z7.object({ featureId: z7.string(), waypointId: z7.string(), endLive: z7.boolean().optional() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
|
|
7486
|
+
converge: publicProcedure.input(z7.object({ featureId: z7.string(), overrideReason: z7.string().min(1).optional() })).mutation(({ ctx, input }) => converge(ctx, { featureId: input.featureId, overrideReason: input.overrideReason })),
|
|
7487
|
+
resendKickoff: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => resendKickoff(ctx, input.sessionId)),
|
|
7488
|
+
endSession: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => endSession(ctx, input.sessionId)),
|
|
7489
|
+
advance: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => advance(ctx, input.featureId)),
|
|
7490
|
+
overrideGate: publicProcedure.input(z7.object({ featureId: z7.string(), gate: gateId, reason: z7.string().min(1) })).mutation(({ ctx, input }) => overrideGate(ctx, input.featureId, input.gate, input.reason)),
|
|
7491
|
+
archive: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => archiveFeature(ctx, input.featureId)),
|
|
7492
|
+
unarchive: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => unarchiveFeature(ctx, input.featureId)),
|
|
7493
|
+
delete: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => deleteFeature(ctx, input.featureId)),
|
|
7494
|
+
burn: publicProcedure.input(z7.object({ featureId: z7.string(), model: z7.string().min(1).optional() })).mutation(({ ctx, input }) => burn(ctx, input.featureId, { modelOverride: input.model })),
|
|
7495
|
+
testDrive: publicProcedure.input(z7.object({ featureId: z7.string(), action: z7.enum(["start", "stop"]) })).mutation(async ({ ctx, input }) => {
|
|
6095
7496
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
6096
7497
|
const project = projectForFeature(ctx, feature);
|
|
6097
7498
|
return testDrive(ctx, project, feature, input.action);
|
|
6098
7499
|
}),
|
|
6099
7500
|
driveInfo: publicProcedure.query(() => activeDriveInfo()),
|
|
6100
|
-
merge: publicProcedure.input(
|
|
7501
|
+
merge: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(async ({ ctx, input }) => {
|
|
6101
7502
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
6102
7503
|
const project = projectForFeature(ctx, feature);
|
|
6103
7504
|
if (activeTestDriveFeatureId() === feature.id) {
|
|
@@ -6120,9 +7521,9 @@ var init_feature = __esm(() => {
|
|
|
6120
7521
|
});
|
|
6121
7522
|
|
|
6122
7523
|
// src/services/fsbrowse.ts
|
|
6123
|
-
import { existsSync as
|
|
7524
|
+
import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
|
|
6124
7525
|
import { homedir as homedir2 } from "os";
|
|
6125
|
-
import { basename, dirname as
|
|
7526
|
+
import { basename, dirname as dirname7, isAbsolute, join as join15, resolve as resolve9, sep as sep3 } from "path";
|
|
6126
7527
|
function expandPath(input, home = homedir2()) {
|
|
6127
7528
|
const trimmed = input.trim();
|
|
6128
7529
|
if (trimmed === "")
|
|
@@ -6131,12 +7532,12 @@ function expandPath(input, home = homedir2()) {
|
|
|
6131
7532
|
if (out === "~")
|
|
6132
7533
|
out = home;
|
|
6133
7534
|
else if (out.startsWith("~/") || out.startsWith("~\\"))
|
|
6134
|
-
out =
|
|
6135
|
-
return
|
|
7535
|
+
out = join15(home, out.slice(2));
|
|
7536
|
+
return resolve9(out);
|
|
6136
7537
|
}
|
|
6137
7538
|
function looksLikeRepo(dir) {
|
|
6138
7539
|
try {
|
|
6139
|
-
return
|
|
7540
|
+
return existsSync15(join15(dir, ".git"));
|
|
6140
7541
|
} catch {
|
|
6141
7542
|
return false;
|
|
6142
7543
|
}
|
|
@@ -6147,20 +7548,20 @@ function listRoots(home = homedir2()) {
|
|
|
6147
7548
|
for (let code = 67;code <= 90; code++) {
|
|
6148
7549
|
const drive = `${String.fromCharCode(code)}:${sep3}`;
|
|
6149
7550
|
try {
|
|
6150
|
-
if (
|
|
7551
|
+
if (existsSync15(drive))
|
|
6151
7552
|
roots.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
|
|
6152
7553
|
} catch {}
|
|
6153
7554
|
}
|
|
6154
7555
|
} else {
|
|
6155
7556
|
roots.push({ label: "/", path: "/", kind: "drive" });
|
|
6156
7557
|
}
|
|
6157
|
-
if (
|
|
7558
|
+
if (existsSync15(home))
|
|
6158
7559
|
roots.push({ label: "Home", path: home, kind: "home" });
|
|
6159
7560
|
const seen = new Set(roots.map(pathKey));
|
|
6160
7561
|
for (const name of COMMON_DIRS) {
|
|
6161
|
-
const candidate =
|
|
7562
|
+
const candidate = join15(home, name);
|
|
6162
7563
|
const key = pathKey({ path: candidate });
|
|
6163
|
-
if (seen.has(key) || !
|
|
7564
|
+
if (seen.has(key) || !existsSync15(candidate))
|
|
6164
7565
|
continue;
|
|
6165
7566
|
seen.add(key);
|
|
6166
7567
|
roots.push({ label: name, path: candidate, kind: "common" });
|
|
@@ -6175,7 +7576,7 @@ function crumbsFor(dir) {
|
|
|
6175
7576
|
let cur = dir;
|
|
6176
7577
|
for (;; ) {
|
|
6177
7578
|
out.unshift({ name: basename(cur) || cur, path: cur });
|
|
6178
|
-
const parent =
|
|
7579
|
+
const parent = dirname7(cur);
|
|
6179
7580
|
if (parent === cur)
|
|
6180
7581
|
break;
|
|
6181
7582
|
cur = parent;
|
|
@@ -6183,7 +7584,7 @@ function crumbsFor(dir) {
|
|
|
6183
7584
|
return out;
|
|
6184
7585
|
}
|
|
6185
7586
|
function parentOf(dir) {
|
|
6186
|
-
const parent =
|
|
7587
|
+
const parent = dirname7(dir);
|
|
6187
7588
|
return parent === dir ? null : parent;
|
|
6188
7589
|
}
|
|
6189
7590
|
function browseDir(input, showHidden = false, home = homedir2()) {
|
|
@@ -6191,13 +7592,13 @@ function browseDir(input, showHidden = false, home = homedir2()) {
|
|
|
6191
7592
|
if (!isAbsolute(target)) {
|
|
6192
7593
|
throw new InvalidInputError(`path is not absolute: ${target}`);
|
|
6193
7594
|
}
|
|
6194
|
-
if (!
|
|
7595
|
+
if (!existsSync15(target)) {
|
|
6195
7596
|
throw new InvalidInputError(`path does not exist: ${target}`);
|
|
6196
7597
|
}
|
|
6197
7598
|
let dir = target;
|
|
6198
7599
|
try {
|
|
6199
7600
|
if (!statSync2(target).isDirectory())
|
|
6200
|
-
dir =
|
|
7601
|
+
dir = dirname7(target);
|
|
6201
7602
|
} catch (e) {
|
|
6202
7603
|
throw new InvalidInputError(`cannot read path: ${target} (${errMsg2(e)})`);
|
|
6203
7604
|
}
|
|
@@ -6219,7 +7620,7 @@ function browseDir(input, showHidden = false, home = homedir2()) {
|
|
|
6219
7620
|
continue;
|
|
6220
7621
|
if (!showHidden && name.startsWith("."))
|
|
6221
7622
|
continue;
|
|
6222
|
-
const path =
|
|
7623
|
+
const path = join15(dir, name);
|
|
6223
7624
|
const isSymlink = dirent.isSymbolicLink();
|
|
6224
7625
|
let isDirectory = dirent.isDirectory();
|
|
6225
7626
|
if (isSymlink) {
|
|
@@ -6254,9 +7655,9 @@ var init_fsbrowse = __esm(() => {
|
|
|
6254
7655
|
});
|
|
6255
7656
|
|
|
6256
7657
|
// src/services/projects.ts
|
|
6257
|
-
import { existsSync as
|
|
6258
|
-
import { basename as basename2, join as
|
|
6259
|
-
import { eq as
|
|
7658
|
+
import { existsSync as existsSync16 } from "fs";
|
|
7659
|
+
import { basename as basename2, join as join16 } from "path";
|
|
7660
|
+
import { eq as eq11, isNull } from "drizzle-orm";
|
|
6260
7661
|
function listProjects(ctx) {
|
|
6261
7662
|
return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
|
|
6262
7663
|
}
|
|
@@ -6278,13 +7679,14 @@ async function openProject(ctx, rawPath) {
|
|
|
6278
7679
|
const name = basename2(repoPath) || repoPath;
|
|
6279
7680
|
const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
|
|
6280
7681
|
if (existing) {
|
|
6281
|
-
ctx.db.update(projects).set({ mainBranch, repoPath, closedAt: null }).where(
|
|
7682
|
+
ctx.db.update(projects).set({ mainBranch, repoPath, closedAt: null }).where(eq11(projects.id, existing.id)).run();
|
|
6282
7683
|
const reopened = { ...existing, mainBranch, repoPath };
|
|
6283
7684
|
emitProject(ctx, existing.id, {
|
|
6284
7685
|
type: "project.opened",
|
|
6285
7686
|
message: `project ${existing.name} re-opened at ${repoPath} (${mainBranch})`
|
|
6286
7687
|
});
|
|
6287
7688
|
warnIfTranslatedMount(ctx, existing.id, repoPath);
|
|
7689
|
+
maybeAutoPrepare(ctx, reopened);
|
|
6288
7690
|
return reopened;
|
|
6289
7691
|
}
|
|
6290
7692
|
const row = {
|
|
@@ -6302,14 +7704,25 @@ async function openProject(ctx, rawPath) {
|
|
|
6302
7704
|
message: `project ${name} at ${repoPath} (${mainBranch})`
|
|
6303
7705
|
});
|
|
6304
7706
|
warnIfTranslatedMount(ctx, project.id, repoPath);
|
|
7707
|
+
maybeAutoPrepare(ctx, project);
|
|
6305
7708
|
return project;
|
|
6306
7709
|
}
|
|
7710
|
+
function maybeAutoPrepare(ctx, project) {
|
|
7711
|
+
if (!shouldAutoPrepare(ctx, project))
|
|
7712
|
+
return;
|
|
7713
|
+
startPrep(ctx, project.id).catch((e) => {
|
|
7714
|
+
emitProject(ctx, project.id, {
|
|
7715
|
+
type: "prep.run.failed",
|
|
7716
|
+
message: `could not start preparation: ${e instanceof Error ? e.message : String(e)}`
|
|
7717
|
+
});
|
|
7718
|
+
});
|
|
7719
|
+
}
|
|
6307
7720
|
function closeProject(ctx, projectId) {
|
|
6308
7721
|
const project = requireProjectById(ctx, projectId);
|
|
6309
7722
|
if (projectHasActiveRun(ctx, projectId)) {
|
|
6310
7723
|
throw new InvalidInputError(`cannot close ${project.name}: a run is in flight \u2014 wait for it to finish or cancel it first`);
|
|
6311
7724
|
}
|
|
6312
|
-
ctx.db.update(projects).set({ closedAt: Date.now() }).where(
|
|
7725
|
+
ctx.db.update(projects).set({ closedAt: Date.now() }).where(eq11(projects.id, projectId)).run();
|
|
6313
7726
|
emitProject(ctx, projectId, {
|
|
6314
7727
|
type: "project.closed",
|
|
6315
7728
|
message: `project ${project.name} closed`
|
|
@@ -6318,7 +7731,7 @@ function closeProject(ctx, projectId) {
|
|
|
6318
7731
|
}
|
|
6319
7732
|
function renameProject(ctx, projectId, name) {
|
|
6320
7733
|
const project = requireProjectById(ctx, projectId);
|
|
6321
|
-
ctx.db.update(projects).set({ name }).where(
|
|
7734
|
+
ctx.db.update(projects).set({ name }).where(eq11(projects.id, projectId)).run();
|
|
6322
7735
|
emitProject(ctx, projectId, {
|
|
6323
7736
|
type: "project.renamed",
|
|
6324
7737
|
message: `project renamed ${project.name} \u2192 ${name}`,
|
|
@@ -6336,7 +7749,7 @@ async function assertRepoTolerant(ctx, repoPath) {
|
|
|
6336
7749
|
} catch (e) {
|
|
6337
7750
|
if (!isNotImplemented(e))
|
|
6338
7751
|
throw e;
|
|
6339
|
-
if (!
|
|
7752
|
+
if (!existsSync16(join16(repoPath, ".git"))) {
|
|
6340
7753
|
throw new InvalidInputError(`not a git repository: ${repoPath}`);
|
|
6341
7754
|
}
|
|
6342
7755
|
}
|
|
@@ -6357,31 +7770,63 @@ var init_projects = __esm(() => {
|
|
|
6357
7770
|
init_events();
|
|
6358
7771
|
init_fsbrowse();
|
|
6359
7772
|
init_git();
|
|
7773
|
+
init_prep();
|
|
6360
7774
|
init_repo();
|
|
6361
7775
|
});
|
|
6362
7776
|
|
|
6363
7777
|
// src/trpc/routers/project.ts
|
|
6364
|
-
import * as
|
|
7778
|
+
import * as z8 from "zod";
|
|
6365
7779
|
var projectRouter;
|
|
6366
7780
|
var init_project = __esm(() => {
|
|
7781
|
+
init_src();
|
|
7782
|
+
init_findings();
|
|
6367
7783
|
init_fsbrowse();
|
|
6368
7784
|
init_git();
|
|
7785
|
+
init_prep();
|
|
7786
|
+
init_launcher();
|
|
7787
|
+
init_sessions();
|
|
6369
7788
|
init_projects();
|
|
6370
7789
|
init_repo();
|
|
6371
7790
|
init_context();
|
|
6372
7791
|
projectRouter = router({
|
|
6373
7792
|
list: publicProcedure.query(({ ctx }) => listProjects(ctx)),
|
|
6374
7793
|
roots: publicProcedure.query(() => listRoots()),
|
|
6375
|
-
browse: publicProcedure.input(
|
|
6376
|
-
branches: publicProcedure.input(
|
|
6377
|
-
open: publicProcedure.input(
|
|
6378
|
-
close: publicProcedure.input(
|
|
6379
|
-
rename: publicProcedure.input(
|
|
7794
|
+
browse: publicProcedure.input(z8.object({ dir: z8.string().optional(), showHidden: z8.boolean().optional() }).optional()).query(({ input }) => browseDir(input?.dir, input?.showHidden ?? false)),
|
|
7795
|
+
branches: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => listBranches(requireProjectById(ctx, input.projectId))),
|
|
7796
|
+
open: publicProcedure.input(z8.object({ repoPath: z8.string().min(1) })).mutation(({ ctx, input }) => openProject(ctx, input.repoPath)),
|
|
7797
|
+
close: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ ctx, input }) => closeProject(ctx, input.projectId)),
|
|
7798
|
+
rename: publicProcedure.input(z8.object({ projectId: z8.string(), name: z8.string().min(1) })).mutation(({ ctx, input }) => renameProject(ctx, input.projectId, input.name)),
|
|
7799
|
+
prepare: publicProcedure.input(z8.object({
|
|
7800
|
+
projectId: z8.string(),
|
|
7801
|
+
refresh: z8.boolean().optional(),
|
|
7802
|
+
keys: z8.array(PreparedKey).optional()
|
|
7803
|
+
})).mutation(async ({ ctx, input }) => {
|
|
7804
|
+
const { prepId, keys } = await startPrep(ctx, input.projectId, {
|
|
7805
|
+
...input.refresh !== undefined ? { refresh: input.refresh } : {},
|
|
7806
|
+
...input.keys ? { keys: input.keys } : {}
|
|
7807
|
+
});
|
|
7808
|
+
return { prepId, keys };
|
|
7809
|
+
}),
|
|
7810
|
+
talkToPrep: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ ctx, input }) => launchPrepareSession(ctx, { projectId: input.projectId })),
|
|
7811
|
+
prepSession: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => activeProjectSession(ctx, input.projectId, "prepare")),
|
|
7812
|
+
cancelPrepare: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ input }) => {
|
|
7813
|
+
cancelPrep(input.projectId);
|
|
7814
|
+
return { ok: true };
|
|
7815
|
+
}),
|
|
7816
|
+
prep: publicProcedure.input(z8.object({ projectId: z8.string() })).query(async ({ ctx, input }) => {
|
|
7817
|
+
const project = requireProjectById(ctx, input.projectId);
|
|
7818
|
+
return {
|
|
7819
|
+
latest: latestPrep(ctx, project.id),
|
|
7820
|
+
running: isPreparing(project.id),
|
|
7821
|
+
pendingKeys: keysToPrepare(ctx, project),
|
|
7822
|
+
findings: await listFindings(ctx, project)
|
|
7823
|
+
};
|
|
7824
|
+
})
|
|
6380
7825
|
});
|
|
6381
7826
|
});
|
|
6382
7827
|
|
|
6383
7828
|
// src/trpc/routers/run.ts
|
|
6384
|
-
import * as
|
|
7829
|
+
import * as z9 from "zod";
|
|
6385
7830
|
var runRouter;
|
|
6386
7831
|
var init_run = __esm(() => {
|
|
6387
7832
|
init_agent_stream();
|
|
@@ -6389,9 +7834,9 @@ var init_run = __esm(() => {
|
|
|
6389
7834
|
init_runner();
|
|
6390
7835
|
init_context();
|
|
6391
7836
|
runRouter = router({
|
|
6392
|
-
get: publicProcedure.input(
|
|
6393
|
-
agentTranscript: publicProcedure.input(
|
|
6394
|
-
cancel: publicProcedure.input(
|
|
7837
|
+
get: publicProcedure.input(z9.object({ runId: z9.string() })).query(({ ctx, input }) => getRunRow(ctx, input.runId)),
|
|
7838
|
+
agentTranscript: publicProcedure.input(z9.object({ ticketId: z9.string(), after: z9.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
|
|
7839
|
+
cancel: publicProcedure.input(z9.object({ runId: z9.string() })).mutation(({ input }) => {
|
|
6395
7840
|
cancelRun(input.runId);
|
|
6396
7841
|
return { ok: true };
|
|
6397
7842
|
})
|
|
@@ -6399,15 +7844,15 @@ var init_run = __esm(() => {
|
|
|
6399
7844
|
});
|
|
6400
7845
|
|
|
6401
7846
|
// src/services/settings.ts
|
|
6402
|
-
import { existsSync as
|
|
6403
|
-
import { dirname as
|
|
6404
|
-
import * as
|
|
6405
|
-
import { eq as
|
|
7847
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
7848
|
+
import { dirname as dirname8 } from "path";
|
|
7849
|
+
import * as z10 from "zod";
|
|
7850
|
+
import { eq as eq12 } from "drizzle-orm";
|
|
6406
7851
|
function readRawConfig(configFile) {
|
|
6407
|
-
if (!
|
|
7852
|
+
if (!existsSync17(configFile))
|
|
6408
7853
|
return {};
|
|
6409
7854
|
try {
|
|
6410
|
-
const parsed = JSON.parse(
|
|
7855
|
+
const parsed = JSON.parse(readFileSync7(configFile, "utf8"));
|
|
6411
7856
|
if (typeof parsed !== "object" || parsed === null)
|
|
6412
7857
|
return {};
|
|
6413
7858
|
return foldLegacyModelConfig(parsed);
|
|
@@ -6431,33 +7876,51 @@ function stepModelFields(fileRaw) {
|
|
|
6431
7876
|
});
|
|
6432
7877
|
}
|
|
6433
7878
|
function projectOverrides(ctx, projectId) {
|
|
6434
|
-
const row = ctx.db.select({
|
|
7879
|
+
const row = ctx.db.select({
|
|
7880
|
+
model: projects.model,
|
|
7881
|
+
sandbox: projects.sandbox,
|
|
7882
|
+
devCommand: projects.devCommand,
|
|
7883
|
+
setupCommand: projects.setupCommand,
|
|
7884
|
+
verifyCommands: projects.verifyCommands,
|
|
7885
|
+
knownFailures: projects.knownFailures,
|
|
7886
|
+
dbResetCommand: projects.dbResetCommand,
|
|
7887
|
+
driveSetupCommand: projects.driveSetupCommand,
|
|
7888
|
+
driveStopCommand: projects.driveStopCommand,
|
|
7889
|
+
driveEnv: projects.driveEnv
|
|
7890
|
+
}).from(projects).where(eq12(projects.id, projectId)).get();
|
|
6435
7891
|
return {
|
|
6436
7892
|
model: row?.model ?? null,
|
|
6437
7893
|
sandbox: row?.sandbox ?? null,
|
|
6438
|
-
devCommand: row?.devCommand ?? null
|
|
7894
|
+
devCommand: row?.devCommand ?? null,
|
|
7895
|
+
setupCommand: row?.setupCommand ?? null,
|
|
7896
|
+
verifyCommands: row?.verifyCommands ?? null,
|
|
7897
|
+
knownFailures: row?.knownFailures ?? null,
|
|
7898
|
+
dbResetCommand: row?.dbResetCommand ?? null,
|
|
7899
|
+
driveSetupCommand: row?.driveSetupCommand ?? null,
|
|
7900
|
+
driveStopCommand: row?.driveStopCommand ?? null,
|
|
7901
|
+
driveEnv: row?.driveEnv ?? null
|
|
6439
7902
|
};
|
|
6440
7903
|
}
|
|
6441
|
-
function resolveField(
|
|
7904
|
+
function resolveField(desc5, layers) {
|
|
6442
7905
|
const scoped = layers.overrides !== null;
|
|
6443
|
-
const scope = scoped &&
|
|
6444
|
-
const base = { key:
|
|
6445
|
-
if (
|
|
6446
|
-
const raw = layers.env[
|
|
7906
|
+
const scope = scoped && desc5.projectColumn ? "project" : "global";
|
|
7907
|
+
const base = { key: desc5.key, restartRequired: desc5.restartRequired, scope };
|
|
7908
|
+
if (desc5.envVar) {
|
|
7909
|
+
const raw = layers.env[desc5.envVar];
|
|
6447
7910
|
if (raw !== undefined && raw !== "") {
|
|
6448
|
-
return { ...base, value:
|
|
7911
|
+
return { ...base, value: desc5.parseEnv(raw), source: "env", editable: false };
|
|
6449
7912
|
}
|
|
6450
7913
|
}
|
|
6451
|
-
if (layers.overrides &&
|
|
6452
|
-
const ov = layers.overrides[
|
|
7914
|
+
if (layers.overrides && desc5.projectColumn) {
|
|
7915
|
+
const ov = layers.overrides[desc5.projectColumn];
|
|
6453
7916
|
if (ov !== null && ov !== "") {
|
|
6454
7917
|
return { ...base, value: ov, source: "project", editable: true };
|
|
6455
7918
|
}
|
|
6456
7919
|
}
|
|
6457
|
-
if (
|
|
6458
|
-
return { ...base, value: layers.fileRaw[
|
|
7920
|
+
if (desc5.configKey && Object.prototype.hasOwnProperty.call(layers.fileRaw, desc5.configKey)) {
|
|
7921
|
+
return { ...base, value: layers.fileRaw[desc5.configKey], source: "file", editable: true };
|
|
6459
7922
|
}
|
|
6460
|
-
const value =
|
|
7923
|
+
const value = desc5.configKey ? DEFAULTS[desc5.configKey] ?? null : null;
|
|
6461
7924
|
return { ...base, value, source: "default", editable: true };
|
|
6462
7925
|
}
|
|
6463
7926
|
function getSettings(ctx, projectId, io = {}) {
|
|
@@ -6474,59 +7937,61 @@ function updateSettings(ctx, input, io = {}) {
|
|
|
6474
7937
|
if (input.key.startsWith("stepModels.")) {
|
|
6475
7938
|
return updateStepModel(ctx, input, configFile, io);
|
|
6476
7939
|
}
|
|
6477
|
-
const
|
|
6478
|
-
if (!
|
|
7940
|
+
const desc5 = DESCRIPTORS.find((d) => d.key === input.key);
|
|
7941
|
+
if (!desc5)
|
|
6479
7942
|
throw new InvalidInputError(`unknown setting: ${input.key}`);
|
|
6480
|
-
if (
|
|
6481
|
-
const raw = env[
|
|
7943
|
+
if (desc5.envVar) {
|
|
7944
|
+
const raw = env[desc5.envVar];
|
|
6482
7945
|
if (raw !== undefined && raw !== "") {
|
|
6483
|
-
throw new InvalidInputError(`${
|
|
7946
|
+
throw new InvalidInputError(`${desc5.key} is set by environment variable ${desc5.envVar}; unset it to edit`);
|
|
6484
7947
|
}
|
|
6485
7948
|
}
|
|
6486
|
-
const toProject = input.projectId !== undefined &&
|
|
7949
|
+
const toProject = input.projectId !== undefined && desc5.projectColumn !== undefined;
|
|
6487
7950
|
if (input.value === null) {
|
|
6488
7951
|
if (!toProject)
|
|
6489
|
-
throw new InvalidInputError(`${
|
|
7952
|
+
throw new InvalidInputError(`${desc5.key} cannot be cleared`);
|
|
6490
7953
|
const project = requireProjectById(ctx, input.projectId);
|
|
6491
|
-
ctx.db.update(projects).set({ [
|
|
7954
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(eq12(projects.id, project.id)).run();
|
|
7955
|
+
recordHuman(ctx, project.id, desc5.key, null);
|
|
6492
7956
|
emitProject(ctx, project.id, {
|
|
6493
7957
|
type: "settings.updated",
|
|
6494
|
-
message: `${
|
|
6495
|
-
data: { key:
|
|
7958
|
+
message: `${desc5.key} override cleared`,
|
|
7959
|
+
data: { key: desc5.key, scope: "project", value: null }
|
|
6496
7960
|
});
|
|
6497
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
7961
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
6498
7962
|
}
|
|
6499
|
-
const parsed =
|
|
7963
|
+
const parsed = desc5.valueSchema.safeParse(input.value);
|
|
6500
7964
|
if (!parsed.success) {
|
|
6501
|
-
throw new InvalidInputError(`invalid value for ${
|
|
7965
|
+
throw new InvalidInputError(`invalid value for ${desc5.key}: ${parsed.error.issues[0]?.message}`);
|
|
6502
7966
|
}
|
|
6503
7967
|
const value = parsed.data;
|
|
6504
7968
|
if (toProject) {
|
|
6505
7969
|
const project = requireProjectById(ctx, input.projectId);
|
|
6506
|
-
ctx.db.update(projects).set({ [
|
|
7970
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(eq12(projects.id, project.id)).run();
|
|
7971
|
+
recordHuman(ctx, project.id, desc5.key, String(value));
|
|
6507
7972
|
emitProject(ctx, project.id, {
|
|
6508
7973
|
type: "settings.updated",
|
|
6509
|
-
message: `${
|
|
6510
|
-
data: { key:
|
|
7974
|
+
message: `${desc5.key} override set to ${String(value)}`,
|
|
7975
|
+
data: { key: desc5.key, scope: "project", value }
|
|
6511
7976
|
});
|
|
6512
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
7977
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
6513
7978
|
}
|
|
6514
|
-
if (!
|
|
6515
|
-
throw new InvalidInputError(`${
|
|
7979
|
+
if (!desc5.configKey) {
|
|
7980
|
+
throw new InvalidInputError(`${desc5.key} is a per-project setting; provide a projectId`);
|
|
6516
7981
|
}
|
|
6517
|
-
writeGlobal(configFile,
|
|
6518
|
-
ctx.config[
|
|
7982
|
+
writeGlobal(configFile, desc5.configKey, value);
|
|
7983
|
+
ctx.config[desc5.configKey] = value;
|
|
6519
7984
|
emitProject(ctx, GLOBAL_EVENT_KEY, {
|
|
6520
7985
|
type: "settings.updated",
|
|
6521
|
-
message: `${
|
|
6522
|
-
data: { key:
|
|
7986
|
+
message: `${desc5.key} set to ${String(value)}`,
|
|
7987
|
+
data: { key: desc5.key, scope: "global", value }
|
|
6523
7988
|
});
|
|
6524
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
7989
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
6525
7990
|
}
|
|
6526
7991
|
function writeGlobal(configFile, configKey, value) {
|
|
6527
7992
|
const raw = readRawConfig(configFile);
|
|
6528
7993
|
raw[configKey] = value;
|
|
6529
|
-
|
|
7994
|
+
mkdirSync8(dirname8(configFile), { recursive: true });
|
|
6530
7995
|
writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
|
|
6531
7996
|
`);
|
|
6532
7997
|
}
|
|
@@ -6537,7 +8002,7 @@ function updateStepModel(ctx, input, configFile, io) {
|
|
|
6537
8002
|
const modelStep = step;
|
|
6538
8003
|
let value = null;
|
|
6539
8004
|
if (input.value !== null) {
|
|
6540
|
-
const parsed =
|
|
8005
|
+
const parsed = z10.string().min(1).safeParse(input.value);
|
|
6541
8006
|
if (!parsed.success) {
|
|
6542
8007
|
throw new InvalidInputError(`invalid value for ${input.key}: ${parsed.error.issues[0]?.message}`);
|
|
6543
8008
|
}
|
|
@@ -6565,7 +8030,7 @@ function writeStepModel(configFile, step, value) {
|
|
|
6565
8030
|
else
|
|
6566
8031
|
stepModels[step] = value;
|
|
6567
8032
|
raw.stepModels = stepModels;
|
|
6568
|
-
|
|
8033
|
+
mkdirSync8(dirname8(configFile), { recursive: true });
|
|
6569
8034
|
writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
|
|
6570
8035
|
`);
|
|
6571
8036
|
}
|
|
@@ -6582,6 +8047,7 @@ var init_settings = __esm(() => {
|
|
|
6582
8047
|
init_schema();
|
|
6583
8048
|
init_errors();
|
|
6584
8049
|
init_events();
|
|
8050
|
+
init_findings();
|
|
6585
8051
|
init_repo();
|
|
6586
8052
|
DESCRIPTORS = [
|
|
6587
8053
|
{
|
|
@@ -6589,7 +8055,7 @@ var init_settings = __esm(() => {
|
|
|
6589
8055
|
configKey: "serverPort",
|
|
6590
8056
|
envVar: "RUNCASTLE_SERVER_PORT",
|
|
6591
8057
|
restartRequired: true,
|
|
6592
|
-
valueSchema:
|
|
8058
|
+
valueSchema: z10.number().int().positive(),
|
|
6593
8059
|
parseEnv: (raw) => Number(raw)
|
|
6594
8060
|
},
|
|
6595
8061
|
{
|
|
@@ -6598,7 +8064,7 @@ var init_settings = __esm(() => {
|
|
|
6598
8064
|
envVar: "RUNCASTLE_MODEL",
|
|
6599
8065
|
projectColumn: "model",
|
|
6600
8066
|
restartRequired: false,
|
|
6601
|
-
valueSchema:
|
|
8067
|
+
valueSchema: z10.string().min(1),
|
|
6602
8068
|
parseEnv: idEnv
|
|
6603
8069
|
},
|
|
6604
8070
|
{
|
|
@@ -6607,7 +8073,7 @@ var init_settings = __esm(() => {
|
|
|
6607
8073
|
envVar: "RUNCASTLE_SANDBOX",
|
|
6608
8074
|
projectColumn: "sandbox",
|
|
6609
8075
|
restartRequired: false,
|
|
6610
|
-
valueSchema:
|
|
8076
|
+
valueSchema: z10.enum(["docker", "podman", "noSandbox"]),
|
|
6611
8077
|
parseEnv: idEnv
|
|
6612
8078
|
},
|
|
6613
8079
|
{
|
|
@@ -6615,7 +8081,15 @@ var init_settings = __esm(() => {
|
|
|
6615
8081
|
configKey: "sandboxImage",
|
|
6616
8082
|
envVar: "RUNCASTLE_SANDBOX_IMAGE",
|
|
6617
8083
|
restartRequired: false,
|
|
6618
|
-
valueSchema:
|
|
8084
|
+
valueSchema: z10.string().min(1),
|
|
8085
|
+
parseEnv: idEnv
|
|
8086
|
+
},
|
|
8087
|
+
{
|
|
8088
|
+
key: "sessionMcp",
|
|
8089
|
+
configKey: "sessionMcp",
|
|
8090
|
+
envVar: "RUNCASTLE_SESSION_MCP",
|
|
8091
|
+
restartRequired: false,
|
|
8092
|
+
valueSchema: z10.enum(["inherit", "runcastleOnly"]),
|
|
6619
8093
|
parseEnv: idEnv
|
|
6620
8094
|
},
|
|
6621
8095
|
{
|
|
@@ -6623,7 +8097,7 @@ var init_settings = __esm(() => {
|
|
|
6623
8097
|
configKey: "burnConcurrency",
|
|
6624
8098
|
envVar: "RUNCASTLE_BURN_CONCURRENCY",
|
|
6625
8099
|
restartRequired: false,
|
|
6626
|
-
valueSchema:
|
|
8100
|
+
valueSchema: z10.number().int().min(1).max(8),
|
|
6627
8101
|
parseEnv: (raw) => Number(raw)
|
|
6628
8102
|
},
|
|
6629
8103
|
{
|
|
@@ -6631,7 +8105,7 @@ var init_settings = __esm(() => {
|
|
|
6631
8105
|
configKey: "burnMaxIterations",
|
|
6632
8106
|
envVar: "RUNCASTLE_BURN_MAX_ITERATIONS",
|
|
6633
8107
|
restartRequired: false,
|
|
6634
|
-
valueSchema:
|
|
8108
|
+
valueSchema: z10.number().int().min(1).max(10),
|
|
6635
8109
|
parseEnv: (raw) => Number(raw)
|
|
6636
8110
|
},
|
|
6637
8111
|
{
|
|
@@ -6639,7 +8113,7 @@ var init_settings = __esm(() => {
|
|
|
6639
8113
|
configKey: "burnAttempts",
|
|
6640
8114
|
envVar: "RUNCASTLE_BURN_ATTEMPTS",
|
|
6641
8115
|
restartRequired: false,
|
|
6642
|
-
valueSchema:
|
|
8116
|
+
valueSchema: z10.number().int().min(1).max(5),
|
|
6643
8117
|
parseEnv: (raw) => Number(raw)
|
|
6644
8118
|
},
|
|
6645
8119
|
{
|
|
@@ -6647,7 +8121,7 @@ var init_settings = __esm(() => {
|
|
|
6647
8121
|
configKey: "burnConflictAttempts",
|
|
6648
8122
|
envVar: "RUNCASTLE_BURN_CONFLICT_ATTEMPTS",
|
|
6649
8123
|
restartRequired: false,
|
|
6650
|
-
valueSchema:
|
|
8124
|
+
valueSchema: z10.number().int().min(0).max(3),
|
|
6651
8125
|
parseEnv: (raw) => Number(raw)
|
|
6652
8126
|
},
|
|
6653
8127
|
{
|
|
@@ -6655,31 +8129,42 @@ var init_settings = __esm(() => {
|
|
|
6655
8129
|
configKey: "burnCpus",
|
|
6656
8130
|
envVar: "RUNCASTLE_BURN_CPUS",
|
|
6657
8131
|
restartRequired: false,
|
|
6658
|
-
valueSchema:
|
|
8132
|
+
valueSchema: z10.number().positive().max(256),
|
|
6659
8133
|
parseEnv: (raw) => Number(raw)
|
|
6660
8134
|
},
|
|
8135
|
+
{
|
|
8136
|
+
key: "autoPrepare",
|
|
8137
|
+
configKey: "autoPrepare",
|
|
8138
|
+
envVar: "RUNCASTLE_AUTO_PREPARE",
|
|
8139
|
+
restartRequired: false,
|
|
8140
|
+
valueSchema: z10.boolean(),
|
|
8141
|
+
parseEnv: (raw) => raw !== "0" && raw.toLowerCase() !== "false"
|
|
8142
|
+
},
|
|
6661
8143
|
{
|
|
6662
8144
|
key: "setupCommand",
|
|
6663
8145
|
configKey: "setupCommand",
|
|
6664
8146
|
envVar: "RUNCASTLE_SETUP_COMMAND",
|
|
8147
|
+
projectColumn: "setupCommand",
|
|
6665
8148
|
restartRequired: false,
|
|
6666
|
-
valueSchema:
|
|
8149
|
+
valueSchema: z10.string().min(1),
|
|
6667
8150
|
parseEnv: idEnv
|
|
6668
8151
|
},
|
|
6669
8152
|
{
|
|
6670
8153
|
key: "verifyCommands",
|
|
6671
8154
|
configKey: "verifyCommands",
|
|
6672
8155
|
envVar: "RUNCASTLE_VERIFY_COMMANDS",
|
|
8156
|
+
projectColumn: "verifyCommands",
|
|
6673
8157
|
restartRequired: false,
|
|
6674
|
-
valueSchema:
|
|
8158
|
+
valueSchema: z10.string().min(1),
|
|
6675
8159
|
parseEnv: idEnv
|
|
6676
8160
|
},
|
|
6677
8161
|
{
|
|
6678
8162
|
key: "knownFailures",
|
|
6679
8163
|
configKey: "knownFailures",
|
|
6680
8164
|
envVar: "RUNCASTLE_KNOWN_FAILURES",
|
|
8165
|
+
projectColumn: "knownFailures",
|
|
6681
8166
|
restartRequired: false,
|
|
6682
|
-
valueSchema:
|
|
8167
|
+
valueSchema: z10.string().min(1),
|
|
6683
8168
|
parseEnv: idEnv
|
|
6684
8169
|
},
|
|
6685
8170
|
{
|
|
@@ -6687,14 +8172,42 @@ var init_settings = __esm(() => {
|
|
|
6687
8172
|
configKey: "mainBranch",
|
|
6688
8173
|
envVar: "RUNCASTLE_MAIN_BRANCH",
|
|
6689
8174
|
restartRequired: false,
|
|
6690
|
-
valueSchema:
|
|
8175
|
+
valueSchema: z10.string().min(1),
|
|
6691
8176
|
parseEnv: idEnv
|
|
6692
8177
|
},
|
|
6693
8178
|
{
|
|
6694
8179
|
key: "devCommand",
|
|
6695
8180
|
projectColumn: "devCommand",
|
|
6696
8181
|
restartRequired: false,
|
|
6697
|
-
valueSchema:
|
|
8182
|
+
valueSchema: z10.string().min(1),
|
|
8183
|
+
parseEnv: idEnv
|
|
8184
|
+
},
|
|
8185
|
+
{
|
|
8186
|
+
key: "dbResetCommand",
|
|
8187
|
+
projectColumn: "dbResetCommand",
|
|
8188
|
+
restartRequired: false,
|
|
8189
|
+
valueSchema: z10.string().min(1),
|
|
8190
|
+
parseEnv: idEnv
|
|
8191
|
+
},
|
|
8192
|
+
{
|
|
8193
|
+
key: "driveSetupCommand",
|
|
8194
|
+
projectColumn: "driveSetupCommand",
|
|
8195
|
+
restartRequired: false,
|
|
8196
|
+
valueSchema: z10.string().min(1),
|
|
8197
|
+
parseEnv: idEnv
|
|
8198
|
+
},
|
|
8199
|
+
{
|
|
8200
|
+
key: "driveStopCommand",
|
|
8201
|
+
projectColumn: "driveStopCommand",
|
|
8202
|
+
restartRequired: false,
|
|
8203
|
+
valueSchema: z10.string().min(1),
|
|
8204
|
+
parseEnv: idEnv
|
|
8205
|
+
},
|
|
8206
|
+
{
|
|
8207
|
+
key: "driveEnv",
|
|
8208
|
+
projectColumn: "driveEnv",
|
|
8209
|
+
restartRequired: false,
|
|
8210
|
+
valueSchema: z10.string().min(1),
|
|
6698
8211
|
parseEnv: idEnv
|
|
6699
8212
|
}
|
|
6700
8213
|
];
|
|
@@ -6703,14 +8216,14 @@ var init_settings = __esm(() => {
|
|
|
6703
8216
|
});
|
|
6704
8217
|
|
|
6705
8218
|
// src/trpc/routers/settings.ts
|
|
6706
|
-
import * as
|
|
8219
|
+
import * as z11 from "zod";
|
|
6707
8220
|
var settingsRouter;
|
|
6708
8221
|
var init_settings2 = __esm(() => {
|
|
6709
8222
|
init_src();
|
|
6710
8223
|
init_settings();
|
|
6711
8224
|
init_context();
|
|
6712
8225
|
settingsRouter = router({
|
|
6713
|
-
get: publicProcedure.input(
|
|
8226
|
+
get: publicProcedure.input(z11.object({ projectId: z11.string().optional() }).optional()).query(({ ctx, input }) => getSettings(ctx, input?.projectId)),
|
|
6714
8227
|
update: publicProcedure.input(SettingsUpdateInput).mutation(({ ctx, input }) => updateSettings(ctx, input))
|
|
6715
8228
|
});
|
|
6716
8229
|
});
|
|
@@ -6877,13 +8390,13 @@ var init_doctor = __esm(() => {
|
|
|
6877
8390
|
});
|
|
6878
8391
|
|
|
6879
8392
|
// src/doctor/system-exec.ts
|
|
6880
|
-
import { spawn as
|
|
8393
|
+
import { spawn as spawn3 } from "child_process";
|
|
6881
8394
|
function createSystemExec(opts = {}) {
|
|
6882
|
-
return (command, args) => new Promise((
|
|
8395
|
+
return (command, args) => new Promise((resolve10) => {
|
|
6883
8396
|
const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
|
|
6884
8397
|
let stdout = "";
|
|
6885
8398
|
let stderr = "";
|
|
6886
|
-
const child =
|
|
8399
|
+
const child = spawn3(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
|
|
6887
8400
|
child.stdout?.on("data", (d) => {
|
|
6888
8401
|
stdout += d.toString();
|
|
6889
8402
|
});
|
|
@@ -6891,10 +8404,10 @@ function createSystemExec(opts = {}) {
|
|
|
6891
8404
|
stderr += d.toString();
|
|
6892
8405
|
});
|
|
6893
8406
|
child.on("error", (err) => {
|
|
6894
|
-
|
|
8407
|
+
resolve10({ ok: false, code: null, stdout, stderr: stderr || String(err) });
|
|
6895
8408
|
});
|
|
6896
8409
|
child.on("close", (code) => {
|
|
6897
|
-
|
|
8410
|
+
resolve10({ ok: true, code: code ?? 0, stdout, stderr });
|
|
6898
8411
|
});
|
|
6899
8412
|
});
|
|
6900
8413
|
}
|
|
@@ -6903,9 +8416,9 @@ var init_system_exec = __esm(() => {
|
|
|
6903
8416
|
});
|
|
6904
8417
|
|
|
6905
8418
|
// src/services/setup.ts
|
|
6906
|
-
import { copyFileSync, existsSync as
|
|
6907
|
-
import { dirname as
|
|
6908
|
-
import { fileURLToPath as
|
|
8419
|
+
import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
8420
|
+
import { dirname as dirname9, join as join17 } from "path";
|
|
8421
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
6909
8422
|
async function writeGitIdentity(exec, identity) {
|
|
6910
8423
|
const name = identity.name.trim();
|
|
6911
8424
|
const email = identity.email.trim();
|
|
@@ -6980,16 +8493,16 @@ async function resolveRuntime(exec, preferred) {
|
|
|
6980
8493
|
function resolveSandcastleBin() {
|
|
6981
8494
|
try {
|
|
6982
8495
|
const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
|
|
6983
|
-
let dir =
|
|
8496
|
+
let dir = dirname9(fileURLToPath7(mainUrl));
|
|
6984
8497
|
for (let hops = 0;hops < 6; hops++) {
|
|
6985
|
-
const manifestPath =
|
|
6986
|
-
if (
|
|
6987
|
-
const manifest = JSON.parse(
|
|
8498
|
+
const manifestPath = join17(dir, "package.json");
|
|
8499
|
+
if (existsSync18(manifestPath)) {
|
|
8500
|
+
const manifest = JSON.parse(readFileSync8(manifestPath, "utf8"));
|
|
6988
8501
|
if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
|
|
6989
|
-
return
|
|
8502
|
+
return join17(dir, manifest.bin.sandcastle);
|
|
6990
8503
|
}
|
|
6991
8504
|
}
|
|
6992
|
-
const parent =
|
|
8505
|
+
const parent = dirname9(dir);
|
|
6993
8506
|
if (parent === dir)
|
|
6994
8507
|
break;
|
|
6995
8508
|
dir = parent;
|
|
@@ -7011,21 +8524,21 @@ function terminalSpec(kind, opts) {
|
|
|
7011
8524
|
};
|
|
7012
8525
|
}
|
|
7013
8526
|
function sandcastleTemplateDir() {
|
|
7014
|
-
return resolveAsset(ASSET_ENV.sandcastleTemplate,
|
|
8527
|
+
return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath7(new URL("../assets/sandcastle", import.meta.url)));
|
|
7015
8528
|
}
|
|
7016
8529
|
function scaffoldSandcastleConfig(templateDir, targetDir) {
|
|
7017
|
-
const dir =
|
|
7018
|
-
if (
|
|
8530
|
+
const dir = join17(targetDir, ".sandcastle");
|
|
8531
|
+
if (existsSync18(dir))
|
|
7019
8532
|
return { scaffolded: false, dir };
|
|
7020
|
-
|
|
8533
|
+
mkdirSync9(dir, { recursive: true });
|
|
7021
8534
|
for (const name of readdirSync6(templateDir)) {
|
|
7022
|
-
copyFileSync(
|
|
8535
|
+
copyFileSync(join17(templateDir, name), join17(dir, name));
|
|
7023
8536
|
}
|
|
7024
8537
|
return { scaffolded: true, dir };
|
|
7025
8538
|
}
|
|
7026
8539
|
function prepareSandboxBuildContext() {
|
|
7027
8540
|
const target = sandboxBuildDir();
|
|
7028
|
-
|
|
8541
|
+
mkdirSync9(target, { recursive: true });
|
|
7029
8542
|
scaffoldSandcastleConfig(sandcastleTemplateDir(), target);
|
|
7030
8543
|
return target;
|
|
7031
8544
|
}
|
|
@@ -7061,9 +8574,9 @@ function createTokenVerifier(exec) {
|
|
|
7061
8574
|
function fileAfkTokenIo(exec) {
|
|
7062
8575
|
const path = envPath();
|
|
7063
8576
|
return {
|
|
7064
|
-
read: () =>
|
|
8577
|
+
read: () => existsSync18(path) ? readFileSync8(path, "utf8") : "",
|
|
7065
8578
|
write: (content) => {
|
|
7066
|
-
|
|
8579
|
+
mkdirSync9(dirname9(path), { recursive: true });
|
|
7067
8580
|
writeFileSync4(path, content, "utf8");
|
|
7068
8581
|
},
|
|
7069
8582
|
verify: createTokenVerifier(exec)
|
|
@@ -7078,7 +8591,7 @@ var init_setup = __esm(() => {
|
|
|
7078
8591
|
});
|
|
7079
8592
|
|
|
7080
8593
|
// src/trpc/routers/setup.ts
|
|
7081
|
-
import * as
|
|
8594
|
+
import * as z12 from "zod";
|
|
7082
8595
|
var setupRouter;
|
|
7083
8596
|
var init_setup2 = __esm(() => {
|
|
7084
8597
|
init_src();
|
|
@@ -7094,9 +8607,9 @@ var init_setup2 = __esm(() => {
|
|
|
7094
8607
|
...ctx.config.sandboxImage ? { imageName: ctx.config.sandboxImage } : {}
|
|
7095
8608
|
})),
|
|
7096
8609
|
runtimeGuide: publicProcedure.query(() => runtimeInstallGuide(process.platform)),
|
|
7097
|
-
gitIdentity: publicProcedure.input(
|
|
7098
|
-
afkToken: publicProcedure.input(
|
|
7099
|
-
startTerminal: publicProcedure.input(
|
|
8610
|
+
gitIdentity: publicProcedure.input(z12.object({ name: z12.string(), email: z12.string() })).mutation(({ input }) => writeGitIdentity(createSystemExec(), input)),
|
|
8611
|
+
afkToken: publicProcedure.input(z12.object({ token: z12.string() })).mutation(({ input }) => saveAfkToken(fileAfkTokenIo(createSystemExec()), input.token)),
|
|
8612
|
+
startTerminal: publicProcedure.input(z12.object({ kind: z12.enum(["setup-token", "build-image"]) })).mutation(async ({ ctx, input }) => {
|
|
7100
8613
|
const exec = createSystemExec();
|
|
7101
8614
|
const preferred = ctx.config.sandbox === "podman" ? "podman" : "docker";
|
|
7102
8615
|
const runtime = await resolveRuntime(exec, preferred);
|
|
@@ -7196,16 +8709,16 @@ var init_update_check = __esm(() => {
|
|
|
7196
8709
|
});
|
|
7197
8710
|
|
|
7198
8711
|
// src/version.ts
|
|
7199
|
-
import { readFileSync as
|
|
7200
|
-
import { dirname as
|
|
7201
|
-
import { fileURLToPath as
|
|
8712
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
8713
|
+
import { dirname as dirname10, join as join18 } from "path";
|
|
8714
|
+
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
7202
8715
|
function runcastleVersion() {
|
|
7203
8716
|
if (cached2)
|
|
7204
8717
|
return cached2;
|
|
7205
|
-
const here =
|
|
7206
|
-
for (const pkgPath of [
|
|
8718
|
+
const here = dirname10(fileURLToPath8(import.meta.url));
|
|
8719
|
+
for (const pkgPath of [join18(here, "..", "package.json"), join18(here, "package.json")]) {
|
|
7207
8720
|
try {
|
|
7208
|
-
const pkg = JSON.parse(
|
|
8721
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
7209
8722
|
if (typeof pkg.version === "string") {
|
|
7210
8723
|
cached2 = pkg.version;
|
|
7211
8724
|
return cached2;
|
|
@@ -7231,7 +8744,7 @@ var init_system = __esm(() => {
|
|
|
7231
8744
|
});
|
|
7232
8745
|
|
|
7233
8746
|
// src/trpc/routers/ticket.ts
|
|
7234
|
-
import * as
|
|
8747
|
+
import * as z13 from "zod";
|
|
7235
8748
|
var ticketRouter;
|
|
7236
8749
|
var init_ticket = __esm(() => {
|
|
7237
8750
|
init_features();
|
|
@@ -7240,8 +8753,8 @@ var init_ticket = __esm(() => {
|
|
|
7240
8753
|
init_ticket_burner();
|
|
7241
8754
|
init_context();
|
|
7242
8755
|
ticketRouter = router({
|
|
7243
|
-
retry: publicProcedure.input(
|
|
7244
|
-
stop: publicProcedure.input(
|
|
8756
|
+
retry: publicProcedure.input(z13.object({ ticketId: z13.string(), fresh: z13.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
|
|
8757
|
+
stop: publicProcedure.input(z13.object({ ticketId: z13.string() })).mutation(({ ctx, input }) => {
|
|
7245
8758
|
if (stopTicketRun(input.ticketId))
|
|
7246
8759
|
return { stopped: true, swept: false };
|
|
7247
8760
|
const ticket = getTicket(ctx, input.ticketId);
|
|
@@ -7251,7 +8764,7 @@ var init_ticket = __esm(() => {
|
|
|
7251
8764
|
}
|
|
7252
8765
|
return { stopped: false, swept: false };
|
|
7253
8766
|
}),
|
|
7254
|
-
cancel: publicProcedure.input(
|
|
8767
|
+
cancel: publicProcedure.input(z13.object({ ticketId: z13.string(), reason: z13.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason))
|
|
7255
8768
|
});
|
|
7256
8769
|
});
|
|
7257
8770
|
|
|
@@ -7282,18 +8795,18 @@ var init_router = __esm(() => {
|
|
|
7282
8795
|
});
|
|
7283
8796
|
|
|
7284
8797
|
// src/workflows/reconcile-runs.ts
|
|
7285
|
-
import { eq as
|
|
8798
|
+
import { eq as eq13 } from "drizzle-orm";
|
|
7286
8799
|
async function reconcileStaleRuns(ctx) {
|
|
7287
|
-
const stale = ctx.db.select().from(runs).where(
|
|
8800
|
+
const stale = ctx.db.select().from(runs).where(eq13(runs.status, "running")).all().map(rowToRun);
|
|
7288
8801
|
const reconciled = [];
|
|
7289
|
-
for (const
|
|
7290
|
-
if (isRunActive(
|
|
8802
|
+
for (const run4 of stale) {
|
|
8803
|
+
if (isRunActive(run4.id))
|
|
7291
8804
|
continue;
|
|
7292
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(
|
|
7293
|
-
const released = releaseForSession(ctx,
|
|
7294
|
-
const swept = workflowClaimsFeatureBranch(
|
|
7295
|
-
if (workflowClaimsFeatureBranch(
|
|
7296
|
-
const feature = tryGetFeature(ctx,
|
|
8805
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq13(runs.id, run4.id)).run();
|
|
8806
|
+
const released = releaseForSession(ctx, run4.id);
|
|
8807
|
+
const swept = workflowClaimsFeatureBranch(run4.workflow) ? sweepOrphanedBurning(ctx, run4.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
|
|
8808
|
+
if (workflowClaimsFeatureBranch(run4.workflow)) {
|
|
8809
|
+
const feature = tryGetFeature(ctx, run4.featureId);
|
|
7297
8810
|
const project = feature ? getProjectById(ctx, feature.projectId) : null;
|
|
7298
8811
|
if (feature && project) {
|
|
7299
8812
|
try {
|
|
@@ -7301,18 +8814,18 @@ async function reconcileStaleRuns(ctx) {
|
|
|
7301
8814
|
} catch {}
|
|
7302
8815
|
}
|
|
7303
8816
|
}
|
|
7304
|
-
emit(ctx,
|
|
8817
|
+
emit(ctx, run4.featureId, {
|
|
7305
8818
|
type: "run.reconciled",
|
|
7306
|
-
message: `run marked failed at boot \u2014 the server restarted while it was running (${
|
|
7307
|
-
runId:
|
|
8819
|
+
message: `run marked failed at boot \u2014 the server restarted while it was running (${run4.workflow})`,
|
|
8820
|
+
runId: run4.id,
|
|
7308
8821
|
data: {
|
|
7309
|
-
runId:
|
|
7310
|
-
workflow:
|
|
8822
|
+
runId: run4.id,
|
|
8823
|
+
workflow: run4.workflow,
|
|
7311
8824
|
releasedWaypointIds: released.map((w) => w.id),
|
|
7312
8825
|
sweptTicketSeqs: swept.map((t2) => t2.seq)
|
|
7313
8826
|
}
|
|
7314
8827
|
});
|
|
7315
|
-
reconciled.push(
|
|
8828
|
+
reconciled.push(run4);
|
|
7316
8829
|
}
|
|
7317
8830
|
for (const project of allProjects(ctx)) {
|
|
7318
8831
|
try {
|
|
@@ -7343,7 +8856,7 @@ import { Hono as Hono4 } from "hono";
|
|
|
7343
8856
|
function buildApp(ctx) {
|
|
7344
8857
|
const app = new Hono4;
|
|
7345
8858
|
setRuntimeCtx(ctx);
|
|
7346
|
-
app.get("/health", (c) => c.json({ ok: true }));
|
|
8859
|
+
app.get("/health", (c) => c.json({ ok: true, dataDir: dataDir() }));
|
|
7347
8860
|
app.use("/api/trpc/*", async (c, next) => {
|
|
7348
8861
|
await next();
|
|
7349
8862
|
if (c.res.headers.get("content-type") === "application/json") {
|
|
@@ -7375,6 +8888,10 @@ async function startServer() {
|
|
|
7375
8888
|
if (staleRuns.length > 0) {
|
|
7376
8889
|
console.log(`reconciled ${staleRuns.length} stale run(s) from a previous server run`);
|
|
7377
8890
|
}
|
|
8891
|
+
const stalePreps = reconcilePreps(ctx);
|
|
8892
|
+
if (stalePreps > 0) {
|
|
8893
|
+
console.log(`reconciled ${stalePreps} stale preparation run(s) from a previous server run`);
|
|
8894
|
+
}
|
|
7378
8895
|
const server = Bun.serve({
|
|
7379
8896
|
port: config2.serverPort,
|
|
7380
8897
|
fetch(req, srv) {
|
|
@@ -7384,7 +8901,7 @@ async function startServer() {
|
|
|
7384
8901
|
},
|
|
7385
8902
|
websocket: terminalWebSocket
|
|
7386
8903
|
});
|
|
7387
|
-
console.log(`runcastle server listening on http://localhost:${config2.serverPort}`);
|
|
8904
|
+
console.log(`runcastle${process.env.RUNCASTLE_DEV ? " [dev]" : ""} server listening on ` + `http://localhost:${config2.serverPort} \u2014 data dir: ${dataDir()}`);
|
|
7388
8905
|
const SHUTDOWN_KEY = Symbol.for("runcastle.shutdown.wired");
|
|
7389
8906
|
const g = globalThis;
|
|
7390
8907
|
if (!g[SHUTDOWN_KEY]) {
|
|
@@ -7404,6 +8921,7 @@ var init_src2 = __esm(() => {
|
|
|
7404
8921
|
init_client();
|
|
7405
8922
|
init_migrate();
|
|
7406
8923
|
init_reconcile();
|
|
8924
|
+
init_prep();
|
|
7407
8925
|
init_server();
|
|
7408
8926
|
init_registry();
|
|
7409
8927
|
init_ws();
|
|
@@ -7468,7 +8986,7 @@ __export(exports_cli, {
|
|
|
7468
8986
|
resolveDoctorEnv: () => resolveDoctorEnv,
|
|
7469
8987
|
parseMode: () => parseMode
|
|
7470
8988
|
});
|
|
7471
|
-
import { existsSync as
|
|
8989
|
+
import { existsSync as existsSync19, readFileSync as readFileSync10 } from "fs";
|
|
7472
8990
|
function parseMode(argv) {
|
|
7473
8991
|
return argv.includes("--gate") || argv.includes("--boot") ? "gate" : "diagnostic";
|
|
7474
8992
|
}
|
|
@@ -7476,8 +8994,8 @@ function envWithToken() {
|
|
|
7476
8994
|
const merged = { ...process.env };
|
|
7477
8995
|
try {
|
|
7478
8996
|
const path = envPath();
|
|
7479
|
-
if (
|
|
7480
|
-
const fromFile = parseEnvFile(
|
|
8997
|
+
if (existsSync19(path)) {
|
|
8998
|
+
const fromFile = parseEnvFile(readFileSync10(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
|
|
7481
8999
|
if (fromFile && fromFile.length > 0)
|
|
7482
9000
|
merged.CLAUDE_CODE_OAUTH_TOKEN = fromFile;
|
|
7483
9001
|
}
|
|
@@ -7519,8 +9037,8 @@ var init_cli = __esm(() => {
|
|
|
7519
9037
|
// src/bin/runcastle.ts
|
|
7520
9038
|
init_asset_paths();
|
|
7521
9039
|
init_version();
|
|
7522
|
-
import { dirname as
|
|
7523
|
-
import { fileURLToPath as
|
|
9040
|
+
import { dirname as dirname11, resolve as resolve10 } from "path";
|
|
9041
|
+
import { fileURLToPath as fileURLToPath9 } from "url";
|
|
7524
9042
|
function parseCommand(argv) {
|
|
7525
9043
|
const [first, ...rest] = argv;
|
|
7526
9044
|
if (first === undefined)
|
|
@@ -7544,7 +9062,7 @@ Usage:
|
|
|
7544
9062
|
runcastle --help Show this help
|
|
7545
9063
|
`;
|
|
7546
9064
|
async function main(argv) {
|
|
7547
|
-
applyInstalledAssetEnv(
|
|
9065
|
+
applyInstalledAssetEnv(resolve10(dirname11(fileURLToPath9(import.meta.url)), ".."));
|
|
7548
9066
|
const { command, args } = parseCommand(argv);
|
|
7549
9067
|
switch (command) {
|
|
7550
9068
|
case "version":
|