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