runcastle 1.2.3 → 1.2.5
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 +832 -368
- package/drizzle/0019_freezing_natasha_romanoff.sql +2 -0
- package/drizzle/0020_petite_ares.sql +2 -0
- package/drizzle/0021_tearful_skrulls.sql +2 -0
- package/drizzle/0022_yielding_iceman.sql +1 -0
- package/drizzle/meta/0019_snapshot.json +860 -0
- package/drizzle/meta/0020_snapshot.json +877 -0
- package/drizzle/meta/0021_snapshot.json +891 -0
- package/drizzle/meta/0022_snapshot.json +898 -0
- package/drizzle/meta/_journal.json +29 -1
- package/index.js +827 -363
- package/package.json +2 -2
- package/pty-host.cjs +10 -0
- package/skills/burner/implement-ticket.md +8 -1
- package/skills/burner/research-waypoint.md +2 -2
- package/skills/packs/README.md +10 -3
- package/skills/packs/runcastle/.claude-plugin/plugin.json +1 -1
- package/skills/packs/runcastle/skills/ideate/SKILL.md +1 -0
- package/skills/packs/runcastle/skills/project/SKILL.md +5 -3
- package/skills/packs/runcastle/skills/revisit/SKILL.md +1 -1
- package/skills/packs/runcastle/skills/spec/SKILL.md +4 -0
- package/skills/packs/runcastle/skills/tickets/SKILL.md +3 -4
- package/web/assets/{index-BhOja4Fc.js → index-D-c432xb.js} +42 -42
- package/web/assets/index-D92MdVG6.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-o2brqt2b.css +0 -1
package/index.js
CHANGED
|
@@ -25,6 +25,7 @@ __export(exports_paths, {
|
|
|
25
25
|
projectWorktreesDir: () => projectWorktreesDir,
|
|
26
26
|
prodDataDir: () => prodDataDir,
|
|
27
27
|
logsDir: () => logsDir,
|
|
28
|
+
installIdPath: () => installIdPath,
|
|
28
29
|
featureDocsRel: () => featureDocsRel,
|
|
29
30
|
envPath: () => envPath,
|
|
30
31
|
devDataDir: () => devDataDir,
|
|
@@ -63,6 +64,9 @@ function configPath() {
|
|
|
63
64
|
function envPath() {
|
|
64
65
|
return join(dataDir(), ".env");
|
|
65
66
|
}
|
|
67
|
+
function installIdPath() {
|
|
68
|
+
return join(dataDir(), "install-id");
|
|
69
|
+
}
|
|
66
70
|
function logsDir() {
|
|
67
71
|
return join(dataDir(), "logs");
|
|
68
72
|
}
|
|
@@ -174,8 +178,11 @@ function loadConfig(env = process.env) {
|
|
|
174
178
|
}
|
|
175
179
|
}
|
|
176
180
|
const overrides = {};
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
const num = (key, raw) => {
|
|
182
|
+
if (raw !== undefined && raw !== "")
|
|
183
|
+
overrides[key] = Number(raw);
|
|
184
|
+
};
|
|
185
|
+
num("serverPort", env.RUNCASTLE_SERVER_PORT);
|
|
179
186
|
if (env.RUNCASTLE_MODEL)
|
|
180
187
|
overrides.model = env.RUNCASTLE_MODEL;
|
|
181
188
|
if (env.RUNCASTLE_SMOKE_MODEL)
|
|
@@ -188,21 +195,12 @@ function loadConfig(env = process.env) {
|
|
|
188
195
|
overrides.mainBranch = env.RUNCASTLE_MAIN_BRANCH;
|
|
189
196
|
if (env.RUNCASTLE_SANDBOX_IMAGE)
|
|
190
197
|
overrides.sandboxImage = env.RUNCASTLE_SANDBOX_IMAGE;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (env.RUNCASTLE_BURN_ATTEMPTS) {
|
|
198
|
-
overrides.burnAttempts = Number(env.RUNCASTLE_BURN_ATTEMPTS);
|
|
199
|
-
}
|
|
200
|
-
if (env.RUNCASTLE_BURN_CONFLICT_ATTEMPTS !== undefined) {
|
|
201
|
-
overrides.burnConflictAttempts = Number(env.RUNCASTLE_BURN_CONFLICT_ATTEMPTS);
|
|
202
|
-
}
|
|
203
|
-
if (env.RUNCASTLE_BURN_CPUS)
|
|
204
|
-
overrides.burnCpus = Number(env.RUNCASTLE_BURN_CPUS);
|
|
205
|
-
if (env.RUNCASTLE_BURN_GUARD !== undefined) {
|
|
198
|
+
num("burnConcurrency", env.RUNCASTLE_BURN_CONCURRENCY);
|
|
199
|
+
num("burnMaxIterations", env.RUNCASTLE_BURN_MAX_ITERATIONS);
|
|
200
|
+
num("burnAttempts", env.RUNCASTLE_BURN_ATTEMPTS);
|
|
201
|
+
num("burnConflictAttempts", env.RUNCASTLE_BURN_CONFLICT_ATTEMPTS);
|
|
202
|
+
num("burnCpus", env.RUNCASTLE_BURN_CPUS);
|
|
203
|
+
if (env.RUNCASTLE_BURN_GUARD !== undefined && env.RUNCASTLE_BURN_GUARD !== "") {
|
|
206
204
|
overrides.burnGuard = env.RUNCASTLE_BURN_GUARD !== "0" && env.RUNCASTLE_BURN_GUARD !== "false";
|
|
207
205
|
}
|
|
208
206
|
if (env.RUNCASTLE_SETUP_COMMAND)
|
|
@@ -231,6 +229,7 @@ function ensureDataDir() {
|
|
|
231
229
|
mkdirSync(join2(root, "sessions"), { recursive: true });
|
|
232
230
|
mkdirSync(join2(root, "worktrees"), { recursive: true });
|
|
233
231
|
}
|
|
232
|
+
var SERVE_HOSTNAME = "127.0.0.1", SERVE_IDLE_TIMEOUT_SECONDS = 120;
|
|
234
233
|
var init_config2 = __esm(() => {
|
|
235
234
|
init_config_load();
|
|
236
235
|
init_paths();
|
|
@@ -245,7 +244,10 @@ var init_ids = () => {};
|
|
|
245
244
|
|
|
246
245
|
// ../core/src/schemas.ts
|
|
247
246
|
import * as z2 from "zod";
|
|
248
|
-
|
|
247
|
+
function isProjectSessionKind(kind) {
|
|
248
|
+
return PROJECT_SESSION_KINDS.includes(kind);
|
|
249
|
+
}
|
|
250
|
+
var Phase, TicketStatus, SessionKind, PROJECT_SESSION_KINDS, SessionPurpose, MergeBranchPair, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNote, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
249
251
|
var init_schemas = __esm(() => {
|
|
250
252
|
Phase = z2.enum([
|
|
251
253
|
"ideation",
|
|
@@ -265,8 +267,14 @@ var init_schemas = __esm(() => {
|
|
|
265
267
|
"prepare",
|
|
266
268
|
"project"
|
|
267
269
|
]);
|
|
270
|
+
PROJECT_SESSION_KINDS = ["prepare", "project"];
|
|
271
|
+
SessionPurpose = z2.enum(["resolve-conflict"]);
|
|
272
|
+
MergeBranchPair = z2.object({
|
|
273
|
+
mergeFrom: z2.string(),
|
|
274
|
+
mergeInto: z2.string()
|
|
275
|
+
});
|
|
268
276
|
RunStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
|
|
269
|
-
FeatureStatus = z2.enum(["active", "shipped", "archived"]);
|
|
277
|
+
FeatureStatus = z2.enum(["draft", "active", "shipped", "archived"]);
|
|
270
278
|
SessionStatus = z2.enum(["launching", "live", "ended"]);
|
|
271
279
|
TicketInput = z2.object({
|
|
272
280
|
title: z2.string(),
|
|
@@ -285,7 +293,8 @@ var init_schemas = __esm(() => {
|
|
|
285
293
|
commits: z2.array(z2.string()),
|
|
286
294
|
error: z2.string().optional(),
|
|
287
295
|
attemptBranch: z2.string().optional(),
|
|
288
|
-
conflictFiles: z2.array(z2.string()).optional()
|
|
296
|
+
conflictFiles: z2.array(z2.string()).optional(),
|
|
297
|
+
digest: z2.string().optional()
|
|
289
298
|
});
|
|
290
299
|
WaypointType = z2.enum(["grilling", "research", "prototype", "task"]);
|
|
291
300
|
WaypointStatus = z2.enum(["open", "claimed", "resolved", "dropped"]);
|
|
@@ -350,7 +359,8 @@ var init_schemas = __esm(() => {
|
|
|
350
359
|
dbResetCommand: z2.string().optional(),
|
|
351
360
|
driveSetupCommand: z2.string().optional(),
|
|
352
361
|
driveStopCommand: z2.string().optional(),
|
|
353
|
-
driveEnv: z2.string().optional()
|
|
362
|
+
driveEnv: z2.string().optional(),
|
|
363
|
+
closedAt: z2.number().optional()
|
|
354
364
|
});
|
|
355
365
|
ProjectFinding = z2.object({
|
|
356
366
|
key: PreparedKey,
|
|
@@ -368,6 +378,7 @@ var init_schemas = __esm(() => {
|
|
|
368
378
|
slug: z2.string(),
|
|
369
379
|
title: z2.string(),
|
|
370
380
|
oneLiner: z2.string(),
|
|
381
|
+
brief: z2.string().optional(),
|
|
371
382
|
mapped: z2.boolean(),
|
|
372
383
|
lap: z2.number(),
|
|
373
384
|
phase: Phase,
|
|
@@ -381,6 +392,8 @@ var init_schemas = __esm(() => {
|
|
|
381
392
|
featureId: z2.string().optional(),
|
|
382
393
|
projectId: z2.string().optional(),
|
|
383
394
|
kind: SessionKind,
|
|
395
|
+
purpose: SessionPurpose.optional(),
|
|
396
|
+
purposeData: MergeBranchPair.optional(),
|
|
384
397
|
ccSessionId: z2.string().optional(),
|
|
385
398
|
transcriptPath: z2.string().optional(),
|
|
386
399
|
status: SessionStatus,
|
|
@@ -394,7 +407,8 @@ var init_schemas = __esm(() => {
|
|
|
394
407
|
status: RunStatus,
|
|
395
408
|
startedAt: z2.number(),
|
|
396
409
|
endedAt: z2.number().optional(),
|
|
397
|
-
summary: z2.string().optional()
|
|
410
|
+
summary: z2.string().optional(),
|
|
411
|
+
digest: z2.string().optional()
|
|
398
412
|
});
|
|
399
413
|
SettingSource = z2.enum(["env", "project", "file", "default"]);
|
|
400
414
|
SettingScope = z2.enum(["global", "project"]);
|
|
@@ -569,7 +583,7 @@ var init_pipeline = __esm(() => {
|
|
|
569
583
|
});
|
|
570
584
|
|
|
571
585
|
// ../core/src/db-schema.ts
|
|
572
|
-
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
586
|
+
import { index, integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
573
587
|
var projects, projectFindings, features, sessions, tickets, testNotes, waypoints, runs, events, gateOverrides;
|
|
574
588
|
var init_db_schema = __esm(() => {
|
|
575
589
|
projects = sqliteTable("projects", {
|
|
@@ -605,6 +619,7 @@ var init_db_schema = __esm(() => {
|
|
|
605
619
|
slug: text("slug").notNull(),
|
|
606
620
|
title: text("title").notNull(),
|
|
607
621
|
oneLiner: text("one_liner").notNull(),
|
|
622
|
+
brief: text("brief"),
|
|
608
623
|
mapped: integer("mapped", { mode: "boolean" }).notNull().default(false),
|
|
609
624
|
lap: integer("lap").notNull().default(1),
|
|
610
625
|
phase: text("phase").notNull().$type(),
|
|
@@ -619,6 +634,8 @@ var init_db_schema = __esm(() => {
|
|
|
619
634
|
projectId: text("project_id"),
|
|
620
635
|
lap: integer("lap").notNull().default(1),
|
|
621
636
|
kind: text("kind").notNull().$type(),
|
|
637
|
+
purpose: text("purpose").$type(),
|
|
638
|
+
purposeData: text("purpose_data", { mode: "json" }).$type(),
|
|
622
639
|
ccSessionId: text("cc_session_id"),
|
|
623
640
|
transcriptPath: text("transcript_path"),
|
|
624
641
|
status: text("status").notNull().$type(),
|
|
@@ -640,7 +657,8 @@ var init_db_schema = __esm(() => {
|
|
|
640
657
|
commits: text("commits", { mode: "json" }).notNull().$type(),
|
|
641
658
|
error: text("error"),
|
|
642
659
|
attemptBranch: text("attempt_branch"),
|
|
643
|
-
conflictFiles: text("conflict_files", { mode: "json" }).$type()
|
|
660
|
+
conflictFiles: text("conflict_files", { mode: "json" }).$type(),
|
|
661
|
+
digest: text("digest")
|
|
644
662
|
});
|
|
645
663
|
testNotes = sqliteTable("test_notes", {
|
|
646
664
|
id: text("id").primaryKey(),
|
|
@@ -673,7 +691,8 @@ var init_db_schema = __esm(() => {
|
|
|
673
691
|
status: text("status").notNull().$type(),
|
|
674
692
|
startedAt: integer("started_at").notNull(),
|
|
675
693
|
endedAt: integer("ended_at"),
|
|
676
|
-
summary: text("summary")
|
|
694
|
+
summary: text("summary"),
|
|
695
|
+
digest: text("digest")
|
|
677
696
|
});
|
|
678
697
|
events = sqliteTable("events", {
|
|
679
698
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
@@ -686,7 +705,10 @@ var init_db_schema = __esm(() => {
|
|
|
686
705
|
type: text("type").notNull(),
|
|
687
706
|
message: text("message").notNull(),
|
|
688
707
|
data: text("data", { mode: "json" })
|
|
689
|
-
})
|
|
708
|
+
}, (t) => [
|
|
709
|
+
index("events_project_id_id_idx").on(t.projectId, t.id),
|
|
710
|
+
index("events_feature_id_id_idx").on(t.featureId, t.id)
|
|
711
|
+
]);
|
|
690
712
|
gateOverrides = sqliteTable("gate_overrides", {
|
|
691
713
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
692
714
|
featureId: text("feature_id").notNull(),
|
|
@@ -1266,6 +1288,16 @@ var init_ring_buffer = __esm(() => {
|
|
|
1266
1288
|
});
|
|
1267
1289
|
|
|
1268
1290
|
// src/pty/registry.ts
|
|
1291
|
+
function killTreeBounded(pty) {
|
|
1292
|
+
return new Promise((resolve3) => {
|
|
1293
|
+
const timer = setTimeout(resolve3, KILL_TREE_TIMEOUT_MS);
|
|
1294
|
+
pty.killTree().catch(() => {}).then(() => {
|
|
1295
|
+
clearTimeout(timer);
|
|
1296
|
+
resolve3();
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1269
1301
|
class PtyRegistry {
|
|
1270
1302
|
entries = new Map;
|
|
1271
1303
|
create(input) {
|
|
@@ -1323,18 +1355,31 @@ class PtyRegistry {
|
|
|
1323
1355
|
const entry = this.entries.get(sessionId);
|
|
1324
1356
|
if (!entry)
|
|
1325
1357
|
return false;
|
|
1326
|
-
|
|
1358
|
+
this.tearDown(entry);
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
async killTree(sessionId) {
|
|
1362
|
+
const entry = this.entries.get(sessionId);
|
|
1363
|
+
if (!entry)
|
|
1364
|
+
return false;
|
|
1365
|
+
await this.tearDown(entry);
|
|
1327
1366
|
return true;
|
|
1328
1367
|
}
|
|
1329
1368
|
remove(sessionId) {
|
|
1330
1369
|
this.entries.delete(sessionId);
|
|
1331
1370
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
if (!entry.exited)
|
|
1335
|
-
entry.pty.kill();
|
|
1336
|
-
}
|
|
1371
|
+
async killAllTrees() {
|
|
1372
|
+
const all = [...this.entries.values()];
|
|
1337
1373
|
this.entries.clear();
|
|
1374
|
+
await Promise.allSettled(all.map((entry) => this.tearDown(entry)));
|
|
1375
|
+
}
|
|
1376
|
+
async tearDown(entry) {
|
|
1377
|
+
if (entry.exited)
|
|
1378
|
+
return;
|
|
1379
|
+
await killTreeBounded(entry.pty);
|
|
1380
|
+
try {
|
|
1381
|
+
entry.pty.kill();
|
|
1382
|
+
} catch {}
|
|
1338
1383
|
}
|
|
1339
1384
|
ids() {
|
|
1340
1385
|
return [...this.entries.keys()];
|
|
@@ -1346,7 +1391,7 @@ function ptyRegistry() {
|
|
|
1346
1391
|
g[REGISTRY_KEY] = new PtyRegistry;
|
|
1347
1392
|
return g[REGISTRY_KEY];
|
|
1348
1393
|
}
|
|
1349
|
-
var REGISTRY_KEY;
|
|
1394
|
+
var KILL_TREE_TIMEOUT_MS = 5000, REGISTRY_KEY;
|
|
1350
1395
|
var init_registry = __esm(() => {
|
|
1351
1396
|
init_pty();
|
|
1352
1397
|
init_ring_buffer();
|
|
@@ -1427,7 +1472,7 @@ var init_bus = __esm(() => {
|
|
|
1427
1472
|
// src/services/events.ts
|
|
1428
1473
|
import { and, asc, desc, eq, gt, max } from "drizzle-orm";
|
|
1429
1474
|
function rowToEvent(row) {
|
|
1430
|
-
return {
|
|
1475
|
+
return EventRow.parse({
|
|
1431
1476
|
id: row.id,
|
|
1432
1477
|
projectId: row.projectId,
|
|
1433
1478
|
featureId: row.featureId ?? undefined,
|
|
@@ -1437,7 +1482,7 @@ function rowToEvent(row) {
|
|
|
1437
1482
|
type: row.type,
|
|
1438
1483
|
message: row.message,
|
|
1439
1484
|
data: row.data ?? undefined
|
|
1440
|
-
};
|
|
1485
|
+
});
|
|
1441
1486
|
}
|
|
1442
1487
|
function projectIdForFeature(ctx, featureId) {
|
|
1443
1488
|
const row = ctx.db.select({ projectId: features.projectId }).from(features).where(eq(features.id, featureId)).get();
|
|
@@ -1508,6 +1553,7 @@ function listByProject(ctx, projectId, afterId = 0) {
|
|
|
1508
1553
|
return rows.map(rowToEvent);
|
|
1509
1554
|
}
|
|
1510
1555
|
var init_events = __esm(() => {
|
|
1556
|
+
init_src();
|
|
1511
1557
|
init_schema();
|
|
1512
1558
|
init_errors();
|
|
1513
1559
|
init_bus();
|
|
@@ -1516,12 +1562,13 @@ var init_events = __esm(() => {
|
|
|
1516
1562
|
// src/services/repo.ts
|
|
1517
1563
|
import { and as and2, desc as desc2, eq as eq2 } from "drizzle-orm";
|
|
1518
1564
|
function rowToFeature(row) {
|
|
1519
|
-
return {
|
|
1565
|
+
return Feature.parse({
|
|
1520
1566
|
id: row.id,
|
|
1521
1567
|
projectId: row.projectId,
|
|
1522
1568
|
slug: row.slug,
|
|
1523
1569
|
title: row.title,
|
|
1524
1570
|
oneLiner: row.oneLiner,
|
|
1571
|
+
brief: row.brief ?? undefined,
|
|
1525
1572
|
mapped: row.mapped,
|
|
1526
1573
|
lap: row.lap,
|
|
1527
1574
|
phase: row.phase,
|
|
@@ -1529,10 +1576,10 @@ function rowToFeature(row) {
|
|
|
1529
1576
|
baseBranch: row.baseBranch ?? undefined,
|
|
1530
1577
|
status: row.status,
|
|
1531
1578
|
createdAt: row.createdAt
|
|
1532
|
-
};
|
|
1579
|
+
});
|
|
1533
1580
|
}
|
|
1534
1581
|
function rowToProject(row) {
|
|
1535
|
-
return {
|
|
1582
|
+
return Project.parse({
|
|
1536
1583
|
id: row.id,
|
|
1537
1584
|
name: row.name,
|
|
1538
1585
|
repoPath: row.repoPath,
|
|
@@ -1545,32 +1592,36 @@ function rowToProject(row) {
|
|
|
1545
1592
|
dbResetCommand: row.dbResetCommand ?? undefined,
|
|
1546
1593
|
driveSetupCommand: row.driveSetupCommand ?? undefined,
|
|
1547
1594
|
driveStopCommand: row.driveStopCommand ?? undefined,
|
|
1548
|
-
driveEnv: row.driveEnv ?? undefined
|
|
1549
|
-
|
|
1595
|
+
driveEnv: row.driveEnv ?? undefined,
|
|
1596
|
+
closedAt: row.closedAt ?? undefined
|
|
1597
|
+
});
|
|
1550
1598
|
}
|
|
1551
1599
|
function rowToRun(row) {
|
|
1552
|
-
return {
|
|
1600
|
+
return Run.parse({
|
|
1553
1601
|
id: row.id,
|
|
1554
1602
|
featureId: row.featureId,
|
|
1555
1603
|
workflow: row.workflow,
|
|
1556
1604
|
status: row.status,
|
|
1557
1605
|
startedAt: row.startedAt,
|
|
1558
1606
|
endedAt: row.endedAt ?? undefined,
|
|
1559
|
-
summary: row.summary ?? undefined
|
|
1560
|
-
|
|
1607
|
+
summary: row.summary ?? undefined,
|
|
1608
|
+
digest: row.digest ?? undefined
|
|
1609
|
+
});
|
|
1561
1610
|
}
|
|
1562
1611
|
function rowToSession(row) {
|
|
1563
|
-
return {
|
|
1612
|
+
return SessionRow.parse({
|
|
1564
1613
|
id: row.id,
|
|
1565
1614
|
featureId: row.featureId ?? undefined,
|
|
1566
1615
|
projectId: row.projectId ?? undefined,
|
|
1567
1616
|
kind: row.kind,
|
|
1617
|
+
purpose: row.purpose ?? undefined,
|
|
1618
|
+
purposeData: row.purposeData ?? undefined,
|
|
1568
1619
|
ccSessionId: row.ccSessionId ?? undefined,
|
|
1569
1620
|
transcriptPath: row.transcriptPath ?? undefined,
|
|
1570
1621
|
status: row.status,
|
|
1571
1622
|
awaitingInput: row.awaitingInput,
|
|
1572
1623
|
worktreePath: row.worktreePath
|
|
1573
|
-
};
|
|
1624
|
+
});
|
|
1574
1625
|
}
|
|
1575
1626
|
function tryGetFeature(ctx, id) {
|
|
1576
1627
|
const row = ctx.db.select().from(features).where(eq2(features.id, id)).get();
|
|
@@ -1646,6 +1697,7 @@ function setFeatureStatus(ctx, featureId, status) {
|
|
|
1646
1697
|
return { ...current, status };
|
|
1647
1698
|
}
|
|
1648
1699
|
var init_repo = __esm(() => {
|
|
1700
|
+
init_src();
|
|
1649
1701
|
init_schema();
|
|
1650
1702
|
init_errors();
|
|
1651
1703
|
init_events();
|
|
@@ -1654,7 +1706,7 @@ var init_repo = __esm(() => {
|
|
|
1654
1706
|
// src/services/waypoints.ts
|
|
1655
1707
|
import { and as and3, asc as asc2, eq as eq3 } from "drizzle-orm";
|
|
1656
1708
|
function rowToWaypoint(row) {
|
|
1657
|
-
return {
|
|
1709
|
+
return Waypoint.parse({
|
|
1658
1710
|
id: row.id,
|
|
1659
1711
|
featureId: row.featureId,
|
|
1660
1712
|
seq: row.seq,
|
|
@@ -1667,7 +1719,7 @@ function rowToWaypoint(row) {
|
|
|
1667
1719
|
claimedBy: row.claimedBy ?? undefined,
|
|
1668
1720
|
lastSessionId: row.lastSessionId ?? undefined,
|
|
1669
1721
|
summary: row.summary ?? undefined
|
|
1670
|
-
};
|
|
1722
|
+
});
|
|
1671
1723
|
}
|
|
1672
1724
|
function listByFeature(ctx, featureId) {
|
|
1673
1725
|
return ctx.db.select().from(waypoints).where(eq3(waypoints.featureId, featureId)).orderBy(asc2(waypoints.seq)).all().map(rowToWaypoint);
|
|
@@ -1783,6 +1835,9 @@ function releaseForSession(ctx, claimant) {
|
|
|
1783
1835
|
}
|
|
1784
1836
|
function resolve3(ctx, id, disposition, summary) {
|
|
1785
1837
|
const wp = getWaypoint(ctx, id);
|
|
1838
|
+
if (isTerminal(wp)) {
|
|
1839
|
+
throw new InvalidInputError(`waypoint ${wp.seq} is already ${wp.status}`);
|
|
1840
|
+
}
|
|
1786
1841
|
const before = listByFeature(ctx, wp.featureId);
|
|
1787
1842
|
const bySeqBefore = new Map(before.map((w) => [w.seq, w]));
|
|
1788
1843
|
ctx.db.update(waypoints).set({ status: disposition, summary, claimedBy: null }).where(eq3(waypoints.id, id)).run();
|
|
@@ -1819,6 +1874,91 @@ var init_waypoints = __esm(() => {
|
|
|
1819
1874
|
TERMINAL = new Set(["resolved", "dropped"]);
|
|
1820
1875
|
});
|
|
1821
1876
|
|
|
1877
|
+
// src/services/feature-docs.ts
|
|
1878
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1879
|
+
import { join as join5 } from "path";
|
|
1880
|
+
function featureDocsDir(project, feature) {
|
|
1881
|
+
const relSegments = featureDocsRel(feature.slug).split("/");
|
|
1882
|
+
const worktree = worktreeDir(project.id, feature.slug);
|
|
1883
|
+
const base = existsSync5(worktree) ? worktree : project.repoPath;
|
|
1884
|
+
return join5(base, ...relSegments);
|
|
1885
|
+
}
|
|
1886
|
+
function featureDocPath(project, feature, fileName) {
|
|
1887
|
+
return join5(featureDocsDir(project, feature), fileName);
|
|
1888
|
+
}
|
|
1889
|
+
var init_feature_docs = __esm(() => {
|
|
1890
|
+
init_paths();
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
// src/services/docs-watch.ts
|
|
1894
|
+
import { watch } from "fs";
|
|
1895
|
+
function startDocsWatch(ctx, feature) {
|
|
1896
|
+
if (watches.has(feature.id))
|
|
1897
|
+
return;
|
|
1898
|
+
try {
|
|
1899
|
+
const dir = featureDocsDir(projectForFeature(ctx, feature), feature);
|
|
1900
|
+
const watcher = watch(dir, { persistent: false }, (_event, filename) => {
|
|
1901
|
+
noteDocsChange(ctx, feature.id, typeof filename === "string" ? filename : null);
|
|
1902
|
+
});
|
|
1903
|
+
watcher.on("error", (err) => {
|
|
1904
|
+
console.error(`docs watcher failed for feature ${feature.id}`, err);
|
|
1905
|
+
stopDocsWatch(feature.id);
|
|
1906
|
+
});
|
|
1907
|
+
watches.set(feature.id, { watcher, timer: undefined, changed: new Set });
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
console.error(`docs watcher could not start for feature ${feature.id}`, err);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
function stopDocsWatch(featureId) {
|
|
1913
|
+
const entry = watches.get(featureId);
|
|
1914
|
+
if (!entry)
|
|
1915
|
+
return;
|
|
1916
|
+
watches.delete(featureId);
|
|
1917
|
+
if (entry.timer)
|
|
1918
|
+
clearTimeout(entry.timer);
|
|
1919
|
+
try {
|
|
1920
|
+
entry.watcher.close();
|
|
1921
|
+
} catch (err) {
|
|
1922
|
+
console.error(`docs watcher failed to close for feature ${featureId}`, err);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
function noteDocsChange(ctx, featureId, filename) {
|
|
1926
|
+
const entry = watches.get(featureId);
|
|
1927
|
+
if (!entry)
|
|
1928
|
+
return;
|
|
1929
|
+
if (filename !== null && !filename.endsWith(".md"))
|
|
1930
|
+
return;
|
|
1931
|
+
if (filename)
|
|
1932
|
+
entry.changed.add(filename);
|
|
1933
|
+
if (entry.timer)
|
|
1934
|
+
clearTimeout(entry.timer);
|
|
1935
|
+
entry.timer = setTimeout(() => emitDocsChanged(ctx, featureId), DOCS_WATCH_DEBOUNCE_MS);
|
|
1936
|
+
}
|
|
1937
|
+
function emitDocsChanged(ctx, featureId) {
|
|
1938
|
+
const entry = watches.get(featureId);
|
|
1939
|
+
if (!entry)
|
|
1940
|
+
return;
|
|
1941
|
+
entry.timer = undefined;
|
|
1942
|
+
const files = [...entry.changed].sort();
|
|
1943
|
+
entry.changed.clear();
|
|
1944
|
+
try {
|
|
1945
|
+
emit(ctx, featureId, {
|
|
1946
|
+
type: "docs.changed",
|
|
1947
|
+
message: files.length > 0 ? `docs changed: ${files.join(", ")}` : "docs changed",
|
|
1948
|
+
data: { files }
|
|
1949
|
+
});
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
console.error(`docs watcher failed to emit for feature ${featureId}`, err);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
var DOCS_WATCH_DEBOUNCE_MS = 300, watches;
|
|
1955
|
+
var init_docs_watch = __esm(() => {
|
|
1956
|
+
init_events();
|
|
1957
|
+
init_feature_docs();
|
|
1958
|
+
init_repo();
|
|
1959
|
+
watches = new Map;
|
|
1960
|
+
});
|
|
1961
|
+
|
|
1822
1962
|
// src/pty/dev-pane.ts
|
|
1823
1963
|
function drivePaneId(ownerId) {
|
|
1824
1964
|
return `${DRIVE_PREFIX}${ownerId}`;
|
|
@@ -1879,24 +2019,12 @@ function devPaneLive(paneId) {
|
|
|
1879
2019
|
const entry = ptyRegistry().get(paneId);
|
|
1880
2020
|
return !!entry && !entry.exited;
|
|
1881
2021
|
}
|
|
1882
|
-
function killTreeBounded(pty) {
|
|
1883
|
-
return new Promise((resolve4) => {
|
|
1884
|
-
const timer = setTimeout(resolve4, KILL_TREE_TIMEOUT_MS);
|
|
1885
|
-
pty.killTree().catch(() => {}).then(() => {
|
|
1886
|
-
clearTimeout(timer);
|
|
1887
|
-
resolve4();
|
|
1888
|
-
});
|
|
1889
|
-
});
|
|
1890
|
-
}
|
|
1891
2022
|
async function stopDevPane(paneId) {
|
|
1892
2023
|
const reg = ptyRegistry();
|
|
1893
|
-
|
|
1894
|
-
if (entry && !entry.exited)
|
|
1895
|
-
await killTreeBounded(entry.pty);
|
|
1896
|
-
reg.kill(paneId);
|
|
2024
|
+
await reg.killTree(paneId);
|
|
1897
2025
|
reg.remove(paneId);
|
|
1898
2026
|
}
|
|
1899
|
-
var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192
|
|
2027
|
+
var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192;
|
|
1900
2028
|
var init_dev_pane = __esm(() => {
|
|
1901
2029
|
init_events();
|
|
1902
2030
|
init_registry();
|
|
@@ -1954,7 +2082,8 @@ function runDriveHook(command, opts) {
|
|
|
1954
2082
|
cwd: opts.cwd,
|
|
1955
2083
|
env: opts.env ?? process.env,
|
|
1956
2084
|
windowsHide: true,
|
|
1957
|
-
windowsVerbatimArguments: verbatim
|
|
2085
|
+
windowsVerbatimArguments: verbatim,
|
|
2086
|
+
detached: process.platform !== "win32"
|
|
1958
2087
|
});
|
|
1959
2088
|
} catch (err) {
|
|
1960
2089
|
output = err instanceof Error ? err.message : String(err);
|
|
@@ -1963,7 +2092,11 @@ function runDriveHook(command, opts) {
|
|
|
1963
2092
|
}
|
|
1964
2093
|
timer = setTimeout(() => {
|
|
1965
2094
|
timedOut = true;
|
|
1966
|
-
child.
|
|
2095
|
+
const pid = child.pid;
|
|
2096
|
+
if (pid === undefined)
|
|
2097
|
+
child.kill();
|
|
2098
|
+
else
|
|
2099
|
+
killProcessTree(pid).then(() => child.kill());
|
|
1967
2100
|
const grace = setTimeout(() => finish(null), KILL_GRACE_MS);
|
|
1968
2101
|
grace.unref?.();
|
|
1969
2102
|
}, timeoutMs);
|
|
@@ -1994,6 +2127,7 @@ function describeHookResult(command, result) {
|
|
|
1994
2127
|
}
|
|
1995
2128
|
var DRIVE_HOOK_TIMEOUT_MS, OUTPUT_TAIL_LINES = 40, KILL_GRACE_MS = 2000;
|
|
1996
2129
|
var init_drive_hooks = __esm(() => {
|
|
2130
|
+
init_kill_tree();
|
|
1997
2131
|
DRIVE_HOOK_TIMEOUT_MS = 10 * 60000;
|
|
1998
2132
|
});
|
|
1999
2133
|
|
|
@@ -2210,9 +2344,9 @@ var init_findings = __esm(() => {
|
|
|
2210
2344
|
});
|
|
2211
2345
|
|
|
2212
2346
|
// src/services/git.ts
|
|
2213
|
-
import { existsSync as
|
|
2347
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, mkdtempSync, realpathSync, rmSync } from "fs";
|
|
2214
2348
|
import { tmpdir } from "os";
|
|
2215
|
-
import { dirname, join as
|
|
2349
|
+
import { dirname, join as join6, resolve as resolve4 } from "path";
|
|
2216
2350
|
import { simpleGit } from "simple-git";
|
|
2217
2351
|
function migrationPaths(paths) {
|
|
2218
2352
|
return paths.filter((p) => MIGRATION_DIR_RE.test(p) || DRIZZLE_SQL_RE.test(p));
|
|
@@ -2237,7 +2371,7 @@ function canon(p) {
|
|
|
2237
2371
|
return process.platform === "win32" ? abs.replace(/\\/g, "/").toLowerCase() : abs;
|
|
2238
2372
|
}
|
|
2239
2373
|
async function assertRepo(repoPath) {
|
|
2240
|
-
if (!
|
|
2374
|
+
if (!existsSync6(repoPath)) {
|
|
2241
2375
|
throw new InvalidInputError(`path does not exist: ${repoPath}`);
|
|
2242
2376
|
}
|
|
2243
2377
|
let isRepo = false;
|
|
@@ -2326,7 +2460,7 @@ async function ensureTalkWorktree(project, feature) {
|
|
|
2326
2460
|
await ensureBranchExists(g, branch, feature.baseBranch ?? project.mainBranch);
|
|
2327
2461
|
if (await worktreeIsValid(g, worktreePath, branch))
|
|
2328
2462
|
return worktreePath;
|
|
2329
|
-
if (
|
|
2463
|
+
if (existsSync6(worktreePath) && (await registeredWorktrees(g)).has(canon(worktreePath))) {
|
|
2330
2464
|
if (await checkoutInWorktree(worktreePath, branch))
|
|
2331
2465
|
return worktreePath;
|
|
2332
2466
|
}
|
|
@@ -2349,14 +2483,13 @@ async function addWorktree(g, worktreePath, branch, label) {
|
|
|
2349
2483
|
}
|
|
2350
2484
|
}
|
|
2351
2485
|
}
|
|
2352
|
-
async function ensureProjectWorktree(project) {
|
|
2486
|
+
async function ensureProjectWorktree(project, onLanded) {
|
|
2353
2487
|
const worktreePath = worktreeDir(project.id, PROJECT_WORKTREE_SLUG);
|
|
2354
2488
|
const g = git(project.repoPath);
|
|
2355
2489
|
const base = project.mainBranch;
|
|
2356
|
-
const
|
|
2357
|
-
if (
|
|
2358
|
-
|
|
2359
|
-
}
|
|
2490
|
+
const landed = await landProjectBranch(project);
|
|
2491
|
+
if (landed)
|
|
2492
|
+
onLanded?.(landed);
|
|
2360
2493
|
const stillAhead = await branchCommitsAhead(project.repoPath, base, PROJECT_BRANCH);
|
|
2361
2494
|
if (stillAhead.length === 0) {
|
|
2362
2495
|
await deleteBranchDetachingWorktrees(g, project.repoPath, PROJECT_BRANCH);
|
|
@@ -2364,7 +2497,7 @@ async function ensureProjectWorktree(project) {
|
|
|
2364
2497
|
}
|
|
2365
2498
|
if (await worktreeIsValid(g, worktreePath, PROJECT_BRANCH))
|
|
2366
2499
|
return worktreePath;
|
|
2367
|
-
if (
|
|
2500
|
+
if (existsSync6(worktreePath) && (await registeredWorktrees(g)).has(canon(worktreePath))) {
|
|
2368
2501
|
if (await checkoutInWorktree(worktreePath, PROJECT_BRANCH))
|
|
2369
2502
|
return worktreePath;
|
|
2370
2503
|
}
|
|
@@ -2390,7 +2523,7 @@ async function ensureBranchExists(g, branch, from) {
|
|
|
2390
2523
|
}
|
|
2391
2524
|
}
|
|
2392
2525
|
async function worktreeIsValid(g, worktreePath, branch) {
|
|
2393
|
-
if (!
|
|
2526
|
+
if (!existsSync6(worktreePath))
|
|
2394
2527
|
return false;
|
|
2395
2528
|
const registered = await registeredWorktrees(g);
|
|
2396
2529
|
if (!registered.has(canon(worktreePath)))
|
|
@@ -2436,7 +2569,7 @@ async function worktreesOnBranch(g, branch, mainRepoPath) {
|
|
|
2436
2569
|
return (await listWorktrees(g)).filter((e) => e.branch === branch && canon(e.path) !== main).map((e) => e.path);
|
|
2437
2570
|
}
|
|
2438
2571
|
async function detachWorktree(path) {
|
|
2439
|
-
if (!
|
|
2572
|
+
if (!existsSync6(path))
|
|
2440
2573
|
return false;
|
|
2441
2574
|
const g = git(path);
|
|
2442
2575
|
let head;
|
|
@@ -2451,7 +2584,7 @@ async function detachWorktree(path) {
|
|
|
2451
2584
|
return true;
|
|
2452
2585
|
}
|
|
2453
2586
|
async function reattachWorktree(path, branch) {
|
|
2454
|
-
if (!
|
|
2587
|
+
if (!existsSync6(path))
|
|
2455
2588
|
return;
|
|
2456
2589
|
try {
|
|
2457
2590
|
await git(path).checkout(branch);
|
|
@@ -2467,21 +2600,21 @@ function ticketBranchName(slug, ticketSeq, unique) {
|
|
|
2467
2600
|
return `${TICKET_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${ticketSeq}-${unique}`;
|
|
2468
2601
|
}
|
|
2469
2602
|
function burnWorktreePath(repoPath, branch) {
|
|
2470
|
-
return
|
|
2603
|
+
return join6(repoPath, ".sandcastle", "worktrees", branch.replace(/\//g, "-"));
|
|
2471
2604
|
}
|
|
2472
2605
|
async function cleanupBurnWorktree(repoPath, branch, opts = {}) {
|
|
2473
2606
|
const path = burnWorktreePath(repoPath, branch);
|
|
2474
2607
|
const attempts = Math.max(1, opts.attempts ?? 3);
|
|
2475
2608
|
const delayMs = opts.delayMs ?? 750;
|
|
2476
2609
|
const g = git(repoPath);
|
|
2477
|
-
for (let attempt = 1;attempt <= attempts &&
|
|
2610
|
+
for (let attempt = 1;attempt <= attempts && existsSync6(path); attempt++) {
|
|
2478
2611
|
try {
|
|
2479
2612
|
await g.raw(["worktree", "remove", "--force", path]);
|
|
2480
2613
|
} catch {}
|
|
2481
|
-
if (
|
|
2614
|
+
if (existsSync6(path) && attempt < attempts)
|
|
2482
2615
|
await sleep(delayMs);
|
|
2483
2616
|
}
|
|
2484
|
-
if (
|
|
2617
|
+
if (existsSync6(path)) {
|
|
2485
2618
|
try {
|
|
2486
2619
|
rmSync(path, { recursive: true, force: true });
|
|
2487
2620
|
} catch {}
|
|
@@ -2489,7 +2622,7 @@ async function cleanupBurnWorktree(repoPath, branch, opts = {}) {
|
|
|
2489
2622
|
try {
|
|
2490
2623
|
await g.raw(["worktree", "prune"]);
|
|
2491
2624
|
} catch {}
|
|
2492
|
-
return !
|
|
2625
|
+
return !existsSync6(path);
|
|
2493
2626
|
}
|
|
2494
2627
|
function sleep(ms) {
|
|
2495
2628
|
return new Promise((done) => setTimeout(done, ms));
|
|
@@ -2637,11 +2770,11 @@ async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
|
|
|
2637
2770
|
async function mergeInDisposableWorktree(g, featureBranchName, tempBranch) {
|
|
2638
2771
|
let dir;
|
|
2639
2772
|
try {
|
|
2640
|
-
dir = mkdtempSync(
|
|
2773
|
+
dir = mkdtempSync(join6(tmpdir(), "rc-land-"));
|
|
2641
2774
|
} catch (e) {
|
|
2642
2775
|
return { ok: false, error: errMsg(e) };
|
|
2643
2776
|
}
|
|
2644
|
-
const wt =
|
|
2777
|
+
const wt = join6(dir, "wt");
|
|
2645
2778
|
try {
|
|
2646
2779
|
await g.raw(["worktree", "add", wt, featureBranchName]);
|
|
2647
2780
|
const gw = git(wt);
|
|
@@ -2748,7 +2881,7 @@ async function removeTalkWorktree(repoPath, worktreePath) {
|
|
|
2748
2881
|
try {
|
|
2749
2882
|
await g.raw(["worktree", "prune"]);
|
|
2750
2883
|
} catch {}
|
|
2751
|
-
if (
|
|
2884
|
+
if (existsSync6(worktreePath)) {
|
|
2752
2885
|
throw new InvalidInputError(`could not remove talk worktree at ${worktreePath} \u2014 a file may be locked; ` + "close anything using it and retry the delete");
|
|
2753
2886
|
}
|
|
2754
2887
|
}
|
|
@@ -2775,7 +2908,7 @@ async function deleteFeatureBranches(repoPath, slug) {
|
|
|
2775
2908
|
return { deleted, kept };
|
|
2776
2909
|
}
|
|
2777
2910
|
async function commitDocs(worktreePath, message) {
|
|
2778
|
-
if (!
|
|
2911
|
+
if (!existsSync6(resolve4(worktreePath, DOCS_PATHSPEC)))
|
|
2779
2912
|
return;
|
|
2780
2913
|
const g = git(worktreePath);
|
|
2781
2914
|
await g.add([DOCS_PATHSPEC]);
|
|
@@ -3180,10 +3313,24 @@ async function conflictedFiles(g) {
|
|
|
3180
3313
|
return [];
|
|
3181
3314
|
}
|
|
3182
3315
|
}
|
|
3316
|
+
async function mergeInProgressAt(worktreePath) {
|
|
3317
|
+
try {
|
|
3318
|
+
return await mergeInProgress(git(worktreePath));
|
|
3319
|
+
} catch {
|
|
3320
|
+
return false;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
async function isAncestor(repoPath, ancestor, descendant) {
|
|
3324
|
+
try {
|
|
3325
|
+
const out = await git(repoPath).raw(["rev-list", "--count", ancestor, `^${descendant}`]);
|
|
3326
|
+
return out.trim() === "0";
|
|
3327
|
+
} catch {
|
|
3328
|
+
return false;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3183
3331
|
async function mergeInProgress(g) {
|
|
3184
3332
|
try {
|
|
3185
|
-
await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"]);
|
|
3186
|
-
return true;
|
|
3333
|
+
return (await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"])).trim() !== "";
|
|
3187
3334
|
} catch {
|
|
3188
3335
|
return false;
|
|
3189
3336
|
}
|
|
@@ -3213,6 +3360,8 @@ function createSessionRow(ctx, input) {
|
|
|
3213
3360
|
projectId: input.projectId ?? null,
|
|
3214
3361
|
lap: input.featureId ? getFeatureRow(ctx, input.featureId).lap : 1,
|
|
3215
3362
|
kind: input.kind,
|
|
3363
|
+
purpose: input.purpose ?? null,
|
|
3364
|
+
purposeData: input.purposeData ?? null,
|
|
3216
3365
|
ccSessionId: null,
|
|
3217
3366
|
transcriptPath: null,
|
|
3218
3367
|
status: "launching",
|
|
@@ -3452,35 +3601,46 @@ function landProjectSession(ctx, session) {
|
|
|
3452
3601
|
const project = getProjectById(ctx, session.projectId);
|
|
3453
3602
|
if (!project)
|
|
3454
3603
|
return;
|
|
3455
|
-
const kept = `they are kept on ${PROJECT_BRANCH} and retried at the next launch`;
|
|
3456
|
-
const report = (type, message, data = {}) => {
|
|
3457
|
-
emitProject(ctx, project.id, {
|
|
3458
|
-
type,
|
|
3459
|
-
message,
|
|
3460
|
-
data: { sessionId: session.id, branch: PROJECT_BRANCH, ...data }
|
|
3461
|
-
});
|
|
3462
|
-
};
|
|
3463
3604
|
const landing = landProjectBranch(project).then((res) => {
|
|
3464
|
-
if (
|
|
3465
|
-
|
|
3466
|
-
if (res.landed) {
|
|
3467
|
-
report("project.landed", `landed ${res.commits} commit(s) from the project session onto ${project.mainBranch}`, { commits: res.commits });
|
|
3468
|
-
} else if (res.conflict) {
|
|
3469
|
-
report("project.land_conflict", `the project session's ${res.commits} commit(s) conflict with ${project.mainBranch} \u2014 nothing was overwritten; ${kept}`, { commits: res.commits, files: res.files ?? [] });
|
|
3470
|
-
} else {
|
|
3471
|
-
report("project.land_failed", `could not land the project session's ${res.commits} commit(s) onto ${project.mainBranch}: ${res.error ?? "unknown error"} \u2014 ${kept}`, { commits: res.commits, error: res.error ?? null });
|
|
3472
|
-
}
|
|
3605
|
+
if (res)
|
|
3606
|
+
reportProjectLanding(ctx, project, res, { sessionId: session.id });
|
|
3473
3607
|
}).catch((e) => {
|
|
3474
|
-
|
|
3608
|
+
emitProject(ctx, project.id, {
|
|
3609
|
+
type: "project.land_failed",
|
|
3610
|
+
message: `could not land the project session's work onto ${project.mainBranch}: ${e instanceof Error ? e.message : String(e)} \u2014 ${LANDING_KEPT}`,
|
|
3611
|
+
data: {
|
|
3612
|
+
sessionId: session.id,
|
|
3613
|
+
branch: PROJECT_BRANCH,
|
|
3614
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3615
|
+
}
|
|
3616
|
+
});
|
|
3475
3617
|
});
|
|
3476
3618
|
inFlightLandings.add(landing);
|
|
3477
3619
|
landing.finally(() => inFlightLandings.delete(landing));
|
|
3478
3620
|
}
|
|
3621
|
+
function reportProjectLanding(ctx, project, res, data = {}) {
|
|
3622
|
+
const report = (type, message, extra) => {
|
|
3623
|
+
emitProject(ctx, project.id, {
|
|
3624
|
+
type,
|
|
3625
|
+
message,
|
|
3626
|
+
data: { branch: PROJECT_BRANCH, ...data, ...extra }
|
|
3627
|
+
});
|
|
3628
|
+
};
|
|
3629
|
+
if (res.landed) {
|
|
3630
|
+
report("project.landed", `landed ${res.commits} commit(s) from the project session onto ${project.mainBranch}`, { commits: res.commits });
|
|
3631
|
+
} else if (res.conflict) {
|
|
3632
|
+
report("project.land_conflict", `the project session's ${res.commits} commit(s) conflict with ${project.mainBranch} \u2014 nothing was overwritten; ${LANDING_KEPT}`, { commits: res.commits, files: res.files ?? [] });
|
|
3633
|
+
} else {
|
|
3634
|
+
report("project.land_failed", `could not land the project session's ${res.commits} commit(s) onto ${project.mainBranch}: ${res.error ?? "unknown error"} \u2014 ${LANDING_KEPT}`, { commits: res.commits, error: res.error ?? null });
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3479
3637
|
function markSessionEnded(ctx, id) {
|
|
3480
3638
|
const existing = getSessionRow(ctx, id);
|
|
3481
3639
|
if (!existing)
|
|
3482
3640
|
return null;
|
|
3483
3641
|
forgetKickoff(id);
|
|
3642
|
+
if (existing.featureId)
|
|
3643
|
+
stopDocsWatch(existing.featureId);
|
|
3484
3644
|
ctx.db.update(sessions).set({ status: "ended" }).where(eq5(sessions.id, id)).run();
|
|
3485
3645
|
return getSessionRow(ctx, id);
|
|
3486
3646
|
}
|
|
@@ -3503,11 +3663,12 @@ function mostRecentLiveSession(ctx) {
|
|
|
3503
3663
|
const row = ctx.db.select().from(sessions).where(eq5(sessions.status, "live")).orderBy(desc3(sql2`rowid`)).limit(1).get();
|
|
3504
3664
|
return row ? rowToSession(row) : null;
|
|
3505
3665
|
}
|
|
3506
|
-
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, landedProjectSessions, inFlightLandings;
|
|
3666
|
+
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, landedProjectSessions, inFlightLandings, LANDING_KEPT;
|
|
3507
3667
|
var init_sessions = __esm(() => {
|
|
3508
3668
|
init_src();
|
|
3509
3669
|
init_schema();
|
|
3510
3670
|
init_registry();
|
|
3671
|
+
init_docs_watch();
|
|
3511
3672
|
init_events();
|
|
3512
3673
|
init_git();
|
|
3513
3674
|
init_waypoints();
|
|
@@ -3527,6 +3688,7 @@ var init_sessions = __esm(() => {
|
|
|
3527
3688
|
deliveries = new Map;
|
|
3528
3689
|
landedProjectSessions = new Set;
|
|
3529
3690
|
inFlightLandings = new Set;
|
|
3691
|
+
LANDING_KEPT = `they are kept on ${PROJECT_BRANCH} and retried at the next launch`;
|
|
3530
3692
|
});
|
|
3531
3693
|
|
|
3532
3694
|
// src/launcher/reconcile.ts
|
|
@@ -3580,26 +3742,10 @@ async function getRuntimeCtx() {
|
|
|
3580
3742
|
}
|
|
3581
3743
|
var current = null;
|
|
3582
3744
|
|
|
3583
|
-
// src/services/feature-docs.ts
|
|
3584
|
-
import { existsSync as existsSync6 } from "fs";
|
|
3585
|
-
import { join as join6 } from "path";
|
|
3586
|
-
function featureDocsDir(project, feature) {
|
|
3587
|
-
const relSegments = featureDocsRel(feature.slug).split("/");
|
|
3588
|
-
const worktree = worktreeDir(project.id, feature.slug);
|
|
3589
|
-
const base = existsSync6(worktree) ? worktree : project.repoPath;
|
|
3590
|
-
return join6(base, ...relSegments);
|
|
3591
|
-
}
|
|
3592
|
-
function featureDocPath(project, feature, fileName) {
|
|
3593
|
-
return join6(featureDocsDir(project, feature), fileName);
|
|
3594
|
-
}
|
|
3595
|
-
var init_feature_docs = __esm(() => {
|
|
3596
|
-
init_paths();
|
|
3597
|
-
});
|
|
3598
|
-
|
|
3599
3745
|
// src/services/tickets.ts
|
|
3600
3746
|
import { and as and6, asc as asc3, eq as eq6 } from "drizzle-orm";
|
|
3601
3747
|
function rowToTicket(row) {
|
|
3602
|
-
return {
|
|
3748
|
+
return Ticket.parse({
|
|
3603
3749
|
id: row.id,
|
|
3604
3750
|
featureId: row.featureId,
|
|
3605
3751
|
seq: row.seq,
|
|
@@ -3614,8 +3760,9 @@ function rowToTicket(row) {
|
|
|
3614
3760
|
commits: row.commits,
|
|
3615
3761
|
error: row.error ?? undefined,
|
|
3616
3762
|
attemptBranch: row.attemptBranch ?? undefined,
|
|
3617
|
-
conflictFiles: row.conflictFiles ?? undefined
|
|
3618
|
-
|
|
3763
|
+
conflictFiles: row.conflictFiles ?? undefined,
|
|
3764
|
+
digest: row.digest ?? undefined
|
|
3765
|
+
});
|
|
3619
3766
|
}
|
|
3620
3767
|
function resolveBlocking(inputs, startSeq) {
|
|
3621
3768
|
try {
|
|
@@ -3651,7 +3798,8 @@ function storeTickets(ctx, featureId, inputs) {
|
|
|
3651
3798
|
commits: [],
|
|
3652
3799
|
error: null,
|
|
3653
3800
|
attemptBranch: null,
|
|
3654
|
-
conflictFiles: null
|
|
3801
|
+
conflictFiles: null,
|
|
3802
|
+
digest: null
|
|
3655
3803
|
}));
|
|
3656
3804
|
ctx.db.insert(tickets).values(rows).run();
|
|
3657
3805
|
emit(ctx, featureId, {
|
|
@@ -3738,6 +3886,8 @@ function updateTicket(ctx, id, patch) {
|
|
|
3738
3886
|
set.attemptBranch = patch.attemptBranch;
|
|
3739
3887
|
if (patch.conflictFiles !== undefined)
|
|
3740
3888
|
set.conflictFiles = patch.conflictFiles;
|
|
3889
|
+
if (patch.digest !== undefined)
|
|
3890
|
+
set.digest = patch.digest;
|
|
3741
3891
|
ctx.db.update(tickets).set(set).where(eq6(tickets.id, id)).run();
|
|
3742
3892
|
emit(ctx, current2.featureId, {
|
|
3743
3893
|
type: "ticket.updated",
|
|
@@ -3823,6 +3973,13 @@ function docGate(ctx, feature, fileName, reason) {
|
|
|
3823
3973
|
}
|
|
3824
3974
|
function overrideGate(ctx, featureId, gate, reason) {
|
|
3825
3975
|
const feature = getFeatureRow(ctx, featureId);
|
|
3976
|
+
const current2 = nextGate(feature);
|
|
3977
|
+
if (!current2) {
|
|
3978
|
+
throw new InvalidInputError(`cannot override ${gate}: feature is at ${feature.phase}, its final phase \u2014 no gate to cross`);
|
|
3979
|
+
}
|
|
3980
|
+
if (current2.id !== gate) {
|
|
3981
|
+
throw new InvalidInputError(`cannot override ${gate}: the feature is at ${feature.phase}, whose gate is ${current2.id}`);
|
|
3982
|
+
}
|
|
3826
3983
|
ctx.db.insert(gateOverrides).values({ featureId, gate, reason, ts: Date.now() }).run();
|
|
3827
3984
|
emit(ctx, featureId, {
|
|
3828
3985
|
type: "gate.overridden",
|
|
@@ -4379,14 +4536,21 @@ function buildIsolatedSetupCommand(tempBranch, setupCommand, pm) {
|
|
|
4379
4536
|
}
|
|
4380
4537
|
function buildWorkspaceNotes(mode) {
|
|
4381
4538
|
if (mode === "mounted") {
|
|
4382
|
-
return
|
|
4539
|
+
return [
|
|
4540
|
+
"Work in the current directory \u2014 it is the repo checkout on your branch.",
|
|
4541
|
+
"",
|
|
4542
|
+
"Write `DIGEST.md` at the root of that checkout, and leave it uncommitted \u2014 the host harvests it from disk."
|
|
4543
|
+
].join(`
|
|
4544
|
+
`);
|
|
4383
4545
|
}
|
|
4384
4546
|
return [
|
|
4385
4547
|
`Your working repository is \`${ISOLATED_REPO_PATH}\` \u2014 a clone on the container's fast native filesystem, with dependencies already installed. Do ALL work there: \`cd ${ISOLATED_REPO_PATH}\` first; every file you read, edit, test, and commit lives under it.`,
|
|
4386
4548
|
"",
|
|
4387
4549
|
`The directory you start in (\`${SANDBOX_WORKSPACE_PATH}\`) is a slow mounted mirror used only to collect your commits \u2014 never edit files, install, or run tests there. Your commits sync back automatically (a post-commit hook pushes them); just commit as normal. If you re-run the dependency install and it reconfigures git hooks (husky), run \`git -C ${ISOLATED_REPO_PATH} config core.hooksPath ${ISOLATED_REPO_PATH}/.git/hooks\` afterwards so the sync hook stays armed.`,
|
|
4388
4550
|
"",
|
|
4389
|
-
`If you are blocked and write \`BLOCKED.md\`, write it at \`${ISOLATED_REPO_PATH}/BLOCKED.md\` AND copy it to \`${SANDBOX_WORKSPACE_PATH}/BLOCKED.md\` so the orchestrator can see it
|
|
4551
|
+
`If you are blocked and write \`BLOCKED.md\`, write it at \`${ISOLATED_REPO_PATH}/BLOCKED.md\` AND copy it to \`${SANDBOX_WORKSPACE_PATH}/BLOCKED.md\` so the orchestrator can see it.`,
|
|
4552
|
+
"",
|
|
4553
|
+
`Write \`DIGEST.md\` at \`${SANDBOX_WORKSPACE_PATH}/DIGEST.md\` \u2014 the mounted mirror, so the host can see it \u2014 and NOT inside \`${ISOLATED_REPO_PATH}\`, where it would be committed with your work.`
|
|
4390
4554
|
].join(`
|
|
4391
4555
|
`);
|
|
4392
4556
|
}
|
|
@@ -4648,6 +4812,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
4648
4812
|
const status = new Map(tickets2.map((t) => [t.seq, t.status]));
|
|
4649
4813
|
const pending = new Set(tickets2.filter((t) => t.status === "pending").map((t) => t.seq));
|
|
4650
4814
|
const inFlight = new Map;
|
|
4815
|
+
const digests = [];
|
|
4651
4816
|
const satisfied = (s) => s === "done" || s === "cancelled";
|
|
4652
4817
|
const readyState = (seq) => {
|
|
4653
4818
|
const t = bySeq.get(seq);
|
|
@@ -4684,13 +4849,22 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
4684
4849
|
const outcome = await execute(ctx, t);
|
|
4685
4850
|
if (outcome.status === "done") {
|
|
4686
4851
|
status.set(seq, "done");
|
|
4687
|
-
ctx.updateTicket(t.id, { status: "done", commits: outcome.commits });
|
|
4852
|
+
ctx.updateTicket(t.id, { status: "done", commits: outcome.commits, digest: outcome.digest });
|
|
4688
4853
|
ctx.emitEvent({
|
|
4689
4854
|
type: "ticket.done",
|
|
4690
4855
|
message: `ticket ${t.seq} done \u2014 ${outcome.commits.length} commit(s)`,
|
|
4691
4856
|
ticketId: t.id,
|
|
4692
4857
|
data: { commits: outcome.commits }
|
|
4693
4858
|
});
|
|
4859
|
+
if (outcome.digest === undefined) {
|
|
4860
|
+
ctx.emitEvent({
|
|
4861
|
+
type: "digest.missing",
|
|
4862
|
+
message: `ticket ${t.seq} done without DIGEST.md`,
|
|
4863
|
+
ticketId: t.id
|
|
4864
|
+
});
|
|
4865
|
+
} else {
|
|
4866
|
+
digests.push({ seq: t.seq, title: t.title, digest: outcome.digest });
|
|
4867
|
+
}
|
|
4694
4868
|
} else {
|
|
4695
4869
|
failTicket(seq, outcome.error, outcome.event);
|
|
4696
4870
|
ctx.emitEvent({
|
|
@@ -4756,7 +4930,16 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
4756
4930
|
for (const s of status.values())
|
|
4757
4931
|
if (s === "done")
|
|
4758
4932
|
done += 1;
|
|
4759
|
-
return done;
|
|
4933
|
+
return { done, digests };
|
|
4934
|
+
}
|
|
4935
|
+
function composeRunDigest(entries) {
|
|
4936
|
+
if (entries.length === 0)
|
|
4937
|
+
return null;
|
|
4938
|
+
return [...entries].sort((a, b) => a.seq - b.seq).map((e) => `## ticket ${e.seq} \u2014 ${e.title}
|
|
4939
|
+
|
|
4940
|
+
${e.digest.trim()}`).join(`
|
|
4941
|
+
|
|
4942
|
+
`);
|
|
4760
4943
|
}
|
|
4761
4944
|
async function burnRun(ctx, deps) {
|
|
4762
4945
|
const tickets2 = ctx.tickets;
|
|
@@ -4777,10 +4960,11 @@ async function burnRun(ctx, deps) {
|
|
|
4777
4960
|
});
|
|
4778
4961
|
return { status: "failed", summary: `dependency cycle: ${path}` };
|
|
4779
4962
|
}
|
|
4780
|
-
const done = await burnTickets(ctx, tickets2, deps.executeTicketRun, deps.concurrency);
|
|
4963
|
+
const { done, digests } = await burnTickets(ctx, tickets2, deps.executeTicketRun, deps.concurrency);
|
|
4781
4964
|
const summary = cancelled > 0 ? `${done}/${total} tickets done (${cancelled} cancelled)` : `${done}/${total} tickets done`;
|
|
4782
4965
|
ctx.emitEvent({ type: "burn.summary", message: summary, data: { done, total, cancelled } });
|
|
4783
|
-
|
|
4966
|
+
const digest = composeRunDigest(digests);
|
|
4967
|
+
return { status: done === total ? "succeeded" : "failed", summary, ...digest ? { digest } : {} };
|
|
4784
4968
|
}
|
|
4785
4969
|
function burnerTemplatePath() {
|
|
4786
4970
|
return burnerAssetPath("implement-ticket.md");
|
|
@@ -4802,11 +4986,11 @@ function readDocsDigestFromDisk(projectId, slug) {
|
|
|
4802
4986
|
const files = readdirSync3(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: readFileSync4(join9(docsDir, e.name), "utf8") }));
|
|
4803
4987
|
return buildDocsDigest(files);
|
|
4804
4988
|
}
|
|
4805
|
-
function
|
|
4989
|
+
function readAgentFile(dirs, file) {
|
|
4806
4990
|
for (const dir of dirs) {
|
|
4807
4991
|
if (!dir)
|
|
4808
4992
|
continue;
|
|
4809
|
-
const p = join9(dir,
|
|
4993
|
+
const p = join9(dir, file);
|
|
4810
4994
|
if (existsSync10(p)) {
|
|
4811
4995
|
try {
|
|
4812
4996
|
return readFileSync4(p, "utf8");
|
|
@@ -4815,10 +4999,26 @@ function readBlockedFile(dirs) {
|
|
|
4815
4999
|
}
|
|
4816
5000
|
return;
|
|
4817
5001
|
}
|
|
5002
|
+
function harvestDigest(dirs) {
|
|
5003
|
+
const trimmed = readAgentFile(dirs, "DIGEST.md")?.trim();
|
|
5004
|
+
return trimmed ? trimmed : undefined;
|
|
5005
|
+
}
|
|
5006
|
+
function buildAgentEnv(onHost, token) {
|
|
5007
|
+
const env = {};
|
|
5008
|
+
if (onHost) {
|
|
5009
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
5010
|
+
if (value !== undefined)
|
|
5011
|
+
env[key] = value;
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
if (token)
|
|
5015
|
+
env.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
5016
|
+
return env;
|
|
5017
|
+
}
|
|
4818
5018
|
function buildBurnAgent(config2, token, model) {
|
|
4819
5019
|
const onHost = config2.sandbox === "noSandbox";
|
|
4820
5020
|
const opts = {
|
|
4821
|
-
|
|
5021
|
+
env: buildAgentEnv(onHost, token),
|
|
4822
5022
|
...onHost ? { permissionMode: "bypassPermissions" } : {}
|
|
4823
5023
|
};
|
|
4824
5024
|
const agent = claudeCode(model, opts);
|
|
@@ -4842,8 +5042,10 @@ function selectSandbox(config2, mounts = []) {
|
|
|
4842
5042
|
return docker(imageOpts);
|
|
4843
5043
|
case "podman":
|
|
4844
5044
|
return podman(imageOpts);
|
|
4845
|
-
|
|
5045
|
+
case "noSandbox":
|
|
4846
5046
|
return noSandbox();
|
|
5047
|
+
default:
|
|
5048
|
+
throw new Error(`no sandbox provider for sandbox "${config2.sandbox}" \u2014 refusing to run the agent unsandboxed`);
|
|
4847
5049
|
}
|
|
4848
5050
|
}
|
|
4849
5051
|
function buildSandboxOptions(config2, mounts = []) {
|
|
@@ -4988,7 +5190,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
4988
5190
|
}
|
|
4989
5191
|
await cleanupBurnWorktree(project.repoPath, resolveBranch);
|
|
4990
5192
|
}
|
|
4991
|
-
const blocked =
|
|
5193
|
+
const blocked = readAgentFile([result?.preservedWorktreePath, project.repoPath], "BLOCKED.md");
|
|
4992
5194
|
if (blocked !== undefined && blocked.trim().length > 0) {
|
|
4993
5195
|
return {
|
|
4994
5196
|
ok: false,
|
|
@@ -5022,11 +5224,16 @@ ${blocked.trim()}`
|
|
|
5022
5224
|
label: `ticket ${ticket.seq}`,
|
|
5023
5225
|
featureBranch: feature.branch
|
|
5024
5226
|
};
|
|
5227
|
+
let harvestedDigest;
|
|
5025
5228
|
const landChain = async (branch, commits) => {
|
|
5026
5229
|
const landing = await landWithResolve(branch, landDeps);
|
|
5027
5230
|
if (landing.status === "landed") {
|
|
5028
5231
|
ctx.updateTicket(ticket.id, { attemptBranch: null, conflictFiles: null });
|
|
5029
|
-
return {
|
|
5232
|
+
return {
|
|
5233
|
+
status: "done",
|
|
5234
|
+
commits: landedCommits.length > 0 ? landedCommits : commits,
|
|
5235
|
+
digest: harvestedDigest
|
|
5236
|
+
};
|
|
5030
5237
|
}
|
|
5031
5238
|
preserveChain(landing.branch);
|
|
5032
5239
|
if (landing.status === "conflict") {
|
|
@@ -5187,7 +5394,9 @@ ${retryNotes}` : basePrompt,
|
|
|
5187
5394
|
}
|
|
5188
5395
|
if (!result)
|
|
5189
5396
|
return { status: "failed", error: "internal: run loop produced no result" };
|
|
5190
|
-
const
|
|
5397
|
+
const agentFileDirs = [result.preservedWorktreePath, project.repoPath];
|
|
5398
|
+
const blocked = readAgentFile(agentFileDirs, "BLOCKED.md");
|
|
5399
|
+
harvestedDigest = harvestDigest(agentFileDirs);
|
|
5191
5400
|
const chain = await branchCommitsAhead(project.repoPath, feature.branch, tempBranch);
|
|
5192
5401
|
let outcome;
|
|
5193
5402
|
if (result.commits.length === 0 && blocked !== undefined && blocked.trim().length > 0) {
|
|
@@ -5321,8 +5530,6 @@ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as re
|
|
|
5321
5530
|
import { dirname as dirname4, join as join10 } from "path";
|
|
5322
5531
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5323
5532
|
import { run as run2 } from "@ai-hero/sandcastle";
|
|
5324
|
-
import { docker as docker2 } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
5325
|
-
import { noSandbox as noSandbox2 } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
|
|
5326
5533
|
function waypointSlug(waypoint) {
|
|
5327
5534
|
const base = waypoint.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5328
5535
|
return `${waypoint.seq}-${base || "waypoint"}`;
|
|
@@ -5336,7 +5543,7 @@ async function researchRun(ctx, deps) {
|
|
|
5336
5543
|
ctx.emitEvent({ type: "research.error", message: "research run started without a waypoint" });
|
|
5337
5544
|
return { status: "failed", summary: "no waypoint to research" };
|
|
5338
5545
|
}
|
|
5339
|
-
if (deps.config.sandbox
|
|
5546
|
+
if (deps.config.sandbox !== "noSandbox" && !deps.hasAuthToken) {
|
|
5340
5547
|
ctx.emitEvent({ type: AUTH_MISSING_EVENT2, message: AUTH_MISSING_MESSAGE2 });
|
|
5341
5548
|
return { status: "failed", summary: "research aborted: auth token missing" };
|
|
5342
5549
|
}
|
|
@@ -5418,7 +5625,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
5418
5625
|
const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
|
|
5419
5626
|
const runOptions = {
|
|
5420
5627
|
agent: buildBurnAgent(config2, token, model),
|
|
5421
|
-
sandbox:
|
|
5628
|
+
sandbox: selectSandbox(config2),
|
|
5422
5629
|
cwd: project.repoPath,
|
|
5423
5630
|
prompt,
|
|
5424
5631
|
branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
|
|
@@ -5594,10 +5801,12 @@ function cancelRun(runId) {
|
|
|
5594
5801
|
async function executeRun(ctx, runId, featureId, workflow2, runPromise, controller, cleanup) {
|
|
5595
5802
|
let status = "failed";
|
|
5596
5803
|
let summary = "run failed";
|
|
5804
|
+
let digest;
|
|
5597
5805
|
try {
|
|
5598
5806
|
const result = await runPromise;
|
|
5599
5807
|
status = result.status;
|
|
5600
5808
|
summary = result.summary;
|
|
5809
|
+
digest = result.digest;
|
|
5601
5810
|
} catch (e) {
|
|
5602
5811
|
if (controller.signal.aborted) {
|
|
5603
5812
|
status = "cancelled";
|
|
@@ -5610,7 +5819,7 @@ async function executeRun(ctx, runId, featureId, workflow2, runPromise, controll
|
|
|
5610
5819
|
} finally {
|
|
5611
5820
|
controllers.delete(runId);
|
|
5612
5821
|
}
|
|
5613
|
-
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary }).where(eq8(runs.id, runId)).run();
|
|
5822
|
+
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary, ...digest ? { digest } : {} }).where(eq8(runs.id, runId)).run();
|
|
5614
5823
|
releaseForSession(ctx, runId);
|
|
5615
5824
|
if (workflowClaimsFeatureBranch(workflow2)) {
|
|
5616
5825
|
sweepOrphanedBurning(ctx, featureId, `orphaned \u2014 the run ended (${status}) while it was burning`);
|
|
@@ -5665,36 +5874,73 @@ async function createFeature(ctx, input) {
|
|
|
5665
5874
|
const slug = uniqueSlug(ctx, project.id, input.title);
|
|
5666
5875
|
const branch = `feature/${slug}`;
|
|
5667
5876
|
const requestedBase = input.baseBranch?.trim() || project.mainBranch;
|
|
5668
|
-
const
|
|
5877
|
+
const cut = input.draft ? null : await ensureFeatureBranch(project, slug, requestedBase);
|
|
5669
5878
|
const row = {
|
|
5670
5879
|
id: newId("feat"),
|
|
5671
5880
|
projectId: project.id,
|
|
5672
5881
|
slug,
|
|
5673
5882
|
title: input.title,
|
|
5674
5883
|
oneLiner: input.oneLiner,
|
|
5884
|
+
brief: input.draft ? input.brief ?? null : null,
|
|
5675
5885
|
mapped: false,
|
|
5676
5886
|
lap: 1,
|
|
5677
5887
|
phase: "ideation",
|
|
5678
5888
|
branch,
|
|
5679
|
-
baseBranch,
|
|
5680
|
-
status: "active",
|
|
5889
|
+
baseBranch: cut?.baseBranch ?? null,
|
|
5890
|
+
status: cut ? "active" : "draft",
|
|
5681
5891
|
createdAt: Date.now()
|
|
5682
5892
|
};
|
|
5683
5893
|
const inserted = ctx.db.insert(features).values(row).returning().get();
|
|
5684
5894
|
const feature = rowToFeature(inserted);
|
|
5685
5895
|
emit(ctx, feature.id, {
|
|
5686
5896
|
type: "feature.created",
|
|
5687
|
-
message: branchReady ? `feature.created (${branch} \u2190 ${baseBranch})` : "feature.created (branch pending)",
|
|
5688
|
-
data: {
|
|
5897
|
+
message: !cut ? `feature.created (draft \u2014 ${branch} not cut yet)` : cut.branchReady ? `feature.created (${branch} \u2190 ${cut.baseBranch})` : "feature.created (branch pending)",
|
|
5898
|
+
data: {
|
|
5899
|
+
slug,
|
|
5900
|
+
branch,
|
|
5901
|
+
baseBranch: cut?.baseBranch,
|
|
5902
|
+
branchReady: cut?.branchReady ?? false,
|
|
5903
|
+
draft: !cut
|
|
5904
|
+
}
|
|
5689
5905
|
});
|
|
5906
|
+
if (!cut)
|
|
5907
|
+
return feature;
|
|
5690
5908
|
scaffoldDocs(ctx, feature, { brief: input.brief });
|
|
5691
|
-
if (branchReady) {
|
|
5909
|
+
if (cut.branchReady) {
|
|
5692
5910
|
try {
|
|
5693
5911
|
await commitDocs(project.repoPath, `runcastle: scaffold ${slug} docs`);
|
|
5694
5912
|
} catch {}
|
|
5695
5913
|
}
|
|
5696
5914
|
return feature;
|
|
5697
5915
|
}
|
|
5916
|
+
function requireNotDraft(feature) {
|
|
5917
|
+
if (feature.status === "draft") {
|
|
5918
|
+
throw new GateError(`\`${feature.slug}\` is a draft \u2014 click Start to cut its branch and begin`);
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
async function startDraft(ctx, featureId, opts = {}) {
|
|
5922
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
5923
|
+
if (feature.status !== "draft") {
|
|
5924
|
+
throw new GateError(`feature ${feature.slug} is not a draft \u2014 it has already been started`);
|
|
5925
|
+
}
|
|
5926
|
+
const project = projectForFeature(ctx, feature);
|
|
5927
|
+
const requestedBase = opts.baseBranch?.trim() || project.mainBranch;
|
|
5928
|
+
const { branchReady, baseBranch } = await ensureFeatureBranch(project, feature.slug, requestedBase);
|
|
5929
|
+
ctx.db.update(features).set({ status: "active", baseBranch }).where(eq9(features.id, featureId)).run();
|
|
5930
|
+
const started = { ...feature, status: "active", baseBranch };
|
|
5931
|
+
emit(ctx, featureId, {
|
|
5932
|
+
type: "feature.started",
|
|
5933
|
+
message: branchReady ? `feature ${feature.slug} started (${feature.branch} \u2190 ${baseBranch})` : `feature ${feature.slug} started (branch pending)`,
|
|
5934
|
+
data: { branch: feature.branch, baseBranch, branchReady }
|
|
5935
|
+
});
|
|
5936
|
+
scaffoldDocs(ctx, started, { brief: feature.brief });
|
|
5937
|
+
if (branchReady) {
|
|
5938
|
+
try {
|
|
5939
|
+
await commitDocs(project.repoPath, `runcastle: scaffold ${feature.slug} docs`);
|
|
5940
|
+
} catch {}
|
|
5941
|
+
}
|
|
5942
|
+
return started;
|
|
5943
|
+
}
|
|
5698
5944
|
async function quickChange(ctx, input) {
|
|
5699
5945
|
const project = requireProjectById(ctx, input.projectId);
|
|
5700
5946
|
const title = input.title.trim();
|
|
@@ -5813,6 +6059,7 @@ function liveSessionOf(ctx, featureId) {
|
|
|
5813
6059
|
}
|
|
5814
6060
|
function advance(ctx, featureId) {
|
|
5815
6061
|
const feature = getFeatureRow(ctx, featureId);
|
|
6062
|
+
requireNotDraft(feature);
|
|
5816
6063
|
const gate = nextGate(feature);
|
|
5817
6064
|
if (!gate)
|
|
5818
6065
|
throw new GateError("feature is already at the final phase");
|
|
@@ -5830,6 +6077,7 @@ function advance(ctx, featureId) {
|
|
|
5830
6077
|
}
|
|
5831
6078
|
async function burn(ctx, featureId, opts = {}) {
|
|
5832
6079
|
const feature = getFeatureRow(ctx, featureId);
|
|
6080
|
+
requireNotDraft(feature);
|
|
5833
6081
|
const running = hasActiveRun(ctx, featureId);
|
|
5834
6082
|
let tickets2 = listByFeature2(ctx, featureId);
|
|
5835
6083
|
const lapTickets = tickets2.filter((t) => t.lap === feature.lap);
|
|
@@ -5880,6 +6128,7 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
5880
6128
|
}
|
|
5881
6129
|
function rethink(ctx, featureId) {
|
|
5882
6130
|
const feature = getFeatureRow(ctx, featureId);
|
|
6131
|
+
requireNotDraft(feature);
|
|
5883
6132
|
if (feature.phase !== RETHINK_LOOP_BACK.from) {
|
|
5884
6133
|
throw new GateError(`feature must be in the review phase to rethink (currently ${feature.phase})`);
|
|
5885
6134
|
}
|
|
@@ -5993,6 +6242,7 @@ function escalateToMap(ctx, featureId, input) {
|
|
|
5993
6242
|
}
|
|
5994
6243
|
function archiveFeature(ctx, featureId) {
|
|
5995
6244
|
const feature = getFeatureRow(ctx, featureId);
|
|
6245
|
+
requireNotDraft(feature);
|
|
5996
6246
|
if (feature.status === "archived") {
|
|
5997
6247
|
throw new GateError(`feature ${feature.slug} is already archived`);
|
|
5998
6248
|
}
|
|
@@ -6276,8 +6526,19 @@ async function toolDryRunDrive(ctx, session, input) {
|
|
|
6276
6526
|
}
|
|
6277
6527
|
return dryRunDrive(ctx, project, input.action);
|
|
6278
6528
|
}
|
|
6529
|
+
function createFeatureProject(ctx, session, input) {
|
|
6530
|
+
if (isProjectSessionKind(session.kind))
|
|
6531
|
+
return requireProject(ctx, session);
|
|
6532
|
+
if (!DRAFTING_KINDS.includes(session.kind)) {
|
|
6533
|
+
throw new GateError(`a ${session.kind} session is read-only and may not create features, drafts included. ` + "Tell the human what is worth capturing; the project session is where features are made.");
|
|
6534
|
+
}
|
|
6535
|
+
if (!input.draft || input.ticket) {
|
|
6536
|
+
throw new GateError(`a ${session.kind} session may only PARK a feature here: call create_feature with ` + "`draft: true` and a `brief` carrying why you deferred it. Cutting a branch \u2014 a full " + "create or the quick-change `ticket` shape \u2014 belongs to the project session; tell the " + "human to open it.");
|
|
6537
|
+
}
|
|
6538
|
+
return projectForFeature(ctx, getFeatureRow(ctx, requireFeatureId(session)));
|
|
6539
|
+
}
|
|
6279
6540
|
async function toolCreateFeature(ctx, session, input) {
|
|
6280
|
-
const project =
|
|
6541
|
+
const project = createFeatureProject(ctx, session, input);
|
|
6281
6542
|
const feature = input.ticket ? await quickChange(ctx, {
|
|
6282
6543
|
projectId: project.id,
|
|
6283
6544
|
title: input.title,
|
|
@@ -6288,7 +6549,8 @@ async function toolCreateFeature(ctx, session, input) {
|
|
|
6288
6549
|
title: input.title,
|
|
6289
6550
|
oneLiner: input.oneLiner,
|
|
6290
6551
|
baseBranch: input.baseBranch,
|
|
6291
|
-
brief: input.brief
|
|
6552
|
+
brief: input.brief,
|
|
6553
|
+
draft: input.draft
|
|
6292
6554
|
});
|
|
6293
6555
|
return {
|
|
6294
6556
|
id: feature.id,
|
|
@@ -6349,7 +6611,8 @@ function toolGetWorkRecord(ctx, session, input) {
|
|
|
6349
6611
|
status: t.status,
|
|
6350
6612
|
seams: t.seams,
|
|
6351
6613
|
commits: t.commits,
|
|
6352
|
-
...t.error !== undefined ? { error: t.error } : {}
|
|
6614
|
+
...t.error !== undefined ? { error: t.error } : {},
|
|
6615
|
+
...t.digest !== undefined ? { digest: t.digest } : {}
|
|
6353
6616
|
}))
|
|
6354
6617
|
});
|
|
6355
6618
|
}
|
|
@@ -6403,12 +6666,13 @@ function buildMcpServer() {
|
|
|
6403
6666
|
});
|
|
6404
6667
|
server.registerTool("create_feature", {
|
|
6405
6668
|
title: "Create a feature",
|
|
6406
|
-
description: "Create a feature from the project session \u2014 the end of intake. Pass `brief` with the " + "reasoning you just worked out with the human (why this feature exists, what it must NOT " + "swallow): it becomes brief.md verbatim, and without it that reasoning is lost. Pass " + "`ticket: { prose }` for a quick change \u2014 work too small to deserve a grill \u2014 which " + "creates the feature at the implementation phase with that one ticket, and whose brief.md " + "is the prose itself (so `brief` is unused there). This does NOT open " + "a terminal on what it creates; the new card in the rail is the feedback, and the human " + "decides what to work on next.",
|
|
6669
|
+
description: "Create a feature from the project session \u2014 the end of intake. Pass `brief` with the " + "reasoning you just worked out with the human (why this feature exists, what it must NOT " + "swallow): it becomes brief.md verbatim, and without it that reasoning is lost. Pass " + "`draft: true` to PARK it instead of starting it \u2014 a row and its brief, no branch and no " + "files, until the human clicks Start; ask them per feature whether to start it now or " + "park it. Pass " + "`ticket: { prose }` for a quick change \u2014 work too small to deserve a grill \u2014 which " + "creates the feature at the implementation phase with that one ticket, and whose brief.md " + "is the prose itself (so `brief` is unused there). From a feature session (ideation, " + "revisit, waypoint, converge) this tool parks drafts and nothing else: `draft: true` is " + "required and the `ticket` shape is refused, so deflect scope creep here and leave full " + "creation to the project session. This does NOT open " + "a terminal on what it creates; the new card in the rail is the feedback, and the human " + "decides what to work on next.",
|
|
6407
6670
|
inputSchema: {
|
|
6408
6671
|
title: z3.string().min(1),
|
|
6409
6672
|
oneLiner: z3.string(),
|
|
6410
6673
|
baseBranch: z3.string().optional(),
|
|
6411
6674
|
brief: z3.string().optional(),
|
|
6675
|
+
draft: z3.boolean().optional(),
|
|
6412
6676
|
ticket: z3.object({ prose: z3.string().min(1) }).optional()
|
|
6413
6677
|
}
|
|
6414
6678
|
}, async (args, extra) => {
|
|
@@ -6429,7 +6693,7 @@ function buildMcpServer() {
|
|
|
6429
6693
|
});
|
|
6430
6694
|
server.registerTool("get_work_record", {
|
|
6431
6695
|
title: "Get work record",
|
|
6432
|
-
description: "What features actually DID \u2014 facts only: per feature its status, ship date, run " + "summaries and tickets as { seq, title, status, seams, commits, error? }.
|
|
6696
|
+
description: "What features actually DID \u2014 facts only: per feature its status, ship date, run " + "summaries and tickets as { seq, title, status, seams, commits, error?, digest? }. The " + "digest is the burner's own account, written after the work: what it did, what " + "surprised it, what it left undone. Never a ticket's goal or acceptance criteria: " + "those are intent from before the code existed. " + 'Query by `featureSlug` ("what did X do?") or by `seam` ("who has touched this area, and ' + 'what happened?") \u2014 the seam match is a case-insensitive substring, because seams are ' + "free prose that differs between features. At least one argument is required.",
|
|
6433
6697
|
inputSchema: { featureSlug: z3.string().optional(), seam: z3.string().optional() }
|
|
6434
6698
|
}, async (args, extra) => {
|
|
6435
6699
|
const rs = await resolveCtxSession(extra);
|
|
@@ -6552,7 +6816,7 @@ function buildMcpServer() {
|
|
|
6552
6816
|
});
|
|
6553
6817
|
return server;
|
|
6554
6818
|
}
|
|
6555
|
-
var mcp, server_default;
|
|
6819
|
+
var DRAFTING_KINDS, mcp, server_default;
|
|
6556
6820
|
var init_server = __esm(() => {
|
|
6557
6821
|
init_src();
|
|
6558
6822
|
init_paths();
|
|
@@ -6566,6 +6830,7 @@ var init_server = __esm(() => {
|
|
|
6566
6830
|
init_repo();
|
|
6567
6831
|
init_tickets();
|
|
6568
6832
|
init_waypoints();
|
|
6833
|
+
DRAFTING_KINDS = ["ideation", "revisit", "waypoint", "converge"];
|
|
6569
6834
|
mcp = new Hono;
|
|
6570
6835
|
mcp.use("*", async (c, next) => {
|
|
6571
6836
|
await next();
|
|
@@ -6657,25 +6922,29 @@ function guardsEdits(kind) {
|
|
|
6657
6922
|
function evaluateEditGuard(input) {
|
|
6658
6923
|
if (!guardsEdits(input.kind))
|
|
6659
6924
|
return null;
|
|
6925
|
+
if (input.purpose === "resolve-conflict" && input.mergeInProgress)
|
|
6926
|
+
return null;
|
|
6660
6927
|
if (!input.toolName || !EDIT_TOOLS.includes(input.toolName))
|
|
6661
6928
|
return null;
|
|
6662
6929
|
if (!input.filePath)
|
|
6663
6930
|
return null;
|
|
6931
|
+
const target = resolve7(input.worktreePath, input.filePath);
|
|
6664
6932
|
if (!input.featureSlug) {
|
|
6665
6933
|
return {
|
|
6666
6934
|
reason: `This ${input.kind} session does not edit files \u2014 it runs in the developer's own ` + "checkout. Record what you establish with the `record_finding` MCP tool, and ask the " + "human to make any change to the repo itself."
|
|
6667
6935
|
};
|
|
6668
6936
|
}
|
|
6669
6937
|
const docs = featureDocsRel(input.featureSlug);
|
|
6670
|
-
|
|
6671
|
-
const target = resolve7(input.worktreePath, input.filePath);
|
|
6672
|
-
const rel = relative2(docsDir, target);
|
|
6673
|
-
if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel))
|
|
6938
|
+
if (within(resolve7(input.worktreePath, docs), target))
|
|
6674
6939
|
return null;
|
|
6675
6940
|
return {
|
|
6676
6941
|
reason: `Talk sessions do not write code. This ${input.kind} session may only write this ` + `feature's docs under \`${docs}/\` \u2014 ${input.filePath} is outside them. The change you ` + "want belongs in a ticket: capture the decision in `decisions.md`, amend `spec.md`, and " + "emit a ticket for the work. An implementation agent burns it in its own sandbox."
|
|
6677
6942
|
};
|
|
6678
6943
|
}
|
|
6944
|
+
function within(dir, target) {
|
|
6945
|
+
const rel = relative2(dir, target);
|
|
6946
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
6947
|
+
}
|
|
6679
6948
|
function editDenyResponse(denial) {
|
|
6680
6949
|
return {
|
|
6681
6950
|
hookSpecificOutput: {
|
|
@@ -6833,22 +7102,40 @@ function handleUserPrompt(ctx, sessionId, feature, payload) {
|
|
|
6833
7102
|
}
|
|
6834
7103
|
};
|
|
6835
7104
|
}
|
|
6836
|
-
function handleSessionEnd(ctx,
|
|
6837
|
-
markSessionEnded(ctx,
|
|
6838
|
-
releaseForSession(ctx,
|
|
7105
|
+
async function handleSessionEnd(ctx, session, feature) {
|
|
7106
|
+
markSessionEnded(ctx, session.id);
|
|
7107
|
+
releaseForSession(ctx, session.id);
|
|
6839
7108
|
emit(ctx, feature.id, {
|
|
6840
7109
|
type: "session.ended",
|
|
6841
7110
|
message: "session ended",
|
|
6842
|
-
data: { sessionId }
|
|
7111
|
+
data: { sessionId: session.id }
|
|
6843
7112
|
});
|
|
7113
|
+
await noteResolvedMerge(ctx, session, feature);
|
|
6844
7114
|
return {};
|
|
6845
7115
|
}
|
|
6846
|
-
function
|
|
7116
|
+
async function noteResolvedMerge(ctx, session, feature) {
|
|
7117
|
+
const pair = session.purpose === "resolve-conflict" ? session.purposeData : undefined;
|
|
7118
|
+
if (!pair)
|
|
7119
|
+
return;
|
|
7120
|
+
try {
|
|
7121
|
+
if (!await isAncestor(session.worktreePath, pair.mergeFrom, pair.mergeInto))
|
|
7122
|
+
return;
|
|
7123
|
+
emit(ctx, feature.id, {
|
|
7124
|
+
type: "merge.resolved",
|
|
7125
|
+
message: `merge conflict resolved \u2014 ${pair.mergeFrom} is in ${pair.mergeInto}`,
|
|
7126
|
+
data: { sessionId: session.id, ...pair }
|
|
7127
|
+
});
|
|
7128
|
+
} catch {}
|
|
7129
|
+
}
|
|
7130
|
+
async function handlePreToolUse(session, feature, payload) {
|
|
6847
7131
|
const toolName = typeof payload?.tool_name === "string" ? payload.tool_name : undefined;
|
|
6848
7132
|
const toolInput = payload?.tool_input ?? {};
|
|
6849
7133
|
const path = typeof toolInput.file_path === "string" ? toolInput.file_path : typeof toolInput.notebook_path === "string" ? toolInput.notebook_path : undefined;
|
|
7134
|
+
const mergeInProgress2 = session.purpose === "resolve-conflict" ? await mergeInProgressAt(session.worktreePath) : false;
|
|
6850
7135
|
const denial = evaluateEditGuard({
|
|
6851
7136
|
kind: session.kind,
|
|
7137
|
+
purpose: session.purpose,
|
|
7138
|
+
mergeInProgress: mergeInProgress2,
|
|
6852
7139
|
worktreePath: session.worktreePath,
|
|
6853
7140
|
toolName,
|
|
6854
7141
|
filePath: path,
|
|
@@ -6877,6 +7164,7 @@ var init_hooks = __esm(() => {
|
|
|
6877
7164
|
init_edit_guard();
|
|
6878
7165
|
init_sessions();
|
|
6879
7166
|
init_events();
|
|
7167
|
+
init_git();
|
|
6880
7168
|
init_prep();
|
|
6881
7169
|
init_repo();
|
|
6882
7170
|
init_tickets();
|
|
@@ -6914,7 +7202,7 @@ var init_hooks = __esm(() => {
|
|
|
6914
7202
|
case "session-end":
|
|
6915
7203
|
return c.json(handleProjectScopedSessionEnd(ctx, session));
|
|
6916
7204
|
case "pre-tool":
|
|
6917
|
-
return c.json(handlePreToolUse(session, undefined, body.payload));
|
|
7205
|
+
return c.json(await handlePreToolUse(session, undefined, body.payload));
|
|
6918
7206
|
default:
|
|
6919
7207
|
return c.json({});
|
|
6920
7208
|
}
|
|
@@ -6928,9 +7216,9 @@ var init_hooks = __esm(() => {
|
|
|
6928
7216
|
case "user-prompt":
|
|
6929
7217
|
return c.json(handleUserPrompt(ctx, sessionId, feature, body.payload));
|
|
6930
7218
|
case "session-end":
|
|
6931
|
-
return c.json(handleSessionEnd(ctx,
|
|
7219
|
+
return c.json(await handleSessionEnd(ctx, session, feature));
|
|
6932
7220
|
case "pre-tool":
|
|
6933
|
-
return c.json(handlePreToolUse(session, feature, body.payload));
|
|
7221
|
+
return c.json(await handlePreToolUse(session, feature, body.payload));
|
|
6934
7222
|
default:
|
|
6935
7223
|
return c.json({});
|
|
6936
7224
|
}
|
|
@@ -7072,6 +7360,188 @@ var init_web = __esm(() => {
|
|
|
7072
7360
|
};
|
|
7073
7361
|
});
|
|
7074
7362
|
|
|
7363
|
+
// src/version.ts
|
|
7364
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
7365
|
+
import { dirname as dirname5, join as join12 } from "path";
|
|
7366
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
7367
|
+
function runcastleVersion() {
|
|
7368
|
+
if (cached2)
|
|
7369
|
+
return cached2;
|
|
7370
|
+
const here = dirname5(fileURLToPath4(import.meta.url));
|
|
7371
|
+
for (const pkgPath of [join12(here, "..", "package.json"), join12(here, "package.json")]) {
|
|
7372
|
+
try {
|
|
7373
|
+
const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
|
|
7374
|
+
if (typeof pkg.version === "string") {
|
|
7375
|
+
cached2 = pkg.version;
|
|
7376
|
+
return cached2;
|
|
7377
|
+
}
|
|
7378
|
+
} catch {}
|
|
7379
|
+
}
|
|
7380
|
+
cached2 = UNKNOWN_VERSION;
|
|
7381
|
+
return cached2;
|
|
7382
|
+
}
|
|
7383
|
+
var cached2, UNKNOWN_VERSION = "0.0.0";
|
|
7384
|
+
var init_version = () => {};
|
|
7385
|
+
|
|
7386
|
+
// src/services/install-id.ts
|
|
7387
|
+
import { randomUUID } from "crypto";
|
|
7388
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
7389
|
+
import { dirname as dirname6 } from "path";
|
|
7390
|
+
function getInstallId() {
|
|
7391
|
+
const file = installIdPath();
|
|
7392
|
+
try {
|
|
7393
|
+
if (existsSync13(file)) {
|
|
7394
|
+
const existing = readFileSync7(file, "utf8").trim();
|
|
7395
|
+
if (UUID_RE.test(existing))
|
|
7396
|
+
return existing;
|
|
7397
|
+
}
|
|
7398
|
+
const id = randomUUID();
|
|
7399
|
+
mkdirSync6(dirname6(file), { recursive: true });
|
|
7400
|
+
writeFileSync2(file, `${id}
|
|
7401
|
+
`);
|
|
7402
|
+
return id;
|
|
7403
|
+
} catch {
|
|
7404
|
+
return randomUUID();
|
|
7405
|
+
}
|
|
7406
|
+
}
|
|
7407
|
+
var UUID_RE;
|
|
7408
|
+
var init_install_id = __esm(() => {
|
|
7409
|
+
init_paths();
|
|
7410
|
+
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
7411
|
+
});
|
|
7412
|
+
|
|
7413
|
+
// src/services/update-check.ts
|
|
7414
|
+
function parse(version) {
|
|
7415
|
+
const cleaned = version.trim().replace(/^v/, "");
|
|
7416
|
+
const [core = "", pre = ""] = cleaned.split("-", 2);
|
|
7417
|
+
const release2 = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
7418
|
+
return { release: release2, pre: pre ? pre.split(".") : [] };
|
|
7419
|
+
}
|
|
7420
|
+
function compareSemver(a, b) {
|
|
7421
|
+
const pa = parse(a);
|
|
7422
|
+
const pb = parse(b);
|
|
7423
|
+
const len = Math.max(pa.release.length, pb.release.length);
|
|
7424
|
+
for (let i = 0;i < len; i++) {
|
|
7425
|
+
const diff = (pa.release[i] ?? 0) - (pb.release[i] ?? 0);
|
|
7426
|
+
if (diff !== 0)
|
|
7427
|
+
return diff;
|
|
7428
|
+
}
|
|
7429
|
+
if (pa.pre.length === 0 && pb.pre.length === 0)
|
|
7430
|
+
return 0;
|
|
7431
|
+
if (pa.pre.length === 0)
|
|
7432
|
+
return 1;
|
|
7433
|
+
if (pb.pre.length === 0)
|
|
7434
|
+
return -1;
|
|
7435
|
+
const preLen = Math.max(pa.pre.length, pb.pre.length);
|
|
7436
|
+
for (let i = 0;i < preLen; i++) {
|
|
7437
|
+
const x = pa.pre[i];
|
|
7438
|
+
const y = pb.pre[i];
|
|
7439
|
+
if (x === undefined)
|
|
7440
|
+
return -1;
|
|
7441
|
+
if (y === undefined)
|
|
7442
|
+
return 1;
|
|
7443
|
+
const nx = Number(x);
|
|
7444
|
+
const ny = Number(y);
|
|
7445
|
+
const bothNumeric = !Number.isNaN(nx) && !Number.isNaN(ny);
|
|
7446
|
+
const cmp = bothNumeric ? nx - ny : x < y ? -1 : x > y ? 1 : 0;
|
|
7447
|
+
if (cmp !== 0)
|
|
7448
|
+
return cmp;
|
|
7449
|
+
}
|
|
7450
|
+
return 0;
|
|
7451
|
+
}
|
|
7452
|
+
function doNotTrack(env) {
|
|
7453
|
+
const raw = env.DO_NOT_TRACK;
|
|
7454
|
+
return raw !== undefined && raw !== "" && raw !== "0";
|
|
7455
|
+
}
|
|
7456
|
+
async function latestFromNpm(fetchImpl) {
|
|
7457
|
+
try {
|
|
7458
|
+
const res = await fetchImpl(NPM_LATEST_URL);
|
|
7459
|
+
if (!res.ok)
|
|
7460
|
+
return null;
|
|
7461
|
+
const body = await res.json();
|
|
7462
|
+
return typeof body.version === "string" ? body.version : null;
|
|
7463
|
+
} catch {
|
|
7464
|
+
return null;
|
|
7465
|
+
}
|
|
7466
|
+
}
|
|
7467
|
+
async function latestFromPing(fetchImpl, current2) {
|
|
7468
|
+
try {
|
|
7469
|
+
const res = await fetchImpl(PING_URL, {
|
|
7470
|
+
method: "POST",
|
|
7471
|
+
headers: { "content-type": "application/json" },
|
|
7472
|
+
body: JSON.stringify({
|
|
7473
|
+
installId: getInstallId(),
|
|
7474
|
+
version: current2,
|
|
7475
|
+
platform: process.platform
|
|
7476
|
+
})
|
|
7477
|
+
});
|
|
7478
|
+
if (!res.ok)
|
|
7479
|
+
return null;
|
|
7480
|
+
const body = await res.json();
|
|
7481
|
+
return typeof body.latest === "string" ? body.latest : null;
|
|
7482
|
+
} catch {
|
|
7483
|
+
return null;
|
|
7484
|
+
}
|
|
7485
|
+
}
|
|
7486
|
+
async function checkForUpdate(opts) {
|
|
7487
|
+
const { current: current2 } = opts;
|
|
7488
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
7489
|
+
const env = opts.env ?? process.env;
|
|
7490
|
+
if (current2 === UNKNOWN_VERSION)
|
|
7491
|
+
return NO_UPDATE(current2);
|
|
7492
|
+
const latest = doNotTrack(env) ? await latestFromNpm(fetchImpl) : await latestFromPing(fetchImpl, current2) ?? await latestFromNpm(fetchImpl);
|
|
7493
|
+
if (!latest)
|
|
7494
|
+
return NO_UPDATE(current2);
|
|
7495
|
+
return {
|
|
7496
|
+
current: current2,
|
|
7497
|
+
latest,
|
|
7498
|
+
updateAvailable: compareSemver(latest, current2) > 0,
|
|
7499
|
+
command: UPDATE_COMMAND
|
|
7500
|
+
};
|
|
7501
|
+
}
|
|
7502
|
+
async function getUpdateInfo(current2, fetchImpl) {
|
|
7503
|
+
if (cache)
|
|
7504
|
+
return cache;
|
|
7505
|
+
const info = await checkForUpdate({ current: current2, fetchImpl });
|
|
7506
|
+
if (info.latest !== null)
|
|
7507
|
+
cache = info;
|
|
7508
|
+
return info;
|
|
7509
|
+
}
|
|
7510
|
+
var PACKAGE_NAME = "runcastle", UPDATE_COMMAND, PING_URL = "https://ping.runcastle.dev/ping", NPM_LATEST_URL, NO_UPDATE = (current2) => ({
|
|
7511
|
+
current: current2,
|
|
7512
|
+
latest: null,
|
|
7513
|
+
updateAvailable: false,
|
|
7514
|
+
command: UPDATE_COMMAND
|
|
7515
|
+
}), cache;
|
|
7516
|
+
var init_update_check = __esm(() => {
|
|
7517
|
+
init_version();
|
|
7518
|
+
init_install_id();
|
|
7519
|
+
UPDATE_COMMAND = `bun add -g ${PACKAGE_NAME}@latest`;
|
|
7520
|
+
NPM_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
7521
|
+
});
|
|
7522
|
+
|
|
7523
|
+
// src/shutdown.ts
|
|
7524
|
+
function createShutdown(deps, timeoutMs = SHUTDOWN_TIMEOUT_MS) {
|
|
7525
|
+
let settling = false;
|
|
7526
|
+
return () => {
|
|
7527
|
+
if (settling) {
|
|
7528
|
+
deps.exit(1);
|
|
7529
|
+
return;
|
|
7530
|
+
}
|
|
7531
|
+
settling = true;
|
|
7532
|
+
(async () => {
|
|
7533
|
+
const deadline = new Promise((resolve9) => {
|
|
7534
|
+
const timer = setTimeout(resolve9, timeoutMs);
|
|
7535
|
+
timer.unref?.();
|
|
7536
|
+
});
|
|
7537
|
+
await Promise.race([deps.killAllTrees().catch(() => {}), deadline]);
|
|
7538
|
+
deps.stop();
|
|
7539
|
+
deps.exit(0);
|
|
7540
|
+
})();
|
|
7541
|
+
};
|
|
7542
|
+
}
|
|
7543
|
+
var SHUTDOWN_TIMEOUT_MS = 5000;
|
|
7544
|
+
|
|
7075
7545
|
// src/trpc/context.ts
|
|
7076
7546
|
import { initTRPC } from "@trpc/server";
|
|
7077
7547
|
var t, errorMapping, router, publicProcedure, createCallerFactory;
|
|
@@ -7120,11 +7590,11 @@ var init_events2 = __esm(() => {
|
|
|
7120
7590
|
});
|
|
7121
7591
|
|
|
7122
7592
|
// src/launcher/artifacts.ts
|
|
7123
|
-
import { mkdirSync as
|
|
7124
|
-
import { fileURLToPath as
|
|
7125
|
-
import { join as
|
|
7593
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
7594
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
7595
|
+
import { join as join13 } from "path";
|
|
7126
7596
|
function hookClientPath() {
|
|
7127
|
-
return resolveAsset(ASSET_ENV.hookClient,
|
|
7597
|
+
return resolveAsset(ASSET_ENV.hookClient, fileURLToPath5(new URL("./hook-client.ts", import.meta.url)));
|
|
7128
7598
|
}
|
|
7129
7599
|
function serverUrlFor(config2) {
|
|
7130
7600
|
return `http://localhost:${config2.serverPort}`;
|
|
@@ -7132,13 +7602,16 @@ function serverUrlFor(config2) {
|
|
|
7132
7602
|
function noCodeRule(docs) {
|
|
7133
7603
|
return `- **Talk sessions do not write code.** You may write this feature's docs under \`${docs}/\` ` + "and nothing else in this checkout. Every code change rides a ticket, burned by an " + "implementation agent in its own sandbox \u2014 even the one-line fix that is obviously faster " + "to just do. This is enforced by a hook, not left to your judgement: edits outside the docs " + "are denied.";
|
|
7134
7604
|
}
|
|
7135
|
-
function
|
|
7605
|
+
function conflictResolutionRule() {
|
|
7606
|
+
return "- **This session resolves a merge conflict, so it DOES write code here.** Edit the " + "conflicted files in this checkout, resolving them from the feature docs\u2019 intent, and " + "commit the merge. That is the exception and its whole extent: the edit guard exempts " + "writes only while the merge is in progress, so work the merge revealed but did not " + "cause still rides a ticket.";
|
|
7607
|
+
}
|
|
7608
|
+
function renderSystemPrompt(feature, kind, waypoint, lap, purpose) {
|
|
7136
7609
|
if (kind === "waypoint")
|
|
7137
7610
|
return renderWaypointPrompt(feature, waypoint);
|
|
7138
7611
|
if (kind === "converge")
|
|
7139
7612
|
return renderConvergePrompt(feature);
|
|
7140
7613
|
if (kind === "revisit")
|
|
7141
|
-
return renderRevisitPrompt(feature, lap);
|
|
7614
|
+
return renderRevisitPrompt(feature, lap, purpose);
|
|
7142
7615
|
const docs = featureDocsRel(feature.slug);
|
|
7143
7616
|
const entry = kind === "ideation" ? "Begin by invoking the `/runcastle:ideate` skill and drive the ideation session to completion." : "This is a Q&A session: invoke `/runcastle:qa`. Answer questions from the docs + code. Do NOT advance phases or emit tickets.";
|
|
7144
7617
|
return [
|
|
@@ -7267,7 +7740,7 @@ function renderConvergePrompt(feature) {
|
|
|
7267
7740
|
].join(`
|
|
7268
7741
|
`);
|
|
7269
7742
|
}
|
|
7270
|
-
function renderRevisitPrompt(feature, lap) {
|
|
7743
|
+
function renderRevisitPrompt(feature, lap, purpose) {
|
|
7271
7744
|
const docs = featureDocsRel(feature.slug);
|
|
7272
7745
|
const lapIteration = lap ? [
|
|
7273
7746
|
`## This is lap ${lap}`,
|
|
@@ -7337,7 +7810,7 @@ function renderRevisitPrompt(feature, lap) {
|
|
|
7337
7810
|
lap ? "- DO call `complete_phase` \u2014 this lap advances ideation \u2192 spec \u2192 tickets, and only you can." : "- Do NOT call `complete_phase` \u2014 a revisit never moves the pipeline.",
|
|
7338
7811
|
"- Do NOT touch `done`/`burning` tickets; if done work is now wrong, emit a new ticket that fixes it.",
|
|
7339
7812
|
"- Docs first, tickets second: capture the decision prose before any ticket surgery.",
|
|
7340
|
-
noCodeRule(docs),
|
|
7813
|
+
purpose === "resolve-conflict" ? conflictResolutionRule() : noCodeRule(docs),
|
|
7341
7814
|
"",
|
|
7342
7815
|
"## Your task",
|
|
7343
7816
|
"Invoke the `/runcastle:revisit` skill and work through what the human brings up.",
|
|
@@ -7495,7 +7968,8 @@ function renderProjectPrompt(brief) {
|
|
|
7495
7968
|
"- `get_project_context()` \u2014 the project, its charter, its live ADRs, and a one-line",
|
|
7496
7969
|
" index of every feature.",
|
|
7497
7970
|
"- `get_work_record({ featureSlug? | seam? })` \u2014 what features actually did: tickets by",
|
|
7498
|
-
" status, seams, commits, errors
|
|
7971
|
+
" status, seams, commits, errors, and each burner's digest of what it actually did,",
|
|
7972
|
+
" what surprised it and what it left undone. Facts, never intent.",
|
|
7499
7973
|
"- `record_event({ type, message })` \u2014 drop a note on the project timeline.",
|
|
7500
7974
|
"",
|
|
7501
7975
|
"Every merged feature's docs are already on disk in this worktree \u2014 read them with your",
|
|
@@ -7544,18 +8018,18 @@ function renderMcpConfig(session, config2) {
|
|
|
7544
8018
|
};
|
|
7545
8019
|
}
|
|
7546
8020
|
async function writeSessionArtifacts(input) {
|
|
7547
|
-
const { session, feature, config: config2, waypoint, prepare, projectBrief, lap } = input;
|
|
8021
|
+
const { session, feature, config: config2, waypoint, prepare, projectBrief, lap, purpose } = input;
|
|
7548
8022
|
const dir = sessionDir(session.id);
|
|
7549
|
-
|
|
7550
|
-
const systemPromptPath =
|
|
7551
|
-
const settingsPath =
|
|
7552
|
-
const mcpConfigPath =
|
|
7553
|
-
const systemPrompt = feature ? renderSystemPrompt(feature, session.kind, waypoint, lap) : prepare ? renderPreparePrompt(prepare) : projectBrief ? renderProjectPrompt(projectBrief) : (() => {
|
|
8023
|
+
mkdirSync7(dir, { recursive: true });
|
|
8024
|
+
const systemPromptPath = join13(dir, "system-prompt.md");
|
|
8025
|
+
const settingsPath = join13(dir, "settings.json");
|
|
8026
|
+
const mcpConfigPath = join13(dir, "mcp.json");
|
|
8027
|
+
const systemPrompt = feature ? renderSystemPrompt(feature, session.kind, waypoint, lap, purpose) : prepare ? renderPreparePrompt(prepare) : projectBrief ? renderProjectPrompt(projectBrief) : (() => {
|
|
7554
8028
|
throw new Error(`session ${session.id} has no feature and no project-session brief`);
|
|
7555
8029
|
})();
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
8030
|
+
writeFileSync3(systemPromptPath, systemPrompt, "utf8");
|
|
8031
|
+
writeFileSync3(settingsPath, JSON.stringify(renderSettings(hookClientPath(), session.kind), null, 2), "utf8");
|
|
8032
|
+
writeFileSync3(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
|
|
7559
8033
|
return { systemPromptPath, settingsPath, mcpConfigPath };
|
|
7560
8034
|
}
|
|
7561
8035
|
var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_READ_RULES, SESSION_BASH_WRITE_RULES, SESSION_BASH_ALLOW_RULES, SESSION_START_SOURCES;
|
|
@@ -7599,9 +8073,9 @@ var init_artifacts = __esm(() => {
|
|
|
7599
8073
|
});
|
|
7600
8074
|
|
|
7601
8075
|
// src/launcher/launcher.ts
|
|
7602
|
-
import { existsSync as
|
|
7603
|
-
import { dirname as
|
|
7604
|
-
import { fileURLToPath as
|
|
8076
|
+
import { existsSync as existsSync14 } from "fs";
|
|
8077
|
+
import { dirname as dirname7, join as join14, resolve as resolve9 } from "path";
|
|
8078
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
7605
8079
|
import { and as and9, eq as eq11 } from "drizzle-orm";
|
|
7606
8080
|
function ptyExitMessage(exitCode) {
|
|
7607
8081
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
@@ -7633,23 +8107,23 @@ function resolveClaudeExecutable() {
|
|
|
7633
8107
|
function claudeSpawnTarget(claudeArgs) {
|
|
7634
8108
|
return spawnTargetFor(resolveClaudeExecutable(), claudeArgs);
|
|
7635
8109
|
}
|
|
7636
|
-
function resolvePluginDir(fromDir =
|
|
7637
|
-
const rel =
|
|
8110
|
+
function resolvePluginDir(fromDir = dirname7(fileURLToPath6(import.meta.url))) {
|
|
8111
|
+
const rel = join14("packages", "skills", "packs", "runcastle");
|
|
7638
8112
|
const override = process.env[SKILLS_DIR_ENV];
|
|
7639
8113
|
if (override) {
|
|
7640
|
-
const dir2 =
|
|
7641
|
-
if (
|
|
8114
|
+
const dir2 = join14(resolve9(override), "packs", "runcastle");
|
|
8115
|
+
if (existsSync14(dir2))
|
|
7642
8116
|
return dir2;
|
|
7643
8117
|
throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
|
|
7644
8118
|
}
|
|
7645
8119
|
const searched = [];
|
|
7646
8120
|
let dir = fromDir;
|
|
7647
8121
|
for (let i = 0;i < 8; i += 1) {
|
|
7648
|
-
const candidate =
|
|
8122
|
+
const candidate = join14(dir, rel);
|
|
7649
8123
|
searched.push(candidate);
|
|
7650
|
-
if (
|
|
8124
|
+
if (existsSync14(candidate))
|
|
7651
8125
|
return candidate;
|
|
7652
|
-
const parent =
|
|
8126
|
+
const parent = dirname7(dir);
|
|
7653
8127
|
if (parent === dir)
|
|
7654
8128
|
break;
|
|
7655
8129
|
dir = parent;
|
|
@@ -7673,8 +8147,10 @@ function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
|
7673
8147
|
}
|
|
7674
8148
|
}
|
|
7675
8149
|
function sessionFinished(ctx, feature, session) {
|
|
7676
|
-
if (session.kind
|
|
8150
|
+
if (session.kind === "ideation" || session.kind === "converge")
|
|
7677
8151
|
return feature.mapped;
|
|
8152
|
+
if (session.kind !== "waypoint")
|
|
8153
|
+
return false;
|
|
7678
8154
|
const own = listByFeature(ctx, feature.id).find((w) => w.lastSessionId === session.id);
|
|
7679
8155
|
return !!own && (own.status === "resolved" || own.status === "dropped");
|
|
7680
8156
|
}
|
|
@@ -7713,11 +8189,14 @@ async function ensureWorktree(ctx, project, feature) {
|
|
|
7713
8189
|
}
|
|
7714
8190
|
async function launchSession(ctx, input, opts = {}) {
|
|
7715
8191
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
8192
|
+
requireNotDraft(feature);
|
|
7716
8193
|
const project = projectForFeature(ctx, feature);
|
|
7717
8194
|
const worktreePath = await ensureWorktree(ctx, project, feature);
|
|
7718
8195
|
const session = createSessionRow(ctx, {
|
|
7719
8196
|
featureId: feature.id,
|
|
7720
8197
|
kind: input.kind,
|
|
8198
|
+
purpose: input.purpose,
|
|
8199
|
+
purposeData: input.purposeData,
|
|
7721
8200
|
worktreePath
|
|
7722
8201
|
});
|
|
7723
8202
|
const plan = planKickoff({ kind: input.kind, lap: feature.lap, kickoffLine: input.kickoffLine });
|
|
@@ -7793,7 +8272,8 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
7793
8272
|
project,
|
|
7794
8273
|
config: ctx.config,
|
|
7795
8274
|
waypoint,
|
|
7796
|
-
lap: plan.lap
|
|
8275
|
+
lap: plan.lap,
|
|
8276
|
+
purpose: input.purpose
|
|
7797
8277
|
});
|
|
7798
8278
|
const serverUrl = serverUrlFor(ctx.config);
|
|
7799
8279
|
const buildInput = {
|
|
@@ -7893,7 +8373,7 @@ async function launchProjectSession(ctx, input, opts = {}) {
|
|
|
7893
8373
|
if (live) {
|
|
7894
8374
|
throw new GateError(`a project session is already open for ${project.name} \u2014 resume or end it first`);
|
|
7895
8375
|
}
|
|
7896
|
-
const worktreePath = await ensureProjectWorktree(project);
|
|
8376
|
+
const worktreePath = await ensureProjectWorktree(project, (res) => reportProjectLanding(ctx, project, res, { retried: true }));
|
|
7897
8377
|
const session = createSessionRow(ctx, {
|
|
7898
8378
|
projectId: project.id,
|
|
7899
8379
|
kind: "project",
|
|
@@ -7964,6 +8444,7 @@ async function buildPrepareBrief(ctx, project) {
|
|
|
7964
8444
|
}
|
|
7965
8445
|
async function workWaypoint(ctx, input, opts = {}) {
|
|
7966
8446
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
8447
|
+
requireNotDraft(feature);
|
|
7967
8448
|
if (!feature.mapped) {
|
|
7968
8449
|
throw new GateError(`feature ${feature.slug} is not mapped \u2014 it has no waypoints to work`);
|
|
7969
8450
|
}
|
|
@@ -7984,6 +8465,7 @@ async function workWaypoint(ctx, input, opts = {}) {
|
|
|
7984
8465
|
}
|
|
7985
8466
|
async function converge(ctx, input, opts = {}) {
|
|
7986
8467
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
8468
|
+
requireNotDraft(feature);
|
|
7987
8469
|
if (!feature.mapped) {
|
|
7988
8470
|
throw new GateError(`feature ${feature.slug} is not mapped \u2014 convergence is only for mapped features`);
|
|
7989
8471
|
}
|
|
@@ -8082,6 +8564,8 @@ function spawnEmbeddedPty(ctx, feature, session, worktreePath, serverUrl, claude
|
|
|
8082
8564
|
opts: { cwd: worktreePath, env, cols: 80, rows: 24, useConpty: true },
|
|
8083
8565
|
onExit: ({ exitCode }) => handlePtyExit(ctx, feature, session, meta, exitCode)
|
|
8084
8566
|
});
|
|
8567
|
+
if (feature)
|
|
8568
|
+
startDocsWatch(ctx, feature);
|
|
8085
8569
|
emitForSession(ctx, session, {
|
|
8086
8570
|
type: "session.launched",
|
|
8087
8571
|
message: "embedded terminal spawned",
|
|
@@ -8108,10 +8592,12 @@ var init_launcher = __esm(() => {
|
|
|
8108
8592
|
init_errors();
|
|
8109
8593
|
init_end_session();
|
|
8110
8594
|
init_registry();
|
|
8595
|
+
init_docs_watch();
|
|
8111
8596
|
init_events();
|
|
8112
8597
|
init_gates();
|
|
8113
8598
|
init_git();
|
|
8114
8599
|
init_repo();
|
|
8600
|
+
init_features();
|
|
8115
8601
|
init_tickets();
|
|
8116
8602
|
init_waypoints();
|
|
8117
8603
|
init_runner();
|
|
@@ -8133,6 +8619,74 @@ var init_launcher = __esm(() => {
|
|
|
8133
8619
|
];
|
|
8134
8620
|
});
|
|
8135
8621
|
|
|
8622
|
+
// src/services/outcome.ts
|
|
8623
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
8624
|
+
import { join as join15 } from "path";
|
|
8625
|
+
function composeOutcomeDoc(feature, tickets2, shippedAt) {
|
|
8626
|
+
const blocks = [
|
|
8627
|
+
`# Outcome \u2014 ${feature.title}`,
|
|
8628
|
+
feature.oneLiner,
|
|
8629
|
+
[`- Shipped: ${shippedDate(shippedAt)}`, `- Lap: ${feature.lap}`].join(`
|
|
8630
|
+
`)
|
|
8631
|
+
];
|
|
8632
|
+
let oneLiners = [];
|
|
8633
|
+
const flush = () => {
|
|
8634
|
+
if (oneLiners.length > 0)
|
|
8635
|
+
blocks.push(oneLiners.join(`
|
|
8636
|
+
`));
|
|
8637
|
+
oneLiners = [];
|
|
8638
|
+
};
|
|
8639
|
+
for (const ticket of [...tickets2].sort((a, b) => a.seq - b.seq)) {
|
|
8640
|
+
if (ticket.status === "done") {
|
|
8641
|
+
flush();
|
|
8642
|
+
blocks.push(`## ${ticket.seq}. ${ticket.title}`, ticket.digest?.trim() || NO_DIGEST);
|
|
8643
|
+
} else {
|
|
8644
|
+
oneLiners.push(oneLineEntry(ticket));
|
|
8645
|
+
}
|
|
8646
|
+
}
|
|
8647
|
+
flush();
|
|
8648
|
+
return `${blocks.join(`
|
|
8649
|
+
|
|
8650
|
+
`)}
|
|
8651
|
+
`;
|
|
8652
|
+
}
|
|
8653
|
+
function shippedDate(shippedAt) {
|
|
8654
|
+
return new Date(shippedAt).toISOString().slice(0, 10);
|
|
8655
|
+
}
|
|
8656
|
+
function oneLineEntry(ticket) {
|
|
8657
|
+
const headline = errorHeadline3(ticket.error ?? "");
|
|
8658
|
+
return `- **${ticket.seq}. ${ticket.title}** \u2014 ${ticket.status}${headline ? `: ${headline}` : ""}`;
|
|
8659
|
+
}
|
|
8660
|
+
function errorHeadline3(error) {
|
|
8661
|
+
const lines = error.split(`
|
|
8662
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
8663
|
+
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
8664
|
+
const headline = causes.at(-1) ?? lines[0] ?? "";
|
|
8665
|
+
return headline.length > HEADLINE_MAX ? `${headline.slice(0, HEADLINE_MAX)}\u2026` : headline;
|
|
8666
|
+
}
|
|
8667
|
+
async function promoteOutcomeDoc(ctx, project, feature) {
|
|
8668
|
+
try {
|
|
8669
|
+
const worktreePath = await ensureTalkWorktree(project, feature);
|
|
8670
|
+
const docsDir = join15(worktreePath, ...featureDocsRel(feature.slug).split("/"));
|
|
8671
|
+
mkdirSync8(docsDir, { recursive: true });
|
|
8672
|
+
const doc = composeOutcomeDoc(feature, listByFeature2(ctx, feature.id), Date.now());
|
|
8673
|
+
writeFileSync4(join15(docsDir, "outcome.md"), doc, "utf8");
|
|
8674
|
+
await commitDocs(worktreePath, `runcastle: outcome for ${feature.slug}`);
|
|
8675
|
+
} catch (e) {
|
|
8676
|
+
emit(ctx, feature.id, {
|
|
8677
|
+
type: "docs.outcome_failed",
|
|
8678
|
+
message: `outcome.md not promoted: ${e instanceof Error ? e.message : String(e)}`
|
|
8679
|
+
});
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8682
|
+
var NO_DIGEST = "_no digest captured_", HEADLINE_MAX = 160;
|
|
8683
|
+
var init_outcome = __esm(() => {
|
|
8684
|
+
init_paths();
|
|
8685
|
+
init_events();
|
|
8686
|
+
init_git();
|
|
8687
|
+
init_tickets();
|
|
8688
|
+
});
|
|
8689
|
+
|
|
8136
8690
|
// src/trpc/routers/feature.ts
|
|
8137
8691
|
import * as z6 from "zod";
|
|
8138
8692
|
var gateId, featureRouter;
|
|
@@ -8144,6 +8698,7 @@ var init_feature = __esm(() => {
|
|
|
8144
8698
|
init_features();
|
|
8145
8699
|
init_gates();
|
|
8146
8700
|
init_git();
|
|
8701
|
+
init_outcome();
|
|
8147
8702
|
init_repo();
|
|
8148
8703
|
init_context();
|
|
8149
8704
|
gateId = z6.enum(["G1", "G2", "G3", "G4", "G5"]);
|
|
@@ -8152,8 +8707,10 @@ var init_feature = __esm(() => {
|
|
|
8152
8707
|
projectId: z6.string(),
|
|
8153
8708
|
title: z6.string().min(1),
|
|
8154
8709
|
oneLiner: z6.string(),
|
|
8155
|
-
baseBranch: z6.string().optional()
|
|
8710
|
+
baseBranch: z6.string().optional(),
|
|
8711
|
+
draft: z6.boolean().optional()
|
|
8156
8712
|
})).mutation(({ ctx, input }) => createFeature(ctx, input)),
|
|
8713
|
+
start: publicProcedure.input(z6.object({ featureId: z6.string(), baseBranch: z6.string().optional() })).mutation(({ ctx, input }) => startDraft(ctx, input.featureId, { baseBranch: input.baseBranch })),
|
|
8157
8714
|
quickChange: publicProcedure.input(z6.object({
|
|
8158
8715
|
projectId: z6.string(),
|
|
8159
8716
|
title: z6.string().min(1),
|
|
@@ -8165,7 +8722,9 @@ var init_feature = __esm(() => {
|
|
|
8165
8722
|
launchSession: publicProcedure.input(z6.object({
|
|
8166
8723
|
featureId: z6.string(),
|
|
8167
8724
|
kind: SessionKind,
|
|
8168
|
-
kickoffLine: z6.string().min(1).optional()
|
|
8725
|
+
kickoffLine: z6.string().min(1).optional(),
|
|
8726
|
+
purpose: SessionPurpose.optional(),
|
|
8727
|
+
purposeData: MergeBranchPair.optional()
|
|
8169
8728
|
})).mutation(({ ctx, input }) => launchSession(ctx, input)),
|
|
8170
8729
|
workWaypoint: publicProcedure.input(z6.object({ featureId: z6.string(), waypointId: z6.string(), endLive: z6.boolean().optional() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
|
|
8171
8730
|
converge: publicProcedure.input(z6.object({ featureId: z6.string(), overrideReason: z6.string().min(1).optional() })).mutation(({ ctx, input }) => converge(ctx, { featureId: input.featureId, overrideReason: input.overrideReason })),
|
|
@@ -8185,6 +8744,7 @@ var init_feature = __esm(() => {
|
|
|
8185
8744
|
burn: publicProcedure.input(z6.object({ featureId: z6.string(), model: z6.string().min(1).optional() })).mutation(({ ctx, input }) => burn(ctx, input.featureId, { modelOverride: input.model })),
|
|
8186
8745
|
testDrive: publicProcedure.input(z6.object({ featureId: z6.string(), action: z6.enum(["start", "stop"]) })).mutation(async ({ ctx, input }) => {
|
|
8187
8746
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
8747
|
+
requireNotDraft(feature);
|
|
8188
8748
|
const project = projectForFeature(ctx, feature);
|
|
8189
8749
|
return testDrive(ctx, project, feature, input.action);
|
|
8190
8750
|
}),
|
|
@@ -8195,10 +8755,12 @@ var init_feature = __esm(() => {
|
|
|
8195
8755
|
}),
|
|
8196
8756
|
merge: publicProcedure.input(z6.object({ featureId: z6.string() })).mutation(async ({ ctx, input }) => {
|
|
8197
8757
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
8758
|
+
requireNotDraft(feature);
|
|
8198
8759
|
const project = projectForFeature(ctx, feature);
|
|
8199
8760
|
if (activeTestDriveFeatureId() === feature.id) {
|
|
8200
8761
|
await testDrive(ctx, project, feature, "stop");
|
|
8201
8762
|
}
|
|
8763
|
+
await promoteOutcomeDoc(ctx, project, feature);
|
|
8202
8764
|
const res = await mergeFeature(project, feature);
|
|
8203
8765
|
if (res.ok) {
|
|
8204
8766
|
setPhase(ctx, input.featureId, "shipped", "feature.shipped", `merged to ${res.target}`);
|
|
@@ -8216,9 +8778,9 @@ var init_feature = __esm(() => {
|
|
|
8216
8778
|
});
|
|
8217
8779
|
|
|
8218
8780
|
// src/services/fsbrowse.ts
|
|
8219
|
-
import { existsSync as
|
|
8781
|
+
import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
|
|
8220
8782
|
import { homedir as homedir2 } from "os";
|
|
8221
|
-
import { basename, dirname as
|
|
8783
|
+
import { basename, dirname as dirname8, isAbsolute as isAbsolute2, join as join16, resolve as resolve10, sep as sep3 } from "path";
|
|
8222
8784
|
function expandPath(input, home = homedir2()) {
|
|
8223
8785
|
const trimmed = input.trim();
|
|
8224
8786
|
if (trimmed === "")
|
|
@@ -8227,12 +8789,12 @@ function expandPath(input, home = homedir2()) {
|
|
|
8227
8789
|
if (out === "~")
|
|
8228
8790
|
out = home;
|
|
8229
8791
|
else if (out.startsWith("~/") || out.startsWith("~\\"))
|
|
8230
|
-
out =
|
|
8792
|
+
out = join16(home, out.slice(2));
|
|
8231
8793
|
return resolve10(out);
|
|
8232
8794
|
}
|
|
8233
8795
|
function looksLikeRepo(dir) {
|
|
8234
8796
|
try {
|
|
8235
|
-
return
|
|
8797
|
+
return existsSync15(join16(dir, ".git"));
|
|
8236
8798
|
} catch {
|
|
8237
8799
|
return false;
|
|
8238
8800
|
}
|
|
@@ -8243,20 +8805,20 @@ function listRoots(home = homedir2()) {
|
|
|
8243
8805
|
for (let code = 67;code <= 90; code++) {
|
|
8244
8806
|
const drive = `${String.fromCharCode(code)}:${sep3}`;
|
|
8245
8807
|
try {
|
|
8246
|
-
if (
|
|
8808
|
+
if (existsSync15(drive))
|
|
8247
8809
|
roots.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
|
|
8248
8810
|
} catch {}
|
|
8249
8811
|
}
|
|
8250
8812
|
} else {
|
|
8251
8813
|
roots.push({ label: "/", path: "/", kind: "drive" });
|
|
8252
8814
|
}
|
|
8253
|
-
if (
|
|
8815
|
+
if (existsSync15(home))
|
|
8254
8816
|
roots.push({ label: "Home", path: home, kind: "home" });
|
|
8255
8817
|
const seen = new Set(roots.map(pathKey));
|
|
8256
8818
|
for (const name of COMMON_DIRS) {
|
|
8257
|
-
const candidate =
|
|
8819
|
+
const candidate = join16(home, name);
|
|
8258
8820
|
const key = pathKey({ path: candidate });
|
|
8259
|
-
if (seen.has(key) || !
|
|
8821
|
+
if (seen.has(key) || !existsSync15(candidate))
|
|
8260
8822
|
continue;
|
|
8261
8823
|
seen.add(key);
|
|
8262
8824
|
roots.push({ label: name, path: candidate, kind: "common" });
|
|
@@ -8271,7 +8833,7 @@ function crumbsFor(dir) {
|
|
|
8271
8833
|
let cur = dir;
|
|
8272
8834
|
for (;; ) {
|
|
8273
8835
|
out.unshift({ name: basename(cur) || cur, path: cur });
|
|
8274
|
-
const parent =
|
|
8836
|
+
const parent = dirname8(cur);
|
|
8275
8837
|
if (parent === cur)
|
|
8276
8838
|
break;
|
|
8277
8839
|
cur = parent;
|
|
@@ -8279,7 +8841,7 @@ function crumbsFor(dir) {
|
|
|
8279
8841
|
return out;
|
|
8280
8842
|
}
|
|
8281
8843
|
function parentOf(dir) {
|
|
8282
|
-
const parent =
|
|
8844
|
+
const parent = dirname8(dir);
|
|
8283
8845
|
return parent === dir ? null : parent;
|
|
8284
8846
|
}
|
|
8285
8847
|
function browseDir(input, showHidden = false, home = homedir2()) {
|
|
@@ -8287,13 +8849,13 @@ function browseDir(input, showHidden = false, home = homedir2()) {
|
|
|
8287
8849
|
if (!isAbsolute2(target)) {
|
|
8288
8850
|
throw new InvalidInputError(`path is not absolute: ${target}`);
|
|
8289
8851
|
}
|
|
8290
|
-
if (!
|
|
8852
|
+
if (!existsSync15(target)) {
|
|
8291
8853
|
throw new InvalidInputError(`path does not exist: ${target}`);
|
|
8292
8854
|
}
|
|
8293
8855
|
let dir = target;
|
|
8294
8856
|
try {
|
|
8295
8857
|
if (!statSync2(target).isDirectory())
|
|
8296
|
-
dir =
|
|
8858
|
+
dir = dirname8(target);
|
|
8297
8859
|
} catch (e) {
|
|
8298
8860
|
throw new InvalidInputError(`cannot read path: ${target} (${errMsg4(e)})`);
|
|
8299
8861
|
}
|
|
@@ -8315,7 +8877,7 @@ function browseDir(input, showHidden = false, home = homedir2()) {
|
|
|
8315
8877
|
continue;
|
|
8316
8878
|
if (!showHidden && name.startsWith("."))
|
|
8317
8879
|
continue;
|
|
8318
|
-
const path =
|
|
8880
|
+
const path = join16(dir, name);
|
|
8319
8881
|
const isSymlink = dirent.isSymbolicLink();
|
|
8320
8882
|
if (isSymlink && !showHidden)
|
|
8321
8883
|
continue;
|
|
@@ -8352,8 +8914,8 @@ var init_fsbrowse = __esm(() => {
|
|
|
8352
8914
|
});
|
|
8353
8915
|
|
|
8354
8916
|
// src/services/projects.ts
|
|
8355
|
-
import { existsSync as
|
|
8356
|
-
import { basename as basename2, join as
|
|
8917
|
+
import { existsSync as existsSync16 } from "fs";
|
|
8918
|
+
import { basename as basename2, join as join17 } from "path";
|
|
8357
8919
|
import { eq as eq12, isNull } from "drizzle-orm";
|
|
8358
8920
|
function listProjects(ctx) {
|
|
8359
8921
|
return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
|
|
@@ -8377,7 +8939,7 @@ async function openProject(ctx, rawPath) {
|
|
|
8377
8939
|
const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
|
|
8378
8940
|
if (existing) {
|
|
8379
8941
|
ctx.db.update(projects).set({ mainBranch, repoPath, closedAt: null }).where(eq12(projects.id, existing.id)).run();
|
|
8380
|
-
const reopened = { ...existing, mainBranch, repoPath };
|
|
8942
|
+
const reopened = { ...existing, mainBranch, repoPath, closedAt: undefined };
|
|
8381
8943
|
emitProject(ctx, existing.id, {
|
|
8382
8944
|
type: "project.opened",
|
|
8383
8945
|
message: `project ${existing.name} re-opened at ${repoPath} (${mainBranch})`
|
|
@@ -8434,7 +8996,7 @@ async function assertRepoTolerant(ctx, repoPath) {
|
|
|
8434
8996
|
} catch (e) {
|
|
8435
8997
|
if (!isNotImplemented(e))
|
|
8436
8998
|
throw e;
|
|
8437
|
-
if (!
|
|
8999
|
+
if (!existsSync16(join17(repoPath, ".git"))) {
|
|
8438
9000
|
throw new InvalidInputError(`not a git repository: ${repoPath}`);
|
|
8439
9001
|
}
|
|
8440
9002
|
}
|
|
@@ -8499,7 +9061,8 @@ var init_run = __esm(() => {
|
|
|
8499
9061
|
runRouter = router({
|
|
8500
9062
|
get: publicProcedure.input(z8.object({ runId: z8.string() })).query(({ ctx, input }) => getRunRow(ctx, input.runId)),
|
|
8501
9063
|
agentTranscript: publicProcedure.input(z8.object({ ticketId: z8.string(), after: z8.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
|
|
8502
|
-
cancel: publicProcedure.input(z8.object({ runId: z8.string() })).mutation(({ input }) => {
|
|
9064
|
+
cancel: publicProcedure.input(z8.object({ runId: z8.string() })).mutation(({ ctx, input }) => {
|
|
9065
|
+
getRunRow(ctx, input.runId);
|
|
8503
9066
|
cancelRun(input.runId);
|
|
8504
9067
|
return { ok: true };
|
|
8505
9068
|
})
|
|
@@ -8507,15 +9070,15 @@ var init_run = __esm(() => {
|
|
|
8507
9070
|
});
|
|
8508
9071
|
|
|
8509
9072
|
// src/services/settings.ts
|
|
8510
|
-
import { existsSync as
|
|
8511
|
-
import { dirname as
|
|
9073
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
9074
|
+
import { dirname as dirname9 } from "path";
|
|
8512
9075
|
import * as z9 from "zod";
|
|
8513
9076
|
import { eq as eq13 } from "drizzle-orm";
|
|
8514
9077
|
function readRawConfig(configFile) {
|
|
8515
|
-
if (!
|
|
9078
|
+
if (!existsSync17(configFile))
|
|
8516
9079
|
return {};
|
|
8517
9080
|
try {
|
|
8518
|
-
const parsed = JSON.parse(
|
|
9081
|
+
const parsed = JSON.parse(readFileSync8(configFile, "utf8"));
|
|
8519
9082
|
if (typeof parsed !== "object" || parsed === null)
|
|
8520
9083
|
return {};
|
|
8521
9084
|
return foldLegacyModelConfig(parsed);
|
|
@@ -8654,8 +9217,8 @@ function updateSettings(ctx, input, io = {}) {
|
|
|
8654
9217
|
function writeGlobal(configFile, configKey, value) {
|
|
8655
9218
|
const raw = readRawConfig(configFile);
|
|
8656
9219
|
raw[configKey] = value;
|
|
8657
|
-
|
|
8658
|
-
|
|
9220
|
+
mkdirSync9(dirname9(configFile), { recursive: true });
|
|
9221
|
+
writeFileSync5(configFile, `${JSON.stringify(raw, null, 2)}
|
|
8659
9222
|
`);
|
|
8660
9223
|
}
|
|
8661
9224
|
function updateStepModel(ctx, input, configFile, io) {
|
|
@@ -8693,8 +9256,8 @@ function writeStepModel(configFile, step, value) {
|
|
|
8693
9256
|
else
|
|
8694
9257
|
stepModels[step] = value;
|
|
8695
9258
|
raw.stepModels = stepModels;
|
|
8696
|
-
|
|
8697
|
-
|
|
9259
|
+
mkdirSync9(dirname9(configFile), { recursive: true });
|
|
9260
|
+
writeFileSync5(configFile, `${JSON.stringify(raw, null, 2)}
|
|
8698
9261
|
`);
|
|
8699
9262
|
}
|
|
8700
9263
|
function field(view, key) {
|
|
@@ -8931,11 +9494,11 @@ async function gitIdentityProbe(exec, cwd) {
|
|
|
8931
9494
|
async function containerRuntimeProbe(exec) {
|
|
8932
9495
|
const id = "container-runtime";
|
|
8933
9496
|
const label = "Container runtime (Docker / Podman)";
|
|
8934
|
-
const
|
|
8935
|
-
if (
|
|
9497
|
+
const docker2 = await exec("docker", ["--version"]);
|
|
9498
|
+
if (docker2.ok && docker2.code === 0) {
|
|
8936
9499
|
const info = await exec("docker", ["info"]);
|
|
8937
9500
|
if (info.ok && info.code === 0) {
|
|
8938
|
-
return { id, label, tier: 2, status: "ok", detail:
|
|
9501
|
+
return { id, label, tier: 2, status: "ok", detail: docker2.stdout.trim() || "docker healthy" };
|
|
8939
9502
|
}
|
|
8940
9503
|
return {
|
|
8941
9504
|
id,
|
|
@@ -9071,9 +9634,9 @@ var init_system_exec = __esm(() => {
|
|
|
9071
9634
|
});
|
|
9072
9635
|
|
|
9073
9636
|
// src/services/setup.ts
|
|
9074
|
-
import { copyFileSync, existsSync as
|
|
9075
|
-
import { dirname as
|
|
9076
|
-
import { fileURLToPath as
|
|
9637
|
+
import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync9, readdirSync as readdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
9638
|
+
import { dirname as dirname10, join as join18 } from "path";
|
|
9639
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
9077
9640
|
async function writeGitIdentity(exec, identity) {
|
|
9078
9641
|
const name = identity.name.trim();
|
|
9079
9642
|
const email = identity.email.trim();
|
|
@@ -9148,16 +9711,16 @@ async function resolveRuntime(exec, preferred) {
|
|
|
9148
9711
|
function resolveSandcastleBin() {
|
|
9149
9712
|
try {
|
|
9150
9713
|
const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
|
|
9151
|
-
let dir =
|
|
9714
|
+
let dir = dirname10(fileURLToPath7(mainUrl));
|
|
9152
9715
|
for (let hops = 0;hops < 6; hops++) {
|
|
9153
|
-
const manifestPath =
|
|
9154
|
-
if (
|
|
9155
|
-
const manifest = JSON.parse(
|
|
9716
|
+
const manifestPath = join18(dir, "package.json");
|
|
9717
|
+
if (existsSync18(manifestPath)) {
|
|
9718
|
+
const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
|
|
9156
9719
|
if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
|
|
9157
|
-
return
|
|
9720
|
+
return join18(dir, manifest.bin.sandcastle);
|
|
9158
9721
|
}
|
|
9159
9722
|
}
|
|
9160
|
-
const parent =
|
|
9723
|
+
const parent = dirname10(dir);
|
|
9161
9724
|
if (parent === dir)
|
|
9162
9725
|
break;
|
|
9163
9726
|
dir = parent;
|
|
@@ -9179,21 +9742,21 @@ function terminalSpec(kind, opts) {
|
|
|
9179
9742
|
};
|
|
9180
9743
|
}
|
|
9181
9744
|
function sandcastleTemplateDir() {
|
|
9182
|
-
return resolveAsset(ASSET_ENV.sandcastleTemplate,
|
|
9745
|
+
return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath7(new URL("../assets/sandcastle", import.meta.url)));
|
|
9183
9746
|
}
|
|
9184
9747
|
function scaffoldSandcastleConfig(templateDir, targetDir) {
|
|
9185
|
-
const dir =
|
|
9186
|
-
if (
|
|
9748
|
+
const dir = join18(targetDir, ".sandcastle");
|
|
9749
|
+
if (existsSync18(dir))
|
|
9187
9750
|
return { scaffolded: false, dir };
|
|
9188
|
-
|
|
9751
|
+
mkdirSync10(dir, { recursive: true });
|
|
9189
9752
|
for (const name of readdirSync6(templateDir)) {
|
|
9190
|
-
copyFileSync(
|
|
9753
|
+
copyFileSync(join18(templateDir, name), join18(dir, name));
|
|
9191
9754
|
}
|
|
9192
9755
|
return { scaffolded: true, dir };
|
|
9193
9756
|
}
|
|
9194
9757
|
function prepareSandboxBuildContext() {
|
|
9195
9758
|
const target = sandboxBuildDir();
|
|
9196
|
-
|
|
9759
|
+
mkdirSync10(target, { recursive: true });
|
|
9197
9760
|
scaffoldSandcastleConfig(sandcastleTemplateDir(), target);
|
|
9198
9761
|
return target;
|
|
9199
9762
|
}
|
|
@@ -9229,10 +9792,10 @@ function createTokenVerifier(exec) {
|
|
|
9229
9792
|
function fileAfkTokenIo(exec) {
|
|
9230
9793
|
const path = envPath();
|
|
9231
9794
|
return {
|
|
9232
|
-
read: () =>
|
|
9795
|
+
read: () => existsSync18(path) ? readFileSync9(path, "utf8") : "",
|
|
9233
9796
|
write: (content) => {
|
|
9234
|
-
|
|
9235
|
-
|
|
9797
|
+
mkdirSync10(dirname10(path), { recursive: true });
|
|
9798
|
+
writeFileSync6(path, content, "utf8");
|
|
9236
9799
|
},
|
|
9237
9800
|
verify: createTokenVerifier(exec)
|
|
9238
9801
|
};
|
|
@@ -9285,110 +9848,6 @@ var init_setup2 = __esm(() => {
|
|
|
9285
9848
|
});
|
|
9286
9849
|
});
|
|
9287
9850
|
|
|
9288
|
-
// src/version.ts
|
|
9289
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
9290
|
-
import { dirname as dirname9, join as join17 } from "path";
|
|
9291
|
-
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
9292
|
-
function runcastleVersion() {
|
|
9293
|
-
if (cached2)
|
|
9294
|
-
return cached2;
|
|
9295
|
-
const here = dirname9(fileURLToPath7(import.meta.url));
|
|
9296
|
-
for (const pkgPath of [join17(here, "..", "package.json"), join17(here, "package.json")]) {
|
|
9297
|
-
try {
|
|
9298
|
-
const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
|
|
9299
|
-
if (typeof pkg.version === "string") {
|
|
9300
|
-
cached2 = pkg.version;
|
|
9301
|
-
return cached2;
|
|
9302
|
-
}
|
|
9303
|
-
} catch {}
|
|
9304
|
-
}
|
|
9305
|
-
cached2 = UNKNOWN_VERSION;
|
|
9306
|
-
return cached2;
|
|
9307
|
-
}
|
|
9308
|
-
var cached2, UNKNOWN_VERSION = "0.0.0";
|
|
9309
|
-
var init_version = () => {};
|
|
9310
|
-
|
|
9311
|
-
// src/services/update-check.ts
|
|
9312
|
-
function parse(version) {
|
|
9313
|
-
const cleaned = version.trim().replace(/^v/, "");
|
|
9314
|
-
const [core = "", pre = ""] = cleaned.split("-", 2);
|
|
9315
|
-
const release2 = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
9316
|
-
return { release: release2, pre: pre ? pre.split(".") : [] };
|
|
9317
|
-
}
|
|
9318
|
-
function compareSemver(a, b) {
|
|
9319
|
-
const pa = parse(a);
|
|
9320
|
-
const pb = parse(b);
|
|
9321
|
-
const len = Math.max(pa.release.length, pb.release.length);
|
|
9322
|
-
for (let i = 0;i < len; i++) {
|
|
9323
|
-
const diff = (pa.release[i] ?? 0) - (pb.release[i] ?? 0);
|
|
9324
|
-
if (diff !== 0)
|
|
9325
|
-
return diff;
|
|
9326
|
-
}
|
|
9327
|
-
if (pa.pre.length === 0 && pb.pre.length === 0)
|
|
9328
|
-
return 0;
|
|
9329
|
-
if (pa.pre.length === 0)
|
|
9330
|
-
return 1;
|
|
9331
|
-
if (pb.pre.length === 0)
|
|
9332
|
-
return -1;
|
|
9333
|
-
const preLen = Math.max(pa.pre.length, pb.pre.length);
|
|
9334
|
-
for (let i = 0;i < preLen; i++) {
|
|
9335
|
-
const x = pa.pre[i];
|
|
9336
|
-
const y = pb.pre[i];
|
|
9337
|
-
if (x === undefined)
|
|
9338
|
-
return -1;
|
|
9339
|
-
if (y === undefined)
|
|
9340
|
-
return 1;
|
|
9341
|
-
const nx = Number(x);
|
|
9342
|
-
const ny = Number(y);
|
|
9343
|
-
const bothNumeric = !Number.isNaN(nx) && !Number.isNaN(ny);
|
|
9344
|
-
const cmp = bothNumeric ? nx - ny : x < y ? -1 : x > y ? 1 : 0;
|
|
9345
|
-
if (cmp !== 0)
|
|
9346
|
-
return cmp;
|
|
9347
|
-
}
|
|
9348
|
-
return 0;
|
|
9349
|
-
}
|
|
9350
|
-
async function checkForUpdate(opts) {
|
|
9351
|
-
const { current: current2 } = opts;
|
|
9352
|
-
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
9353
|
-
if (current2 === UNKNOWN_VERSION)
|
|
9354
|
-
return NO_UPDATE(current2);
|
|
9355
|
-
try {
|
|
9356
|
-
const res = await fetchImpl(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
|
|
9357
|
-
if (!res.ok)
|
|
9358
|
-
return NO_UPDATE(current2);
|
|
9359
|
-
const body = await res.json();
|
|
9360
|
-
const latest = typeof body.version === "string" ? body.version : null;
|
|
9361
|
-
if (!latest)
|
|
9362
|
-
return NO_UPDATE(current2);
|
|
9363
|
-
return {
|
|
9364
|
-
current: current2,
|
|
9365
|
-
latest,
|
|
9366
|
-
updateAvailable: compareSemver(latest, current2) > 0,
|
|
9367
|
-
command: UPDATE_COMMAND
|
|
9368
|
-
};
|
|
9369
|
-
} catch {
|
|
9370
|
-
return NO_UPDATE(current2);
|
|
9371
|
-
}
|
|
9372
|
-
}
|
|
9373
|
-
async function getUpdateInfo(current2, fetchImpl) {
|
|
9374
|
-
if (cache)
|
|
9375
|
-
return cache;
|
|
9376
|
-
const info = await checkForUpdate({ current: current2, fetchImpl });
|
|
9377
|
-
if (info.latest !== null)
|
|
9378
|
-
cache = info;
|
|
9379
|
-
return info;
|
|
9380
|
-
}
|
|
9381
|
-
var PACKAGE_NAME = "runcastle", UPDATE_COMMAND, NO_UPDATE = (current2) => ({
|
|
9382
|
-
current: current2,
|
|
9383
|
-
latest: null,
|
|
9384
|
-
updateAvailable: false,
|
|
9385
|
-
command: UPDATE_COMMAND
|
|
9386
|
-
}), cache;
|
|
9387
|
-
var init_update_check = __esm(() => {
|
|
9388
|
-
init_version();
|
|
9389
|
-
UPDATE_COMMAND = `bun add -g ${PACKAGE_NAME}@latest`;
|
|
9390
|
-
});
|
|
9391
|
-
|
|
9392
9851
|
// src/trpc/routers/system.ts
|
|
9393
9852
|
var systemRouter;
|
|
9394
9853
|
var init_system = __esm(() => {
|
|
@@ -9402,11 +9861,11 @@ var init_system = __esm(() => {
|
|
|
9402
9861
|
});
|
|
9403
9862
|
|
|
9404
9863
|
// src/services/test-notes.ts
|
|
9405
|
-
import { mkdirSync as
|
|
9406
|
-
import { dirname as
|
|
9864
|
+
import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync7 } from "fs";
|
|
9865
|
+
import { dirname as dirname11 } from "path";
|
|
9407
9866
|
import { asc as asc4, eq as eq14 } from "drizzle-orm";
|
|
9408
9867
|
function rowToNote(row) {
|
|
9409
|
-
return {
|
|
9868
|
+
return TestNote.parse({
|
|
9410
9869
|
id: row.id,
|
|
9411
9870
|
featureId: row.featureId,
|
|
9412
9871
|
lap: row.lap,
|
|
@@ -9415,7 +9874,7 @@ function rowToNote(row) {
|
|
|
9415
9874
|
ticketId: row.ticketId ?? undefined,
|
|
9416
9875
|
createdAt: row.createdAt,
|
|
9417
9876
|
updatedAt: row.updatedAt
|
|
9418
|
-
};
|
|
9877
|
+
});
|
|
9419
9878
|
}
|
|
9420
9879
|
function getNote(ctx, id) {
|
|
9421
9880
|
const row = ctx.db.select().from(testNotes).where(eq14(testNotes.id, id)).get();
|
|
@@ -9548,8 +10007,8 @@ function renderTestNotes(ctx, feature) {
|
|
|
9548
10007
|
lines.push(noteLine(ctx, note));
|
|
9549
10008
|
}
|
|
9550
10009
|
const path = featureDocPath(projectForFeature(ctx, feature), feature, "test-notes.md");
|
|
9551
|
-
|
|
9552
|
-
|
|
10010
|
+
mkdirSync11(dirname11(path), { recursive: true });
|
|
10011
|
+
writeFileSync7(path, `${lines.join(`
|
|
9553
10012
|
`)}
|
|
9554
10013
|
`, "utf8");
|
|
9555
10014
|
}
|
|
@@ -9740,6 +10199,8 @@ async function startServer() {
|
|
|
9740
10199
|
}
|
|
9741
10200
|
const server = Bun.serve({
|
|
9742
10201
|
port: config2.serverPort,
|
|
10202
|
+
hostname: SERVE_HOSTNAME,
|
|
10203
|
+
idleTimeout: SERVE_IDLE_TIMEOUT_SECONDS,
|
|
9743
10204
|
fetch(req, srv) {
|
|
9744
10205
|
if (tryUpgradeTerminal(req, srv))
|
|
9745
10206
|
return;
|
|
@@ -9748,15 +10209,16 @@ async function startServer() {
|
|
|
9748
10209
|
websocket: terminalWebSocket
|
|
9749
10210
|
});
|
|
9750
10211
|
console.log(`runcastle${process.env.RUNCASTLE_DEV ? " [dev]" : ""} server listening on ` + `http://localhost:${config2.serverPort} \u2014 data dir: ${dataDir()}`);
|
|
10212
|
+
getUpdateInfo(runcastleVersion()).catch(() => {});
|
|
9751
10213
|
const SHUTDOWN_KEY = Symbol.for("runcastle.shutdown.wired");
|
|
9752
10214
|
const g = globalThis;
|
|
9753
10215
|
if (!g[SHUTDOWN_KEY]) {
|
|
9754
10216
|
g[SHUTDOWN_KEY] = true;
|
|
9755
|
-
const shutdown = (
|
|
9756
|
-
ptyRegistry().
|
|
9757
|
-
server.stop()
|
|
9758
|
-
process.exit(
|
|
9759
|
-
};
|
|
10217
|
+
const shutdown = createShutdown({
|
|
10218
|
+
killAllTrees: () => ptyRegistry().killAllTrees(),
|
|
10219
|
+
stop: () => server.stop(),
|
|
10220
|
+
exit: (code) => process.exit(code)
|
|
10221
|
+
});
|
|
9760
10222
|
process.on("SIGINT", shutdown);
|
|
9761
10223
|
process.on("SIGTERM", shutdown);
|
|
9762
10224
|
}
|
|
@@ -9773,7 +10235,9 @@ var init_src2 = __esm(() => {
|
|
|
9773
10235
|
init_hooks();
|
|
9774
10236
|
init_stream();
|
|
9775
10237
|
init_web();
|
|
10238
|
+
init_update_check();
|
|
9776
10239
|
init_router();
|
|
10240
|
+
init_version();
|
|
9777
10241
|
init_reconcile_runs();
|
|
9778
10242
|
});
|
|
9779
10243
|
init_src2();
|