runcastle 1.2.4 → 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 +555 -206
- 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/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 +22 -1
- package/index.js +555 -206
- 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-CzDDtq7w.js → index-D-c432xb.js} +42 -42
- package/web/assets/index-D92MdVG6.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-o2brqt2b.css +0 -1
package/index.js
CHANGED
|
@@ -178,8 +178,11 @@ function loadConfig(env = process.env) {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
const overrides = {};
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
const num = (key, raw) => {
|
|
182
|
+
if (raw !== undefined && raw !== "")
|
|
183
|
+
overrides[key] = Number(raw);
|
|
184
|
+
};
|
|
185
|
+
num("serverPort", env.RUNCASTLE_SERVER_PORT);
|
|
183
186
|
if (env.RUNCASTLE_MODEL)
|
|
184
187
|
overrides.model = env.RUNCASTLE_MODEL;
|
|
185
188
|
if (env.RUNCASTLE_SMOKE_MODEL)
|
|
@@ -192,21 +195,12 @@ function loadConfig(env = process.env) {
|
|
|
192
195
|
overrides.mainBranch = env.RUNCASTLE_MAIN_BRANCH;
|
|
193
196
|
if (env.RUNCASTLE_SANDBOX_IMAGE)
|
|
194
197
|
overrides.sandboxImage = env.RUNCASTLE_SANDBOX_IMAGE;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (env.RUNCASTLE_BURN_ATTEMPTS) {
|
|
202
|
-
overrides.burnAttempts = Number(env.RUNCASTLE_BURN_ATTEMPTS);
|
|
203
|
-
}
|
|
204
|
-
if (env.RUNCASTLE_BURN_CONFLICT_ATTEMPTS !== undefined) {
|
|
205
|
-
overrides.burnConflictAttempts = Number(env.RUNCASTLE_BURN_CONFLICT_ATTEMPTS);
|
|
206
|
-
}
|
|
207
|
-
if (env.RUNCASTLE_BURN_CPUS)
|
|
208
|
-
overrides.burnCpus = Number(env.RUNCASTLE_BURN_CPUS);
|
|
209
|
-
if (env.RUNCASTLE_BURN_GUARD !== undefined) {
|
|
198
|
+
num("burnConcurrency", env.RUNCASTLE_BURN_CONCURRENCY);
|
|
199
|
+
num("burnMaxIterations", env.RUNCASTLE_BURN_MAX_ITERATIONS);
|
|
200
|
+
num("burnAttempts", env.RUNCASTLE_BURN_ATTEMPTS);
|
|
201
|
+
num("burnConflictAttempts", env.RUNCASTLE_BURN_CONFLICT_ATTEMPTS);
|
|
202
|
+
num("burnCpus", env.RUNCASTLE_BURN_CPUS);
|
|
203
|
+
if (env.RUNCASTLE_BURN_GUARD !== undefined && env.RUNCASTLE_BURN_GUARD !== "") {
|
|
210
204
|
overrides.burnGuard = env.RUNCASTLE_BURN_GUARD !== "0" && env.RUNCASTLE_BURN_GUARD !== "false";
|
|
211
205
|
}
|
|
212
206
|
if (env.RUNCASTLE_SETUP_COMMAND)
|
|
@@ -235,6 +229,7 @@ function ensureDataDir() {
|
|
|
235
229
|
mkdirSync(join2(root, "sessions"), { recursive: true });
|
|
236
230
|
mkdirSync(join2(root, "worktrees"), { recursive: true });
|
|
237
231
|
}
|
|
232
|
+
var SERVE_HOSTNAME = "127.0.0.1", SERVE_IDLE_TIMEOUT_SECONDS = 120;
|
|
238
233
|
var init_config2 = __esm(() => {
|
|
239
234
|
init_config_load();
|
|
240
235
|
init_paths();
|
|
@@ -249,7 +244,10 @@ var init_ids = () => {};
|
|
|
249
244
|
|
|
250
245
|
// ../core/src/schemas.ts
|
|
251
246
|
import * as z2 from "zod";
|
|
252
|
-
|
|
247
|
+
function isProjectSessionKind(kind) {
|
|
248
|
+
return PROJECT_SESSION_KINDS.includes(kind);
|
|
249
|
+
}
|
|
250
|
+
var Phase, TicketStatus, SessionKind, PROJECT_SESSION_KINDS, SessionPurpose, MergeBranchPair, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNote, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
253
251
|
var init_schemas = __esm(() => {
|
|
254
252
|
Phase = z2.enum([
|
|
255
253
|
"ideation",
|
|
@@ -269,13 +267,14 @@ var init_schemas = __esm(() => {
|
|
|
269
267
|
"prepare",
|
|
270
268
|
"project"
|
|
271
269
|
]);
|
|
270
|
+
PROJECT_SESSION_KINDS = ["prepare", "project"];
|
|
272
271
|
SessionPurpose = z2.enum(["resolve-conflict"]);
|
|
273
272
|
MergeBranchPair = z2.object({
|
|
274
273
|
mergeFrom: z2.string(),
|
|
275
274
|
mergeInto: z2.string()
|
|
276
275
|
});
|
|
277
276
|
RunStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
|
|
278
|
-
FeatureStatus = z2.enum(["active", "shipped", "archived"]);
|
|
277
|
+
FeatureStatus = z2.enum(["draft", "active", "shipped", "archived"]);
|
|
279
278
|
SessionStatus = z2.enum(["launching", "live", "ended"]);
|
|
280
279
|
TicketInput = z2.object({
|
|
281
280
|
title: z2.string(),
|
|
@@ -294,7 +293,8 @@ var init_schemas = __esm(() => {
|
|
|
294
293
|
commits: z2.array(z2.string()),
|
|
295
294
|
error: z2.string().optional(),
|
|
296
295
|
attemptBranch: z2.string().optional(),
|
|
297
|
-
conflictFiles: z2.array(z2.string()).optional()
|
|
296
|
+
conflictFiles: z2.array(z2.string()).optional(),
|
|
297
|
+
digest: z2.string().optional()
|
|
298
298
|
});
|
|
299
299
|
WaypointType = z2.enum(["grilling", "research", "prototype", "task"]);
|
|
300
300
|
WaypointStatus = z2.enum(["open", "claimed", "resolved", "dropped"]);
|
|
@@ -359,7 +359,8 @@ var init_schemas = __esm(() => {
|
|
|
359
359
|
dbResetCommand: z2.string().optional(),
|
|
360
360
|
driveSetupCommand: z2.string().optional(),
|
|
361
361
|
driveStopCommand: z2.string().optional(),
|
|
362
|
-
driveEnv: z2.string().optional()
|
|
362
|
+
driveEnv: z2.string().optional(),
|
|
363
|
+
closedAt: z2.number().optional()
|
|
363
364
|
});
|
|
364
365
|
ProjectFinding = z2.object({
|
|
365
366
|
key: PreparedKey,
|
|
@@ -377,6 +378,7 @@ var init_schemas = __esm(() => {
|
|
|
377
378
|
slug: z2.string(),
|
|
378
379
|
title: z2.string(),
|
|
379
380
|
oneLiner: z2.string(),
|
|
381
|
+
brief: z2.string().optional(),
|
|
380
382
|
mapped: z2.boolean(),
|
|
381
383
|
lap: z2.number(),
|
|
382
384
|
phase: Phase,
|
|
@@ -405,7 +407,8 @@ var init_schemas = __esm(() => {
|
|
|
405
407
|
status: RunStatus,
|
|
406
408
|
startedAt: z2.number(),
|
|
407
409
|
endedAt: z2.number().optional(),
|
|
408
|
-
summary: z2.string().optional()
|
|
410
|
+
summary: z2.string().optional(),
|
|
411
|
+
digest: z2.string().optional()
|
|
409
412
|
});
|
|
410
413
|
SettingSource = z2.enum(["env", "project", "file", "default"]);
|
|
411
414
|
SettingScope = z2.enum(["global", "project"]);
|
|
@@ -580,7 +583,7 @@ var init_pipeline = __esm(() => {
|
|
|
580
583
|
});
|
|
581
584
|
|
|
582
585
|
// ../core/src/db-schema.ts
|
|
583
|
-
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
586
|
+
import { index, integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
584
587
|
var projects, projectFindings, features, sessions, tickets, testNotes, waypoints, runs, events, gateOverrides;
|
|
585
588
|
var init_db_schema = __esm(() => {
|
|
586
589
|
projects = sqliteTable("projects", {
|
|
@@ -616,6 +619,7 @@ var init_db_schema = __esm(() => {
|
|
|
616
619
|
slug: text("slug").notNull(),
|
|
617
620
|
title: text("title").notNull(),
|
|
618
621
|
oneLiner: text("one_liner").notNull(),
|
|
622
|
+
brief: text("brief"),
|
|
619
623
|
mapped: integer("mapped", { mode: "boolean" }).notNull().default(false),
|
|
620
624
|
lap: integer("lap").notNull().default(1),
|
|
621
625
|
phase: text("phase").notNull().$type(),
|
|
@@ -653,7 +657,8 @@ var init_db_schema = __esm(() => {
|
|
|
653
657
|
commits: text("commits", { mode: "json" }).notNull().$type(),
|
|
654
658
|
error: text("error"),
|
|
655
659
|
attemptBranch: text("attempt_branch"),
|
|
656
|
-
conflictFiles: text("conflict_files", { mode: "json" }).$type()
|
|
660
|
+
conflictFiles: text("conflict_files", { mode: "json" }).$type(),
|
|
661
|
+
digest: text("digest")
|
|
657
662
|
});
|
|
658
663
|
testNotes = sqliteTable("test_notes", {
|
|
659
664
|
id: text("id").primaryKey(),
|
|
@@ -686,7 +691,8 @@ var init_db_schema = __esm(() => {
|
|
|
686
691
|
status: text("status").notNull().$type(),
|
|
687
692
|
startedAt: integer("started_at").notNull(),
|
|
688
693
|
endedAt: integer("ended_at"),
|
|
689
|
-
summary: text("summary")
|
|
694
|
+
summary: text("summary"),
|
|
695
|
+
digest: text("digest")
|
|
690
696
|
});
|
|
691
697
|
events = sqliteTable("events", {
|
|
692
698
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
@@ -699,7 +705,10 @@ var init_db_schema = __esm(() => {
|
|
|
699
705
|
type: text("type").notNull(),
|
|
700
706
|
message: text("message").notNull(),
|
|
701
707
|
data: text("data", { mode: "json" })
|
|
702
|
-
})
|
|
708
|
+
}, (t) => [
|
|
709
|
+
index("events_project_id_id_idx").on(t.projectId, t.id),
|
|
710
|
+
index("events_feature_id_id_idx").on(t.featureId, t.id)
|
|
711
|
+
]);
|
|
703
712
|
gateOverrides = sqliteTable("gate_overrides", {
|
|
704
713
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
705
714
|
featureId: text("feature_id").notNull(),
|
|
@@ -1279,6 +1288,16 @@ var init_ring_buffer = __esm(() => {
|
|
|
1279
1288
|
});
|
|
1280
1289
|
|
|
1281
1290
|
// src/pty/registry.ts
|
|
1291
|
+
function killTreeBounded(pty) {
|
|
1292
|
+
return new Promise((resolve3) => {
|
|
1293
|
+
const timer = setTimeout(resolve3, KILL_TREE_TIMEOUT_MS);
|
|
1294
|
+
pty.killTree().catch(() => {}).then(() => {
|
|
1295
|
+
clearTimeout(timer);
|
|
1296
|
+
resolve3();
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1282
1301
|
class PtyRegistry {
|
|
1283
1302
|
entries = new Map;
|
|
1284
1303
|
create(input) {
|
|
@@ -1336,18 +1355,31 @@ class PtyRegistry {
|
|
|
1336
1355
|
const entry = this.entries.get(sessionId);
|
|
1337
1356
|
if (!entry)
|
|
1338
1357
|
return false;
|
|
1339
|
-
|
|
1358
|
+
this.tearDown(entry);
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
async killTree(sessionId) {
|
|
1362
|
+
const entry = this.entries.get(sessionId);
|
|
1363
|
+
if (!entry)
|
|
1364
|
+
return false;
|
|
1365
|
+
await this.tearDown(entry);
|
|
1340
1366
|
return true;
|
|
1341
1367
|
}
|
|
1342
1368
|
remove(sessionId) {
|
|
1343
1369
|
this.entries.delete(sessionId);
|
|
1344
1370
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
if (!entry.exited)
|
|
1348
|
-
entry.pty.kill();
|
|
1349
|
-
}
|
|
1371
|
+
async killAllTrees() {
|
|
1372
|
+
const all = [...this.entries.values()];
|
|
1350
1373
|
this.entries.clear();
|
|
1374
|
+
await Promise.allSettled(all.map((entry) => this.tearDown(entry)));
|
|
1375
|
+
}
|
|
1376
|
+
async tearDown(entry) {
|
|
1377
|
+
if (entry.exited)
|
|
1378
|
+
return;
|
|
1379
|
+
await killTreeBounded(entry.pty);
|
|
1380
|
+
try {
|
|
1381
|
+
entry.pty.kill();
|
|
1382
|
+
} catch {}
|
|
1351
1383
|
}
|
|
1352
1384
|
ids() {
|
|
1353
1385
|
return [...this.entries.keys()];
|
|
@@ -1359,7 +1391,7 @@ function ptyRegistry() {
|
|
|
1359
1391
|
g[REGISTRY_KEY] = new PtyRegistry;
|
|
1360
1392
|
return g[REGISTRY_KEY];
|
|
1361
1393
|
}
|
|
1362
|
-
var REGISTRY_KEY;
|
|
1394
|
+
var KILL_TREE_TIMEOUT_MS = 5000, REGISTRY_KEY;
|
|
1363
1395
|
var init_registry = __esm(() => {
|
|
1364
1396
|
init_pty();
|
|
1365
1397
|
init_ring_buffer();
|
|
@@ -1440,7 +1472,7 @@ var init_bus = __esm(() => {
|
|
|
1440
1472
|
// src/services/events.ts
|
|
1441
1473
|
import { and, asc, desc, eq, gt, max } from "drizzle-orm";
|
|
1442
1474
|
function rowToEvent(row) {
|
|
1443
|
-
return {
|
|
1475
|
+
return EventRow.parse({
|
|
1444
1476
|
id: row.id,
|
|
1445
1477
|
projectId: row.projectId,
|
|
1446
1478
|
featureId: row.featureId ?? undefined,
|
|
@@ -1450,7 +1482,7 @@ function rowToEvent(row) {
|
|
|
1450
1482
|
type: row.type,
|
|
1451
1483
|
message: row.message,
|
|
1452
1484
|
data: row.data ?? undefined
|
|
1453
|
-
};
|
|
1485
|
+
});
|
|
1454
1486
|
}
|
|
1455
1487
|
function projectIdForFeature(ctx, featureId) {
|
|
1456
1488
|
const row = ctx.db.select({ projectId: features.projectId }).from(features).where(eq(features.id, featureId)).get();
|
|
@@ -1521,6 +1553,7 @@ function listByProject(ctx, projectId, afterId = 0) {
|
|
|
1521
1553
|
return rows.map(rowToEvent);
|
|
1522
1554
|
}
|
|
1523
1555
|
var init_events = __esm(() => {
|
|
1556
|
+
init_src();
|
|
1524
1557
|
init_schema();
|
|
1525
1558
|
init_errors();
|
|
1526
1559
|
init_bus();
|
|
@@ -1529,12 +1562,13 @@ var init_events = __esm(() => {
|
|
|
1529
1562
|
// src/services/repo.ts
|
|
1530
1563
|
import { and as and2, desc as desc2, eq as eq2 } from "drizzle-orm";
|
|
1531
1564
|
function rowToFeature(row) {
|
|
1532
|
-
return {
|
|
1565
|
+
return Feature.parse({
|
|
1533
1566
|
id: row.id,
|
|
1534
1567
|
projectId: row.projectId,
|
|
1535
1568
|
slug: row.slug,
|
|
1536
1569
|
title: row.title,
|
|
1537
1570
|
oneLiner: row.oneLiner,
|
|
1571
|
+
brief: row.brief ?? undefined,
|
|
1538
1572
|
mapped: row.mapped,
|
|
1539
1573
|
lap: row.lap,
|
|
1540
1574
|
phase: row.phase,
|
|
@@ -1542,10 +1576,10 @@ function rowToFeature(row) {
|
|
|
1542
1576
|
baseBranch: row.baseBranch ?? undefined,
|
|
1543
1577
|
status: row.status,
|
|
1544
1578
|
createdAt: row.createdAt
|
|
1545
|
-
};
|
|
1579
|
+
});
|
|
1546
1580
|
}
|
|
1547
1581
|
function rowToProject(row) {
|
|
1548
|
-
return {
|
|
1582
|
+
return Project.parse({
|
|
1549
1583
|
id: row.id,
|
|
1550
1584
|
name: row.name,
|
|
1551
1585
|
repoPath: row.repoPath,
|
|
@@ -1558,22 +1592,24 @@ function rowToProject(row) {
|
|
|
1558
1592
|
dbResetCommand: row.dbResetCommand ?? undefined,
|
|
1559
1593
|
driveSetupCommand: row.driveSetupCommand ?? undefined,
|
|
1560
1594
|
driveStopCommand: row.driveStopCommand ?? undefined,
|
|
1561
|
-
driveEnv: row.driveEnv ?? undefined
|
|
1562
|
-
|
|
1595
|
+
driveEnv: row.driveEnv ?? undefined,
|
|
1596
|
+
closedAt: row.closedAt ?? undefined
|
|
1597
|
+
});
|
|
1563
1598
|
}
|
|
1564
1599
|
function rowToRun(row) {
|
|
1565
|
-
return {
|
|
1600
|
+
return Run.parse({
|
|
1566
1601
|
id: row.id,
|
|
1567
1602
|
featureId: row.featureId,
|
|
1568
1603
|
workflow: row.workflow,
|
|
1569
1604
|
status: row.status,
|
|
1570
1605
|
startedAt: row.startedAt,
|
|
1571
1606
|
endedAt: row.endedAt ?? undefined,
|
|
1572
|
-
summary: row.summary ?? undefined
|
|
1573
|
-
|
|
1607
|
+
summary: row.summary ?? undefined,
|
|
1608
|
+
digest: row.digest ?? undefined
|
|
1609
|
+
});
|
|
1574
1610
|
}
|
|
1575
1611
|
function rowToSession(row) {
|
|
1576
|
-
return {
|
|
1612
|
+
return SessionRow.parse({
|
|
1577
1613
|
id: row.id,
|
|
1578
1614
|
featureId: row.featureId ?? undefined,
|
|
1579
1615
|
projectId: row.projectId ?? undefined,
|
|
@@ -1585,7 +1621,7 @@ function rowToSession(row) {
|
|
|
1585
1621
|
status: row.status,
|
|
1586
1622
|
awaitingInput: row.awaitingInput,
|
|
1587
1623
|
worktreePath: row.worktreePath
|
|
1588
|
-
};
|
|
1624
|
+
});
|
|
1589
1625
|
}
|
|
1590
1626
|
function tryGetFeature(ctx, id) {
|
|
1591
1627
|
const row = ctx.db.select().from(features).where(eq2(features.id, id)).get();
|
|
@@ -1661,6 +1697,7 @@ function setFeatureStatus(ctx, featureId, status) {
|
|
|
1661
1697
|
return { ...current, status };
|
|
1662
1698
|
}
|
|
1663
1699
|
var init_repo = __esm(() => {
|
|
1700
|
+
init_src();
|
|
1664
1701
|
init_schema();
|
|
1665
1702
|
init_errors();
|
|
1666
1703
|
init_events();
|
|
@@ -1669,7 +1706,7 @@ var init_repo = __esm(() => {
|
|
|
1669
1706
|
// src/services/waypoints.ts
|
|
1670
1707
|
import { and as and3, asc as asc2, eq as eq3 } from "drizzle-orm";
|
|
1671
1708
|
function rowToWaypoint(row) {
|
|
1672
|
-
return {
|
|
1709
|
+
return Waypoint.parse({
|
|
1673
1710
|
id: row.id,
|
|
1674
1711
|
featureId: row.featureId,
|
|
1675
1712
|
seq: row.seq,
|
|
@@ -1682,7 +1719,7 @@ function rowToWaypoint(row) {
|
|
|
1682
1719
|
claimedBy: row.claimedBy ?? undefined,
|
|
1683
1720
|
lastSessionId: row.lastSessionId ?? undefined,
|
|
1684
1721
|
summary: row.summary ?? undefined
|
|
1685
|
-
};
|
|
1722
|
+
});
|
|
1686
1723
|
}
|
|
1687
1724
|
function listByFeature(ctx, featureId) {
|
|
1688
1725
|
return ctx.db.select().from(waypoints).where(eq3(waypoints.featureId, featureId)).orderBy(asc2(waypoints.seq)).all().map(rowToWaypoint);
|
|
@@ -1798,6 +1835,9 @@ function releaseForSession(ctx, claimant) {
|
|
|
1798
1835
|
}
|
|
1799
1836
|
function resolve3(ctx, id, disposition, summary) {
|
|
1800
1837
|
const wp = getWaypoint(ctx, id);
|
|
1838
|
+
if (isTerminal(wp)) {
|
|
1839
|
+
throw new InvalidInputError(`waypoint ${wp.seq} is already ${wp.status}`);
|
|
1840
|
+
}
|
|
1801
1841
|
const before = listByFeature(ctx, wp.featureId);
|
|
1802
1842
|
const bySeqBefore = new Map(before.map((w) => [w.seq, w]));
|
|
1803
1843
|
ctx.db.update(waypoints).set({ status: disposition, summary, claimedBy: null }).where(eq3(waypoints.id, id)).run();
|
|
@@ -1834,6 +1874,91 @@ var init_waypoints = __esm(() => {
|
|
|
1834
1874
|
TERMINAL = new Set(["resolved", "dropped"]);
|
|
1835
1875
|
});
|
|
1836
1876
|
|
|
1877
|
+
// src/services/feature-docs.ts
|
|
1878
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1879
|
+
import { join as join5 } from "path";
|
|
1880
|
+
function featureDocsDir(project, feature) {
|
|
1881
|
+
const relSegments = featureDocsRel(feature.slug).split("/");
|
|
1882
|
+
const worktree = worktreeDir(project.id, feature.slug);
|
|
1883
|
+
const base = existsSync5(worktree) ? worktree : project.repoPath;
|
|
1884
|
+
return join5(base, ...relSegments);
|
|
1885
|
+
}
|
|
1886
|
+
function featureDocPath(project, feature, fileName) {
|
|
1887
|
+
return join5(featureDocsDir(project, feature), fileName);
|
|
1888
|
+
}
|
|
1889
|
+
var init_feature_docs = __esm(() => {
|
|
1890
|
+
init_paths();
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
// src/services/docs-watch.ts
|
|
1894
|
+
import { watch } from "fs";
|
|
1895
|
+
function startDocsWatch(ctx, feature) {
|
|
1896
|
+
if (watches.has(feature.id))
|
|
1897
|
+
return;
|
|
1898
|
+
try {
|
|
1899
|
+
const dir = featureDocsDir(projectForFeature(ctx, feature), feature);
|
|
1900
|
+
const watcher = watch(dir, { persistent: false }, (_event, filename) => {
|
|
1901
|
+
noteDocsChange(ctx, feature.id, typeof filename === "string" ? filename : null);
|
|
1902
|
+
});
|
|
1903
|
+
watcher.on("error", (err) => {
|
|
1904
|
+
console.error(`docs watcher failed for feature ${feature.id}`, err);
|
|
1905
|
+
stopDocsWatch(feature.id);
|
|
1906
|
+
});
|
|
1907
|
+
watches.set(feature.id, { watcher, timer: undefined, changed: new Set });
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
console.error(`docs watcher could not start for feature ${feature.id}`, err);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
function stopDocsWatch(featureId) {
|
|
1913
|
+
const entry = watches.get(featureId);
|
|
1914
|
+
if (!entry)
|
|
1915
|
+
return;
|
|
1916
|
+
watches.delete(featureId);
|
|
1917
|
+
if (entry.timer)
|
|
1918
|
+
clearTimeout(entry.timer);
|
|
1919
|
+
try {
|
|
1920
|
+
entry.watcher.close();
|
|
1921
|
+
} catch (err) {
|
|
1922
|
+
console.error(`docs watcher failed to close for feature ${featureId}`, err);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
function noteDocsChange(ctx, featureId, filename) {
|
|
1926
|
+
const entry = watches.get(featureId);
|
|
1927
|
+
if (!entry)
|
|
1928
|
+
return;
|
|
1929
|
+
if (filename !== null && !filename.endsWith(".md"))
|
|
1930
|
+
return;
|
|
1931
|
+
if (filename)
|
|
1932
|
+
entry.changed.add(filename);
|
|
1933
|
+
if (entry.timer)
|
|
1934
|
+
clearTimeout(entry.timer);
|
|
1935
|
+
entry.timer = setTimeout(() => emitDocsChanged(ctx, featureId), DOCS_WATCH_DEBOUNCE_MS);
|
|
1936
|
+
}
|
|
1937
|
+
function emitDocsChanged(ctx, featureId) {
|
|
1938
|
+
const entry = watches.get(featureId);
|
|
1939
|
+
if (!entry)
|
|
1940
|
+
return;
|
|
1941
|
+
entry.timer = undefined;
|
|
1942
|
+
const files = [...entry.changed].sort();
|
|
1943
|
+
entry.changed.clear();
|
|
1944
|
+
try {
|
|
1945
|
+
emit(ctx, featureId, {
|
|
1946
|
+
type: "docs.changed",
|
|
1947
|
+
message: files.length > 0 ? `docs changed: ${files.join(", ")}` : "docs changed",
|
|
1948
|
+
data: { files }
|
|
1949
|
+
});
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
console.error(`docs watcher failed to emit for feature ${featureId}`, err);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
var DOCS_WATCH_DEBOUNCE_MS = 300, watches;
|
|
1955
|
+
var init_docs_watch = __esm(() => {
|
|
1956
|
+
init_events();
|
|
1957
|
+
init_feature_docs();
|
|
1958
|
+
init_repo();
|
|
1959
|
+
watches = new Map;
|
|
1960
|
+
});
|
|
1961
|
+
|
|
1837
1962
|
// src/pty/dev-pane.ts
|
|
1838
1963
|
function drivePaneId(ownerId) {
|
|
1839
1964
|
return `${DRIVE_PREFIX}${ownerId}`;
|
|
@@ -1894,24 +2019,12 @@ function devPaneLive(paneId) {
|
|
|
1894
2019
|
const entry = ptyRegistry().get(paneId);
|
|
1895
2020
|
return !!entry && !entry.exited;
|
|
1896
2021
|
}
|
|
1897
|
-
function killTreeBounded(pty) {
|
|
1898
|
-
return new Promise((resolve4) => {
|
|
1899
|
-
const timer = setTimeout(resolve4, KILL_TREE_TIMEOUT_MS);
|
|
1900
|
-
pty.killTree().catch(() => {}).then(() => {
|
|
1901
|
-
clearTimeout(timer);
|
|
1902
|
-
resolve4();
|
|
1903
|
-
});
|
|
1904
|
-
});
|
|
1905
|
-
}
|
|
1906
2022
|
async function stopDevPane(paneId) {
|
|
1907
2023
|
const reg = ptyRegistry();
|
|
1908
|
-
|
|
1909
|
-
if (entry && !entry.exited)
|
|
1910
|
-
await killTreeBounded(entry.pty);
|
|
1911
|
-
reg.kill(paneId);
|
|
2024
|
+
await reg.killTree(paneId);
|
|
1912
2025
|
reg.remove(paneId);
|
|
1913
2026
|
}
|
|
1914
|
-
var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192
|
|
2027
|
+
var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192;
|
|
1915
2028
|
var init_dev_pane = __esm(() => {
|
|
1916
2029
|
init_events();
|
|
1917
2030
|
init_registry();
|
|
@@ -1969,7 +2082,8 @@ function runDriveHook(command, opts) {
|
|
|
1969
2082
|
cwd: opts.cwd,
|
|
1970
2083
|
env: opts.env ?? process.env,
|
|
1971
2084
|
windowsHide: true,
|
|
1972
|
-
windowsVerbatimArguments: verbatim
|
|
2085
|
+
windowsVerbatimArguments: verbatim,
|
|
2086
|
+
detached: process.platform !== "win32"
|
|
1973
2087
|
});
|
|
1974
2088
|
} catch (err) {
|
|
1975
2089
|
output = err instanceof Error ? err.message : String(err);
|
|
@@ -1978,7 +2092,11 @@ function runDriveHook(command, opts) {
|
|
|
1978
2092
|
}
|
|
1979
2093
|
timer = setTimeout(() => {
|
|
1980
2094
|
timedOut = true;
|
|
1981
|
-
child.
|
|
2095
|
+
const pid = child.pid;
|
|
2096
|
+
if (pid === undefined)
|
|
2097
|
+
child.kill();
|
|
2098
|
+
else
|
|
2099
|
+
killProcessTree(pid).then(() => child.kill());
|
|
1982
2100
|
const grace = setTimeout(() => finish(null), KILL_GRACE_MS);
|
|
1983
2101
|
grace.unref?.();
|
|
1984
2102
|
}, timeoutMs);
|
|
@@ -2009,6 +2127,7 @@ function describeHookResult(command, result) {
|
|
|
2009
2127
|
}
|
|
2010
2128
|
var DRIVE_HOOK_TIMEOUT_MS, OUTPUT_TAIL_LINES = 40, KILL_GRACE_MS = 2000;
|
|
2011
2129
|
var init_drive_hooks = __esm(() => {
|
|
2130
|
+
init_kill_tree();
|
|
2012
2131
|
DRIVE_HOOK_TIMEOUT_MS = 10 * 60000;
|
|
2013
2132
|
});
|
|
2014
2133
|
|
|
@@ -2225,9 +2344,9 @@ var init_findings = __esm(() => {
|
|
|
2225
2344
|
});
|
|
2226
2345
|
|
|
2227
2346
|
// src/services/git.ts
|
|
2228
|
-
import { existsSync as
|
|
2347
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, mkdtempSync, realpathSync, rmSync } from "fs";
|
|
2229
2348
|
import { tmpdir } from "os";
|
|
2230
|
-
import { dirname, join as
|
|
2349
|
+
import { dirname, join as join6, resolve as resolve4 } from "path";
|
|
2231
2350
|
import { simpleGit } from "simple-git";
|
|
2232
2351
|
function migrationPaths(paths) {
|
|
2233
2352
|
return paths.filter((p) => MIGRATION_DIR_RE.test(p) || DRIZZLE_SQL_RE.test(p));
|
|
@@ -2252,7 +2371,7 @@ function canon(p) {
|
|
|
2252
2371
|
return process.platform === "win32" ? abs.replace(/\\/g, "/").toLowerCase() : abs;
|
|
2253
2372
|
}
|
|
2254
2373
|
async function assertRepo(repoPath) {
|
|
2255
|
-
if (!
|
|
2374
|
+
if (!existsSync6(repoPath)) {
|
|
2256
2375
|
throw new InvalidInputError(`path does not exist: ${repoPath}`);
|
|
2257
2376
|
}
|
|
2258
2377
|
let isRepo = false;
|
|
@@ -2341,7 +2460,7 @@ async function ensureTalkWorktree(project, feature) {
|
|
|
2341
2460
|
await ensureBranchExists(g, branch, feature.baseBranch ?? project.mainBranch);
|
|
2342
2461
|
if (await worktreeIsValid(g, worktreePath, branch))
|
|
2343
2462
|
return worktreePath;
|
|
2344
|
-
if (
|
|
2463
|
+
if (existsSync6(worktreePath) && (await registeredWorktrees(g)).has(canon(worktreePath))) {
|
|
2345
2464
|
if (await checkoutInWorktree(worktreePath, branch))
|
|
2346
2465
|
return worktreePath;
|
|
2347
2466
|
}
|
|
@@ -2364,14 +2483,13 @@ async function addWorktree(g, worktreePath, branch, label) {
|
|
|
2364
2483
|
}
|
|
2365
2484
|
}
|
|
2366
2485
|
}
|
|
2367
|
-
async function ensureProjectWorktree(project) {
|
|
2486
|
+
async function ensureProjectWorktree(project, onLanded) {
|
|
2368
2487
|
const worktreePath = worktreeDir(project.id, PROJECT_WORKTREE_SLUG);
|
|
2369
2488
|
const g = git(project.repoPath);
|
|
2370
2489
|
const base = project.mainBranch;
|
|
2371
|
-
const
|
|
2372
|
-
if (
|
|
2373
|
-
|
|
2374
|
-
}
|
|
2490
|
+
const landed = await landProjectBranch(project);
|
|
2491
|
+
if (landed)
|
|
2492
|
+
onLanded?.(landed);
|
|
2375
2493
|
const stillAhead = await branchCommitsAhead(project.repoPath, base, PROJECT_BRANCH);
|
|
2376
2494
|
if (stillAhead.length === 0) {
|
|
2377
2495
|
await deleteBranchDetachingWorktrees(g, project.repoPath, PROJECT_BRANCH);
|
|
@@ -2379,7 +2497,7 @@ async function ensureProjectWorktree(project) {
|
|
|
2379
2497
|
}
|
|
2380
2498
|
if (await worktreeIsValid(g, worktreePath, PROJECT_BRANCH))
|
|
2381
2499
|
return worktreePath;
|
|
2382
|
-
if (
|
|
2500
|
+
if (existsSync6(worktreePath) && (await registeredWorktrees(g)).has(canon(worktreePath))) {
|
|
2383
2501
|
if (await checkoutInWorktree(worktreePath, PROJECT_BRANCH))
|
|
2384
2502
|
return worktreePath;
|
|
2385
2503
|
}
|
|
@@ -2405,7 +2523,7 @@ async function ensureBranchExists(g, branch, from) {
|
|
|
2405
2523
|
}
|
|
2406
2524
|
}
|
|
2407
2525
|
async function worktreeIsValid(g, worktreePath, branch) {
|
|
2408
|
-
if (!
|
|
2526
|
+
if (!existsSync6(worktreePath))
|
|
2409
2527
|
return false;
|
|
2410
2528
|
const registered = await registeredWorktrees(g);
|
|
2411
2529
|
if (!registered.has(canon(worktreePath)))
|
|
@@ -2451,7 +2569,7 @@ async function worktreesOnBranch(g, branch, mainRepoPath) {
|
|
|
2451
2569
|
return (await listWorktrees(g)).filter((e) => e.branch === branch && canon(e.path) !== main).map((e) => e.path);
|
|
2452
2570
|
}
|
|
2453
2571
|
async function detachWorktree(path) {
|
|
2454
|
-
if (!
|
|
2572
|
+
if (!existsSync6(path))
|
|
2455
2573
|
return false;
|
|
2456
2574
|
const g = git(path);
|
|
2457
2575
|
let head;
|
|
@@ -2466,7 +2584,7 @@ async function detachWorktree(path) {
|
|
|
2466
2584
|
return true;
|
|
2467
2585
|
}
|
|
2468
2586
|
async function reattachWorktree(path, branch) {
|
|
2469
|
-
if (!
|
|
2587
|
+
if (!existsSync6(path))
|
|
2470
2588
|
return;
|
|
2471
2589
|
try {
|
|
2472
2590
|
await git(path).checkout(branch);
|
|
@@ -2482,21 +2600,21 @@ function ticketBranchName(slug, ticketSeq, unique) {
|
|
|
2482
2600
|
return `${TICKET_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${ticketSeq}-${unique}`;
|
|
2483
2601
|
}
|
|
2484
2602
|
function burnWorktreePath(repoPath, branch) {
|
|
2485
|
-
return
|
|
2603
|
+
return join6(repoPath, ".sandcastle", "worktrees", branch.replace(/\//g, "-"));
|
|
2486
2604
|
}
|
|
2487
2605
|
async function cleanupBurnWorktree(repoPath, branch, opts = {}) {
|
|
2488
2606
|
const path = burnWorktreePath(repoPath, branch);
|
|
2489
2607
|
const attempts = Math.max(1, opts.attempts ?? 3);
|
|
2490
2608
|
const delayMs = opts.delayMs ?? 750;
|
|
2491
2609
|
const g = git(repoPath);
|
|
2492
|
-
for (let attempt = 1;attempt <= attempts &&
|
|
2610
|
+
for (let attempt = 1;attempt <= attempts && existsSync6(path); attempt++) {
|
|
2493
2611
|
try {
|
|
2494
2612
|
await g.raw(["worktree", "remove", "--force", path]);
|
|
2495
2613
|
} catch {}
|
|
2496
|
-
if (
|
|
2614
|
+
if (existsSync6(path) && attempt < attempts)
|
|
2497
2615
|
await sleep(delayMs);
|
|
2498
2616
|
}
|
|
2499
|
-
if (
|
|
2617
|
+
if (existsSync6(path)) {
|
|
2500
2618
|
try {
|
|
2501
2619
|
rmSync(path, { recursive: true, force: true });
|
|
2502
2620
|
} catch {}
|
|
@@ -2504,7 +2622,7 @@ async function cleanupBurnWorktree(repoPath, branch, opts = {}) {
|
|
|
2504
2622
|
try {
|
|
2505
2623
|
await g.raw(["worktree", "prune"]);
|
|
2506
2624
|
} catch {}
|
|
2507
|
-
return !
|
|
2625
|
+
return !existsSync6(path);
|
|
2508
2626
|
}
|
|
2509
2627
|
function sleep(ms) {
|
|
2510
2628
|
return new Promise((done) => setTimeout(done, ms));
|
|
@@ -2652,11 +2770,11 @@ async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
|
|
|
2652
2770
|
async function mergeInDisposableWorktree(g, featureBranchName, tempBranch) {
|
|
2653
2771
|
let dir;
|
|
2654
2772
|
try {
|
|
2655
|
-
dir = mkdtempSync(
|
|
2773
|
+
dir = mkdtempSync(join6(tmpdir(), "rc-land-"));
|
|
2656
2774
|
} catch (e) {
|
|
2657
2775
|
return { ok: false, error: errMsg(e) };
|
|
2658
2776
|
}
|
|
2659
|
-
const wt =
|
|
2777
|
+
const wt = join6(dir, "wt");
|
|
2660
2778
|
try {
|
|
2661
2779
|
await g.raw(["worktree", "add", wt, featureBranchName]);
|
|
2662
2780
|
const gw = git(wt);
|
|
@@ -2763,7 +2881,7 @@ async function removeTalkWorktree(repoPath, worktreePath) {
|
|
|
2763
2881
|
try {
|
|
2764
2882
|
await g.raw(["worktree", "prune"]);
|
|
2765
2883
|
} catch {}
|
|
2766
|
-
if (
|
|
2884
|
+
if (existsSync6(worktreePath)) {
|
|
2767
2885
|
throw new InvalidInputError(`could not remove talk worktree at ${worktreePath} \u2014 a file may be locked; ` + "close anything using it and retry the delete");
|
|
2768
2886
|
}
|
|
2769
2887
|
}
|
|
@@ -2790,7 +2908,7 @@ async function deleteFeatureBranches(repoPath, slug) {
|
|
|
2790
2908
|
return { deleted, kept };
|
|
2791
2909
|
}
|
|
2792
2910
|
async function commitDocs(worktreePath, message) {
|
|
2793
|
-
if (!
|
|
2911
|
+
if (!existsSync6(resolve4(worktreePath, DOCS_PATHSPEC)))
|
|
2794
2912
|
return;
|
|
2795
2913
|
const g = git(worktreePath);
|
|
2796
2914
|
await g.add([DOCS_PATHSPEC]);
|
|
@@ -3483,35 +3601,46 @@ function landProjectSession(ctx, session) {
|
|
|
3483
3601
|
const project = getProjectById(ctx, session.projectId);
|
|
3484
3602
|
if (!project)
|
|
3485
3603
|
return;
|
|
3486
|
-
const kept = `they are kept on ${PROJECT_BRANCH} and retried at the next launch`;
|
|
3487
|
-
const report = (type, message, data = {}) => {
|
|
3488
|
-
emitProject(ctx, project.id, {
|
|
3489
|
-
type,
|
|
3490
|
-
message,
|
|
3491
|
-
data: { sessionId: session.id, branch: PROJECT_BRANCH, ...data }
|
|
3492
|
-
});
|
|
3493
|
-
};
|
|
3494
3604
|
const landing = landProjectBranch(project).then((res) => {
|
|
3495
|
-
if (
|
|
3496
|
-
|
|
3497
|
-
if (res.landed) {
|
|
3498
|
-
report("project.landed", `landed ${res.commits} commit(s) from the project session onto ${project.mainBranch}`, { commits: res.commits });
|
|
3499
|
-
} else if (res.conflict) {
|
|
3500
|
-
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 ?? [] });
|
|
3501
|
-
} else {
|
|
3502
|
-
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 });
|
|
3503
|
-
}
|
|
3605
|
+
if (res)
|
|
3606
|
+
reportProjectLanding(ctx, project, res, { sessionId: session.id });
|
|
3504
3607
|
}).catch((e) => {
|
|
3505
|
-
|
|
3608
|
+
emitProject(ctx, project.id, {
|
|
3609
|
+
type: "project.land_failed",
|
|
3610
|
+
message: `could not land the project session's work onto ${project.mainBranch}: ${e instanceof Error ? e.message : String(e)} \u2014 ${LANDING_KEPT}`,
|
|
3611
|
+
data: {
|
|
3612
|
+
sessionId: session.id,
|
|
3613
|
+
branch: PROJECT_BRANCH,
|
|
3614
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3615
|
+
}
|
|
3616
|
+
});
|
|
3506
3617
|
});
|
|
3507
3618
|
inFlightLandings.add(landing);
|
|
3508
3619
|
landing.finally(() => inFlightLandings.delete(landing));
|
|
3509
3620
|
}
|
|
3621
|
+
function reportProjectLanding(ctx, project, res, data = {}) {
|
|
3622
|
+
const report = (type, message, extra) => {
|
|
3623
|
+
emitProject(ctx, project.id, {
|
|
3624
|
+
type,
|
|
3625
|
+
message,
|
|
3626
|
+
data: { branch: PROJECT_BRANCH, ...data, ...extra }
|
|
3627
|
+
});
|
|
3628
|
+
};
|
|
3629
|
+
if (res.landed) {
|
|
3630
|
+
report("project.landed", `landed ${res.commits} commit(s) from the project session onto ${project.mainBranch}`, { commits: res.commits });
|
|
3631
|
+
} else if (res.conflict) {
|
|
3632
|
+
report("project.land_conflict", `the project session's ${res.commits} commit(s) conflict with ${project.mainBranch} \u2014 nothing was overwritten; ${LANDING_KEPT}`, { commits: res.commits, files: res.files ?? [] });
|
|
3633
|
+
} else {
|
|
3634
|
+
report("project.land_failed", `could not land the project session's ${res.commits} commit(s) onto ${project.mainBranch}: ${res.error ?? "unknown error"} \u2014 ${LANDING_KEPT}`, { commits: res.commits, error: res.error ?? null });
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3510
3637
|
function markSessionEnded(ctx, id) {
|
|
3511
3638
|
const existing = getSessionRow(ctx, id);
|
|
3512
3639
|
if (!existing)
|
|
3513
3640
|
return null;
|
|
3514
3641
|
forgetKickoff(id);
|
|
3642
|
+
if (existing.featureId)
|
|
3643
|
+
stopDocsWatch(existing.featureId);
|
|
3515
3644
|
ctx.db.update(sessions).set({ status: "ended" }).where(eq5(sessions.id, id)).run();
|
|
3516
3645
|
return getSessionRow(ctx, id);
|
|
3517
3646
|
}
|
|
@@ -3534,11 +3663,12 @@ function mostRecentLiveSession(ctx) {
|
|
|
3534
3663
|
const row = ctx.db.select().from(sessions).where(eq5(sessions.status, "live")).orderBy(desc3(sql2`rowid`)).limit(1).get();
|
|
3535
3664
|
return row ? rowToSession(row) : null;
|
|
3536
3665
|
}
|
|
3537
|
-
var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, KICKOFF_CONFIRM_MS = 12000, KICKOFF_MAX_ATTEMPTS = 3, CLEAR_INPUT = "\x15", SESSION_READY_TIMEOUT_MS = 25000, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides, deliveries, MATCH_PREFIX = 40, landedProjectSessions, inFlightLandings;
|
|
3666
|
+
var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, KICKOFF_CONFIRM_MS = 12000, KICKOFF_MAX_ATTEMPTS = 3, CLEAR_INPUT = "\x15", SESSION_READY_TIMEOUT_MS = 25000, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides, deliveries, MATCH_PREFIX = 40, landedProjectSessions, inFlightLandings, LANDING_KEPT;
|
|
3538
3667
|
var init_sessions = __esm(() => {
|
|
3539
3668
|
init_src();
|
|
3540
3669
|
init_schema();
|
|
3541
3670
|
init_registry();
|
|
3671
|
+
init_docs_watch();
|
|
3542
3672
|
init_events();
|
|
3543
3673
|
init_git();
|
|
3544
3674
|
init_waypoints();
|
|
@@ -3558,6 +3688,7 @@ var init_sessions = __esm(() => {
|
|
|
3558
3688
|
deliveries = new Map;
|
|
3559
3689
|
landedProjectSessions = new Set;
|
|
3560
3690
|
inFlightLandings = new Set;
|
|
3691
|
+
LANDING_KEPT = `they are kept on ${PROJECT_BRANCH} and retried at the next launch`;
|
|
3561
3692
|
});
|
|
3562
3693
|
|
|
3563
3694
|
// src/launcher/reconcile.ts
|
|
@@ -3611,26 +3742,10 @@ async function getRuntimeCtx() {
|
|
|
3611
3742
|
}
|
|
3612
3743
|
var current = null;
|
|
3613
3744
|
|
|
3614
|
-
// src/services/feature-docs.ts
|
|
3615
|
-
import { existsSync as existsSync6 } from "fs";
|
|
3616
|
-
import { join as join6 } from "path";
|
|
3617
|
-
function featureDocsDir(project, feature) {
|
|
3618
|
-
const relSegments = featureDocsRel(feature.slug).split("/");
|
|
3619
|
-
const worktree = worktreeDir(project.id, feature.slug);
|
|
3620
|
-
const base = existsSync6(worktree) ? worktree : project.repoPath;
|
|
3621
|
-
return join6(base, ...relSegments);
|
|
3622
|
-
}
|
|
3623
|
-
function featureDocPath(project, feature, fileName) {
|
|
3624
|
-
return join6(featureDocsDir(project, feature), fileName);
|
|
3625
|
-
}
|
|
3626
|
-
var init_feature_docs = __esm(() => {
|
|
3627
|
-
init_paths();
|
|
3628
|
-
});
|
|
3629
|
-
|
|
3630
3745
|
// src/services/tickets.ts
|
|
3631
3746
|
import { and as and6, asc as asc3, eq as eq6 } from "drizzle-orm";
|
|
3632
3747
|
function rowToTicket(row) {
|
|
3633
|
-
return {
|
|
3748
|
+
return Ticket.parse({
|
|
3634
3749
|
id: row.id,
|
|
3635
3750
|
featureId: row.featureId,
|
|
3636
3751
|
seq: row.seq,
|
|
@@ -3645,8 +3760,9 @@ function rowToTicket(row) {
|
|
|
3645
3760
|
commits: row.commits,
|
|
3646
3761
|
error: row.error ?? undefined,
|
|
3647
3762
|
attemptBranch: row.attemptBranch ?? undefined,
|
|
3648
|
-
conflictFiles: row.conflictFiles ?? undefined
|
|
3649
|
-
|
|
3763
|
+
conflictFiles: row.conflictFiles ?? undefined,
|
|
3764
|
+
digest: row.digest ?? undefined
|
|
3765
|
+
});
|
|
3650
3766
|
}
|
|
3651
3767
|
function resolveBlocking(inputs, startSeq) {
|
|
3652
3768
|
try {
|
|
@@ -3682,7 +3798,8 @@ function storeTickets(ctx, featureId, inputs) {
|
|
|
3682
3798
|
commits: [],
|
|
3683
3799
|
error: null,
|
|
3684
3800
|
attemptBranch: null,
|
|
3685
|
-
conflictFiles: null
|
|
3801
|
+
conflictFiles: null,
|
|
3802
|
+
digest: null
|
|
3686
3803
|
}));
|
|
3687
3804
|
ctx.db.insert(tickets).values(rows).run();
|
|
3688
3805
|
emit(ctx, featureId, {
|
|
@@ -3769,6 +3886,8 @@ function updateTicket(ctx, id, patch) {
|
|
|
3769
3886
|
set.attemptBranch = patch.attemptBranch;
|
|
3770
3887
|
if (patch.conflictFiles !== undefined)
|
|
3771
3888
|
set.conflictFiles = patch.conflictFiles;
|
|
3889
|
+
if (patch.digest !== undefined)
|
|
3890
|
+
set.digest = patch.digest;
|
|
3772
3891
|
ctx.db.update(tickets).set(set).where(eq6(tickets.id, id)).run();
|
|
3773
3892
|
emit(ctx, current2.featureId, {
|
|
3774
3893
|
type: "ticket.updated",
|
|
@@ -3854,6 +3973,13 @@ function docGate(ctx, feature, fileName, reason) {
|
|
|
3854
3973
|
}
|
|
3855
3974
|
function overrideGate(ctx, featureId, gate, reason) {
|
|
3856
3975
|
const feature = getFeatureRow(ctx, featureId);
|
|
3976
|
+
const current2 = nextGate(feature);
|
|
3977
|
+
if (!current2) {
|
|
3978
|
+
throw new InvalidInputError(`cannot override ${gate}: feature is at ${feature.phase}, its final phase \u2014 no gate to cross`);
|
|
3979
|
+
}
|
|
3980
|
+
if (current2.id !== gate) {
|
|
3981
|
+
throw new InvalidInputError(`cannot override ${gate}: the feature is at ${feature.phase}, whose gate is ${current2.id}`);
|
|
3982
|
+
}
|
|
3857
3983
|
ctx.db.insert(gateOverrides).values({ featureId, gate, reason, ts: Date.now() }).run();
|
|
3858
3984
|
emit(ctx, featureId, {
|
|
3859
3985
|
type: "gate.overridden",
|
|
@@ -4410,14 +4536,21 @@ function buildIsolatedSetupCommand(tempBranch, setupCommand, pm) {
|
|
|
4410
4536
|
}
|
|
4411
4537
|
function buildWorkspaceNotes(mode) {
|
|
4412
4538
|
if (mode === "mounted") {
|
|
4413
|
-
return
|
|
4539
|
+
return [
|
|
4540
|
+
"Work in the current directory \u2014 it is the repo checkout on your branch.",
|
|
4541
|
+
"",
|
|
4542
|
+
"Write `DIGEST.md` at the root of that checkout, and leave it uncommitted \u2014 the host harvests it from disk."
|
|
4543
|
+
].join(`
|
|
4544
|
+
`);
|
|
4414
4545
|
}
|
|
4415
4546
|
return [
|
|
4416
4547
|
`Your working repository is \`${ISOLATED_REPO_PATH}\` \u2014 a clone on the container's fast native filesystem, with dependencies already installed. Do ALL work there: \`cd ${ISOLATED_REPO_PATH}\` first; every file you read, edit, test, and commit lives under it.`,
|
|
4417
4548
|
"",
|
|
4418
4549
|
`The directory you start in (\`${SANDBOX_WORKSPACE_PATH}\`) is a slow mounted mirror used only to collect your commits \u2014 never edit files, install, or run tests there. Your commits sync back automatically (a post-commit hook pushes them); just commit as normal. If you re-run the dependency install and it reconfigures git hooks (husky), run \`git -C ${ISOLATED_REPO_PATH} config core.hooksPath ${ISOLATED_REPO_PATH}/.git/hooks\` afterwards so the sync hook stays armed.`,
|
|
4419
4550
|
"",
|
|
4420
|
-
`If you are blocked and write \`BLOCKED.md\`, write it at \`${ISOLATED_REPO_PATH}/BLOCKED.md\` AND copy it to \`${SANDBOX_WORKSPACE_PATH}/BLOCKED.md\` so the orchestrator can see it
|
|
4551
|
+
`If you are blocked and write \`BLOCKED.md\`, write it at \`${ISOLATED_REPO_PATH}/BLOCKED.md\` AND copy it to \`${SANDBOX_WORKSPACE_PATH}/BLOCKED.md\` so the orchestrator can see it.`,
|
|
4552
|
+
"",
|
|
4553
|
+
`Write \`DIGEST.md\` at \`${SANDBOX_WORKSPACE_PATH}/DIGEST.md\` \u2014 the mounted mirror, so the host can see it \u2014 and NOT inside \`${ISOLATED_REPO_PATH}\`, where it would be committed with your work.`
|
|
4421
4554
|
].join(`
|
|
4422
4555
|
`);
|
|
4423
4556
|
}
|
|
@@ -4679,6 +4812,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
4679
4812
|
const status = new Map(tickets2.map((t) => [t.seq, t.status]));
|
|
4680
4813
|
const pending = new Set(tickets2.filter((t) => t.status === "pending").map((t) => t.seq));
|
|
4681
4814
|
const inFlight = new Map;
|
|
4815
|
+
const digests = [];
|
|
4682
4816
|
const satisfied = (s) => s === "done" || s === "cancelled";
|
|
4683
4817
|
const readyState = (seq) => {
|
|
4684
4818
|
const t = bySeq.get(seq);
|
|
@@ -4715,13 +4849,22 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
4715
4849
|
const outcome = await execute(ctx, t);
|
|
4716
4850
|
if (outcome.status === "done") {
|
|
4717
4851
|
status.set(seq, "done");
|
|
4718
|
-
ctx.updateTicket(t.id, { status: "done", commits: outcome.commits });
|
|
4852
|
+
ctx.updateTicket(t.id, { status: "done", commits: outcome.commits, digest: outcome.digest });
|
|
4719
4853
|
ctx.emitEvent({
|
|
4720
4854
|
type: "ticket.done",
|
|
4721
4855
|
message: `ticket ${t.seq} done \u2014 ${outcome.commits.length} commit(s)`,
|
|
4722
4856
|
ticketId: t.id,
|
|
4723
4857
|
data: { commits: outcome.commits }
|
|
4724
4858
|
});
|
|
4859
|
+
if (outcome.digest === undefined) {
|
|
4860
|
+
ctx.emitEvent({
|
|
4861
|
+
type: "digest.missing",
|
|
4862
|
+
message: `ticket ${t.seq} done without DIGEST.md`,
|
|
4863
|
+
ticketId: t.id
|
|
4864
|
+
});
|
|
4865
|
+
} else {
|
|
4866
|
+
digests.push({ seq: t.seq, title: t.title, digest: outcome.digest });
|
|
4867
|
+
}
|
|
4725
4868
|
} else {
|
|
4726
4869
|
failTicket(seq, outcome.error, outcome.event);
|
|
4727
4870
|
ctx.emitEvent({
|
|
@@ -4787,7 +4930,16 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
4787
4930
|
for (const s of status.values())
|
|
4788
4931
|
if (s === "done")
|
|
4789
4932
|
done += 1;
|
|
4790
|
-
return done;
|
|
4933
|
+
return { done, digests };
|
|
4934
|
+
}
|
|
4935
|
+
function composeRunDigest(entries) {
|
|
4936
|
+
if (entries.length === 0)
|
|
4937
|
+
return null;
|
|
4938
|
+
return [...entries].sort((a, b) => a.seq - b.seq).map((e) => `## ticket ${e.seq} \u2014 ${e.title}
|
|
4939
|
+
|
|
4940
|
+
${e.digest.trim()}`).join(`
|
|
4941
|
+
|
|
4942
|
+
`);
|
|
4791
4943
|
}
|
|
4792
4944
|
async function burnRun(ctx, deps) {
|
|
4793
4945
|
const tickets2 = ctx.tickets;
|
|
@@ -4808,10 +4960,11 @@ async function burnRun(ctx, deps) {
|
|
|
4808
4960
|
});
|
|
4809
4961
|
return { status: "failed", summary: `dependency cycle: ${path}` };
|
|
4810
4962
|
}
|
|
4811
|
-
const done = await burnTickets(ctx, tickets2, deps.executeTicketRun, deps.concurrency);
|
|
4963
|
+
const { done, digests } = await burnTickets(ctx, tickets2, deps.executeTicketRun, deps.concurrency);
|
|
4812
4964
|
const summary = cancelled > 0 ? `${done}/${total} tickets done (${cancelled} cancelled)` : `${done}/${total} tickets done`;
|
|
4813
4965
|
ctx.emitEvent({ type: "burn.summary", message: summary, data: { done, total, cancelled } });
|
|
4814
|
-
|
|
4966
|
+
const digest = composeRunDigest(digests);
|
|
4967
|
+
return { status: done === total ? "succeeded" : "failed", summary, ...digest ? { digest } : {} };
|
|
4815
4968
|
}
|
|
4816
4969
|
function burnerTemplatePath() {
|
|
4817
4970
|
return burnerAssetPath("implement-ticket.md");
|
|
@@ -4833,11 +4986,11 @@ function readDocsDigestFromDisk(projectId, slug) {
|
|
|
4833
4986
|
const files = readdirSync3(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync4(join9(docsDir, e.name), "utf8") }));
|
|
4834
4987
|
return buildDocsDigest(files);
|
|
4835
4988
|
}
|
|
4836
|
-
function
|
|
4989
|
+
function readAgentFile(dirs, file) {
|
|
4837
4990
|
for (const dir of dirs) {
|
|
4838
4991
|
if (!dir)
|
|
4839
4992
|
continue;
|
|
4840
|
-
const p = join9(dir,
|
|
4993
|
+
const p = join9(dir, file);
|
|
4841
4994
|
if (existsSync10(p)) {
|
|
4842
4995
|
try {
|
|
4843
4996
|
return readFileSync4(p, "utf8");
|
|
@@ -4846,10 +4999,26 @@ function readBlockedFile(dirs) {
|
|
|
4846
4999
|
}
|
|
4847
5000
|
return;
|
|
4848
5001
|
}
|
|
5002
|
+
function harvestDigest(dirs) {
|
|
5003
|
+
const trimmed = readAgentFile(dirs, "DIGEST.md")?.trim();
|
|
5004
|
+
return trimmed ? trimmed : undefined;
|
|
5005
|
+
}
|
|
5006
|
+
function buildAgentEnv(onHost, token) {
|
|
5007
|
+
const env = {};
|
|
5008
|
+
if (onHost) {
|
|
5009
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
5010
|
+
if (value !== undefined)
|
|
5011
|
+
env[key] = value;
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
if (token)
|
|
5015
|
+
env.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
5016
|
+
return env;
|
|
5017
|
+
}
|
|
4849
5018
|
function buildBurnAgent(config2, token, model) {
|
|
4850
5019
|
const onHost = config2.sandbox === "noSandbox";
|
|
4851
5020
|
const opts = {
|
|
4852
|
-
|
|
5021
|
+
env: buildAgentEnv(onHost, token),
|
|
4853
5022
|
...onHost ? { permissionMode: "bypassPermissions" } : {}
|
|
4854
5023
|
};
|
|
4855
5024
|
const agent = claudeCode(model, opts);
|
|
@@ -4873,8 +5042,10 @@ function selectSandbox(config2, mounts = []) {
|
|
|
4873
5042
|
return docker(imageOpts);
|
|
4874
5043
|
case "podman":
|
|
4875
5044
|
return podman(imageOpts);
|
|
4876
|
-
|
|
5045
|
+
case "noSandbox":
|
|
4877
5046
|
return noSandbox();
|
|
5047
|
+
default:
|
|
5048
|
+
throw new Error(`no sandbox provider for sandbox "${config2.sandbox}" \u2014 refusing to run the agent unsandboxed`);
|
|
4878
5049
|
}
|
|
4879
5050
|
}
|
|
4880
5051
|
function buildSandboxOptions(config2, mounts = []) {
|
|
@@ -5019,7 +5190,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
5019
5190
|
}
|
|
5020
5191
|
await cleanupBurnWorktree(project.repoPath, resolveBranch);
|
|
5021
5192
|
}
|
|
5022
|
-
const blocked =
|
|
5193
|
+
const blocked = readAgentFile([result?.preservedWorktreePath, project.repoPath], "BLOCKED.md");
|
|
5023
5194
|
if (blocked !== undefined && blocked.trim().length > 0) {
|
|
5024
5195
|
return {
|
|
5025
5196
|
ok: false,
|
|
@@ -5053,11 +5224,16 @@ ${blocked.trim()}`
|
|
|
5053
5224
|
label: `ticket ${ticket.seq}`,
|
|
5054
5225
|
featureBranch: feature.branch
|
|
5055
5226
|
};
|
|
5227
|
+
let harvestedDigest;
|
|
5056
5228
|
const landChain = async (branch, commits) => {
|
|
5057
5229
|
const landing = await landWithResolve(branch, landDeps);
|
|
5058
5230
|
if (landing.status === "landed") {
|
|
5059
5231
|
ctx.updateTicket(ticket.id, { attemptBranch: null, conflictFiles: null });
|
|
5060
|
-
return {
|
|
5232
|
+
return {
|
|
5233
|
+
status: "done",
|
|
5234
|
+
commits: landedCommits.length > 0 ? landedCommits : commits,
|
|
5235
|
+
digest: harvestedDigest
|
|
5236
|
+
};
|
|
5061
5237
|
}
|
|
5062
5238
|
preserveChain(landing.branch);
|
|
5063
5239
|
if (landing.status === "conflict") {
|
|
@@ -5218,7 +5394,9 @@ ${retryNotes}` : basePrompt,
|
|
|
5218
5394
|
}
|
|
5219
5395
|
if (!result)
|
|
5220
5396
|
return { status: "failed", error: "internal: run loop produced no result" };
|
|
5221
|
-
const
|
|
5397
|
+
const agentFileDirs = [result.preservedWorktreePath, project.repoPath];
|
|
5398
|
+
const blocked = readAgentFile(agentFileDirs, "BLOCKED.md");
|
|
5399
|
+
harvestedDigest = harvestDigest(agentFileDirs);
|
|
5222
5400
|
const chain = await branchCommitsAhead(project.repoPath, feature.branch, tempBranch);
|
|
5223
5401
|
let outcome;
|
|
5224
5402
|
if (result.commits.length === 0 && blocked !== undefined && blocked.trim().length > 0) {
|
|
@@ -5352,8 +5530,6 @@ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as re
|
|
|
5352
5530
|
import { dirname as dirname4, join as join10 } from "path";
|
|
5353
5531
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5354
5532
|
import { run as run2 } from "@ai-hero/sandcastle";
|
|
5355
|
-
import { docker as docker2 } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
5356
|
-
import { noSandbox as noSandbox2 } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
|
|
5357
5533
|
function waypointSlug(waypoint) {
|
|
5358
5534
|
const base = waypoint.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5359
5535
|
return `${waypoint.seq}-${base || "waypoint"}`;
|
|
@@ -5367,7 +5543,7 @@ async function researchRun(ctx, deps) {
|
|
|
5367
5543
|
ctx.emitEvent({ type: "research.error", message: "research run started without a waypoint" });
|
|
5368
5544
|
return { status: "failed", summary: "no waypoint to research" };
|
|
5369
5545
|
}
|
|
5370
|
-
if (deps.config.sandbox
|
|
5546
|
+
if (deps.config.sandbox !== "noSandbox" && !deps.hasAuthToken) {
|
|
5371
5547
|
ctx.emitEvent({ type: AUTH_MISSING_EVENT2, message: AUTH_MISSING_MESSAGE2 });
|
|
5372
5548
|
return { status: "failed", summary: "research aborted: auth token missing" };
|
|
5373
5549
|
}
|
|
@@ -5449,7 +5625,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
5449
5625
|
const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
|
|
5450
5626
|
const runOptions = {
|
|
5451
5627
|
agent: buildBurnAgent(config2, token, model),
|
|
5452
|
-
sandbox:
|
|
5628
|
+
sandbox: selectSandbox(config2),
|
|
5453
5629
|
cwd: project.repoPath,
|
|
5454
5630
|
prompt,
|
|
5455
5631
|
branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
|
|
@@ -5625,10 +5801,12 @@ function cancelRun(runId) {
|
|
|
5625
5801
|
async function executeRun(ctx, runId, featureId, workflow2, runPromise, controller, cleanup) {
|
|
5626
5802
|
let status = "failed";
|
|
5627
5803
|
let summary = "run failed";
|
|
5804
|
+
let digest;
|
|
5628
5805
|
try {
|
|
5629
5806
|
const result = await runPromise;
|
|
5630
5807
|
status = result.status;
|
|
5631
5808
|
summary = result.summary;
|
|
5809
|
+
digest = result.digest;
|
|
5632
5810
|
} catch (e) {
|
|
5633
5811
|
if (controller.signal.aborted) {
|
|
5634
5812
|
status = "cancelled";
|
|
@@ -5641,7 +5819,7 @@ async function executeRun(ctx, runId, featureId, workflow2, runPromise, controll
|
|
|
5641
5819
|
} finally {
|
|
5642
5820
|
controllers.delete(runId);
|
|
5643
5821
|
}
|
|
5644
|
-
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary }).where(eq8(runs.id, runId)).run();
|
|
5822
|
+
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary, ...digest ? { digest } : {} }).where(eq8(runs.id, runId)).run();
|
|
5645
5823
|
releaseForSession(ctx, runId);
|
|
5646
5824
|
if (workflowClaimsFeatureBranch(workflow2)) {
|
|
5647
5825
|
sweepOrphanedBurning(ctx, featureId, `orphaned \u2014 the run ended (${status}) while it was burning`);
|
|
@@ -5696,36 +5874,73 @@ async function createFeature(ctx, input) {
|
|
|
5696
5874
|
const slug = uniqueSlug(ctx, project.id, input.title);
|
|
5697
5875
|
const branch = `feature/${slug}`;
|
|
5698
5876
|
const requestedBase = input.baseBranch?.trim() || project.mainBranch;
|
|
5699
|
-
const
|
|
5877
|
+
const cut = input.draft ? null : await ensureFeatureBranch(project, slug, requestedBase);
|
|
5700
5878
|
const row = {
|
|
5701
5879
|
id: newId("feat"),
|
|
5702
5880
|
projectId: project.id,
|
|
5703
5881
|
slug,
|
|
5704
5882
|
title: input.title,
|
|
5705
5883
|
oneLiner: input.oneLiner,
|
|
5884
|
+
brief: input.draft ? input.brief ?? null : null,
|
|
5706
5885
|
mapped: false,
|
|
5707
5886
|
lap: 1,
|
|
5708
5887
|
phase: "ideation",
|
|
5709
5888
|
branch,
|
|
5710
|
-
baseBranch,
|
|
5711
|
-
status: "active",
|
|
5889
|
+
baseBranch: cut?.baseBranch ?? null,
|
|
5890
|
+
status: cut ? "active" : "draft",
|
|
5712
5891
|
createdAt: Date.now()
|
|
5713
5892
|
};
|
|
5714
5893
|
const inserted = ctx.db.insert(features).values(row).returning().get();
|
|
5715
5894
|
const feature = rowToFeature(inserted);
|
|
5716
5895
|
emit(ctx, feature.id, {
|
|
5717
5896
|
type: "feature.created",
|
|
5718
|
-
message: branchReady ? `feature.created (${branch} \u2190 ${baseBranch})` : "feature.created (branch pending)",
|
|
5719
|
-
data: {
|
|
5897
|
+
message: !cut ? `feature.created (draft \u2014 ${branch} not cut yet)` : cut.branchReady ? `feature.created (${branch} \u2190 ${cut.baseBranch})` : "feature.created (branch pending)",
|
|
5898
|
+
data: {
|
|
5899
|
+
slug,
|
|
5900
|
+
branch,
|
|
5901
|
+
baseBranch: cut?.baseBranch,
|
|
5902
|
+
branchReady: cut?.branchReady ?? false,
|
|
5903
|
+
draft: !cut
|
|
5904
|
+
}
|
|
5720
5905
|
});
|
|
5906
|
+
if (!cut)
|
|
5907
|
+
return feature;
|
|
5721
5908
|
scaffoldDocs(ctx, feature, { brief: input.brief });
|
|
5722
|
-
if (branchReady) {
|
|
5909
|
+
if (cut.branchReady) {
|
|
5723
5910
|
try {
|
|
5724
5911
|
await commitDocs(project.repoPath, `runcastle: scaffold ${slug} docs`);
|
|
5725
5912
|
} catch {}
|
|
5726
5913
|
}
|
|
5727
5914
|
return feature;
|
|
5728
5915
|
}
|
|
5916
|
+
function requireNotDraft(feature) {
|
|
5917
|
+
if (feature.status === "draft") {
|
|
5918
|
+
throw new GateError(`\`${feature.slug}\` is a draft \u2014 click Start to cut its branch and begin`);
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
async function startDraft(ctx, featureId, opts = {}) {
|
|
5922
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
5923
|
+
if (feature.status !== "draft") {
|
|
5924
|
+
throw new GateError(`feature ${feature.slug} is not a draft \u2014 it has already been started`);
|
|
5925
|
+
}
|
|
5926
|
+
const project = projectForFeature(ctx, feature);
|
|
5927
|
+
const requestedBase = opts.baseBranch?.trim() || project.mainBranch;
|
|
5928
|
+
const { branchReady, baseBranch } = await ensureFeatureBranch(project, feature.slug, requestedBase);
|
|
5929
|
+
ctx.db.update(features).set({ status: "active", baseBranch }).where(eq9(features.id, featureId)).run();
|
|
5930
|
+
const started = { ...feature, status: "active", baseBranch };
|
|
5931
|
+
emit(ctx, featureId, {
|
|
5932
|
+
type: "feature.started",
|
|
5933
|
+
message: branchReady ? `feature ${feature.slug} started (${feature.branch} \u2190 ${baseBranch})` : `feature ${feature.slug} started (branch pending)`,
|
|
5934
|
+
data: { branch: feature.branch, baseBranch, branchReady }
|
|
5935
|
+
});
|
|
5936
|
+
scaffoldDocs(ctx, started, { brief: feature.brief });
|
|
5937
|
+
if (branchReady) {
|
|
5938
|
+
try {
|
|
5939
|
+
await commitDocs(project.repoPath, `runcastle: scaffold ${feature.slug} docs`);
|
|
5940
|
+
} catch {}
|
|
5941
|
+
}
|
|
5942
|
+
return started;
|
|
5943
|
+
}
|
|
5729
5944
|
async function quickChange(ctx, input) {
|
|
5730
5945
|
const project = requireProjectById(ctx, input.projectId);
|
|
5731
5946
|
const title = input.title.trim();
|
|
@@ -5844,6 +6059,7 @@ function liveSessionOf(ctx, featureId) {
|
|
|
5844
6059
|
}
|
|
5845
6060
|
function advance(ctx, featureId) {
|
|
5846
6061
|
const feature = getFeatureRow(ctx, featureId);
|
|
6062
|
+
requireNotDraft(feature);
|
|
5847
6063
|
const gate = nextGate(feature);
|
|
5848
6064
|
if (!gate)
|
|
5849
6065
|
throw new GateError("feature is already at the final phase");
|
|
@@ -5861,6 +6077,7 @@ function advance(ctx, featureId) {
|
|
|
5861
6077
|
}
|
|
5862
6078
|
async function burn(ctx, featureId, opts = {}) {
|
|
5863
6079
|
const feature = getFeatureRow(ctx, featureId);
|
|
6080
|
+
requireNotDraft(feature);
|
|
5864
6081
|
const running = hasActiveRun(ctx, featureId);
|
|
5865
6082
|
let tickets2 = listByFeature2(ctx, featureId);
|
|
5866
6083
|
const lapTickets = tickets2.filter((t) => t.lap === feature.lap);
|
|
@@ -5911,6 +6128,7 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
5911
6128
|
}
|
|
5912
6129
|
function rethink(ctx, featureId) {
|
|
5913
6130
|
const feature = getFeatureRow(ctx, featureId);
|
|
6131
|
+
requireNotDraft(feature);
|
|
5914
6132
|
if (feature.phase !== RETHINK_LOOP_BACK.from) {
|
|
5915
6133
|
throw new GateError(`feature must be in the review phase to rethink (currently ${feature.phase})`);
|
|
5916
6134
|
}
|
|
@@ -6024,6 +6242,7 @@ function escalateToMap(ctx, featureId, input) {
|
|
|
6024
6242
|
}
|
|
6025
6243
|
function archiveFeature(ctx, featureId) {
|
|
6026
6244
|
const feature = getFeatureRow(ctx, featureId);
|
|
6245
|
+
requireNotDraft(feature);
|
|
6027
6246
|
if (feature.status === "archived") {
|
|
6028
6247
|
throw new GateError(`feature ${feature.slug} is already archived`);
|
|
6029
6248
|
}
|
|
@@ -6307,8 +6526,19 @@ async function toolDryRunDrive(ctx, session, input) {
|
|
|
6307
6526
|
}
|
|
6308
6527
|
return dryRunDrive(ctx, project, input.action);
|
|
6309
6528
|
}
|
|
6529
|
+
function createFeatureProject(ctx, session, input) {
|
|
6530
|
+
if (isProjectSessionKind(session.kind))
|
|
6531
|
+
return requireProject(ctx, session);
|
|
6532
|
+
if (!DRAFTING_KINDS.includes(session.kind)) {
|
|
6533
|
+
throw new GateError(`a ${session.kind} session is read-only and may not create features, drafts included. ` + "Tell the human what is worth capturing; the project session is where features are made.");
|
|
6534
|
+
}
|
|
6535
|
+
if (!input.draft || input.ticket) {
|
|
6536
|
+
throw new GateError(`a ${session.kind} session may only PARK a feature here: call create_feature with ` + "`draft: true` and a `brief` carrying why you deferred it. Cutting a branch \u2014 a full " + "create or the quick-change `ticket` shape \u2014 belongs to the project session; tell the " + "human to open it.");
|
|
6537
|
+
}
|
|
6538
|
+
return projectForFeature(ctx, getFeatureRow(ctx, requireFeatureId(session)));
|
|
6539
|
+
}
|
|
6310
6540
|
async function toolCreateFeature(ctx, session, input) {
|
|
6311
|
-
const project =
|
|
6541
|
+
const project = createFeatureProject(ctx, session, input);
|
|
6312
6542
|
const feature = input.ticket ? await quickChange(ctx, {
|
|
6313
6543
|
projectId: project.id,
|
|
6314
6544
|
title: input.title,
|
|
@@ -6319,7 +6549,8 @@ async function toolCreateFeature(ctx, session, input) {
|
|
|
6319
6549
|
title: input.title,
|
|
6320
6550
|
oneLiner: input.oneLiner,
|
|
6321
6551
|
baseBranch: input.baseBranch,
|
|
6322
|
-
brief: input.brief
|
|
6552
|
+
brief: input.brief,
|
|
6553
|
+
draft: input.draft
|
|
6323
6554
|
});
|
|
6324
6555
|
return {
|
|
6325
6556
|
id: feature.id,
|
|
@@ -6380,7 +6611,8 @@ function toolGetWorkRecord(ctx, session, input) {
|
|
|
6380
6611
|
status: t.status,
|
|
6381
6612
|
seams: t.seams,
|
|
6382
6613
|
commits: t.commits,
|
|
6383
|
-
...t.error !== undefined ? { error: t.error } : {}
|
|
6614
|
+
...t.error !== undefined ? { error: t.error } : {},
|
|
6615
|
+
...t.digest !== undefined ? { digest: t.digest } : {}
|
|
6384
6616
|
}))
|
|
6385
6617
|
});
|
|
6386
6618
|
}
|
|
@@ -6434,12 +6666,13 @@ function buildMcpServer() {
|
|
|
6434
6666
|
});
|
|
6435
6667
|
server.registerTool("create_feature", {
|
|
6436
6668
|
title: "Create a feature",
|
|
6437
|
-
description: "Create a feature from the project session \u2014 the end of intake. Pass `brief` with the " + "reasoning you just worked out with the human (why this feature exists, what it must NOT " + "swallow): it becomes brief.md verbatim, and without it that reasoning is lost. Pass " + "`ticket: { prose }` for a quick change \u2014 work too small to deserve a grill \u2014 which " + "creates the feature at the implementation phase with that one ticket, and whose brief.md " + "is the prose itself (so `brief` is unused there). This does NOT open " + "a terminal on what it creates; the new card in the rail is the feedback, and the human " + "decides what to work on next.",
|
|
6669
|
+
description: "Create a feature from the project session \u2014 the end of intake. Pass `brief` with the " + "reasoning you just worked out with the human (why this feature exists, what it must NOT " + "swallow): it becomes brief.md verbatim, and without it that reasoning is lost. Pass " + "`draft: true` to PARK it instead of starting it \u2014 a row and its brief, no branch and no " + "files, until the human clicks Start; ask them per feature whether to start it now or " + "park it. Pass " + "`ticket: { prose }` for a quick change \u2014 work too small to deserve a grill \u2014 which " + "creates the feature at the implementation phase with that one ticket, and whose brief.md " + "is the prose itself (so `brief` is unused there). From a feature session (ideation, " + "revisit, waypoint, converge) this tool parks drafts and nothing else: `draft: true` is " + "required and the `ticket` shape is refused, so deflect scope creep here and leave full " + "creation to the project session. This does NOT open " + "a terminal on what it creates; the new card in the rail is the feedback, and the human " + "decides what to work on next.",
|
|
6438
6670
|
inputSchema: {
|
|
6439
6671
|
title: z3.string().min(1),
|
|
6440
6672
|
oneLiner: z3.string(),
|
|
6441
6673
|
baseBranch: z3.string().optional(),
|
|
6442
6674
|
brief: z3.string().optional(),
|
|
6675
|
+
draft: z3.boolean().optional(),
|
|
6443
6676
|
ticket: z3.object({ prose: z3.string().min(1) }).optional()
|
|
6444
6677
|
}
|
|
6445
6678
|
}, async (args, extra) => {
|
|
@@ -6460,7 +6693,7 @@ function buildMcpServer() {
|
|
|
6460
6693
|
});
|
|
6461
6694
|
server.registerTool("get_work_record", {
|
|
6462
6695
|
title: "Get work record",
|
|
6463
|
-
description: "What features actually DID \u2014 facts only: per feature its status, ship date, run " + "summaries and tickets as { seq, title, status, seams, commits, error? }.
|
|
6696
|
+
description: "What features actually DID \u2014 facts only: per feature its status, ship date, run " + "summaries and tickets as { seq, title, status, seams, commits, error?, digest? }. The " + "digest is the burner's own account, written after the work: what it did, what " + "surprised it, what it left undone. Never a ticket's goal or acceptance criteria: " + "those are intent from before the code existed. " + 'Query by `featureSlug` ("what did X do?") or by `seam` ("who has touched this area, and ' + 'what happened?") \u2014 the seam match is a case-insensitive substring, because seams are ' + "free prose that differs between features. At least one argument is required.",
|
|
6464
6697
|
inputSchema: { featureSlug: z3.string().optional(), seam: z3.string().optional() }
|
|
6465
6698
|
}, async (args, extra) => {
|
|
6466
6699
|
const rs = await resolveCtxSession(extra);
|
|
@@ -6583,7 +6816,7 @@ function buildMcpServer() {
|
|
|
6583
6816
|
});
|
|
6584
6817
|
return server;
|
|
6585
6818
|
}
|
|
6586
|
-
var mcp, server_default;
|
|
6819
|
+
var DRAFTING_KINDS, mcp, server_default;
|
|
6587
6820
|
var init_server = __esm(() => {
|
|
6588
6821
|
init_src();
|
|
6589
6822
|
init_paths();
|
|
@@ -6597,6 +6830,7 @@ var init_server = __esm(() => {
|
|
|
6597
6830
|
init_repo();
|
|
6598
6831
|
init_tickets();
|
|
6599
6832
|
init_waypoints();
|
|
6833
|
+
DRAFTING_KINDS = ["ideation", "revisit", "waypoint", "converge"];
|
|
6600
6834
|
mcp = new Hono;
|
|
6601
6835
|
mcp.use("*", async (c, next) => {
|
|
6602
6836
|
await next();
|
|
@@ -6694,21 +6928,23 @@ function evaluateEditGuard(input) {
|
|
|
6694
6928
|
return null;
|
|
6695
6929
|
if (!input.filePath)
|
|
6696
6930
|
return null;
|
|
6931
|
+
const target = resolve7(input.worktreePath, input.filePath);
|
|
6697
6932
|
if (!input.featureSlug) {
|
|
6698
6933
|
return {
|
|
6699
6934
|
reason: `This ${input.kind} session does not edit files \u2014 it runs in the developer's own ` + "checkout. Record what you establish with the `record_finding` MCP tool, and ask the " + "human to make any change to the repo itself."
|
|
6700
6935
|
};
|
|
6701
6936
|
}
|
|
6702
6937
|
const docs = featureDocsRel(input.featureSlug);
|
|
6703
|
-
|
|
6704
|
-
const target = resolve7(input.worktreePath, input.filePath);
|
|
6705
|
-
const rel = relative2(docsDir, target);
|
|
6706
|
-
if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel))
|
|
6938
|
+
if (within(resolve7(input.worktreePath, docs), target))
|
|
6707
6939
|
return null;
|
|
6708
6940
|
return {
|
|
6709
6941
|
reason: `Talk sessions do not write code. This ${input.kind} session may only write this ` + `feature's docs under \`${docs}/\` \u2014 ${input.filePath} is outside them. The change you ` + "want belongs in a ticket: capture the decision in `decisions.md`, amend `spec.md`, and " + "emit a ticket for the work. An implementation agent burns it in its own sandbox."
|
|
6710
6942
|
};
|
|
6711
6943
|
}
|
|
6944
|
+
function within(dir, target) {
|
|
6945
|
+
const rel = relative2(dir, target);
|
|
6946
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
6947
|
+
}
|
|
6712
6948
|
function editDenyResponse(denial) {
|
|
6713
6949
|
return {
|
|
6714
6950
|
hookSpecificOutput: {
|
|
@@ -7284,6 +7520,28 @@ var init_update_check = __esm(() => {
|
|
|
7284
7520
|
NPM_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
7285
7521
|
});
|
|
7286
7522
|
|
|
7523
|
+
// src/shutdown.ts
|
|
7524
|
+
function createShutdown(deps, timeoutMs = SHUTDOWN_TIMEOUT_MS) {
|
|
7525
|
+
let settling = false;
|
|
7526
|
+
return () => {
|
|
7527
|
+
if (settling) {
|
|
7528
|
+
deps.exit(1);
|
|
7529
|
+
return;
|
|
7530
|
+
}
|
|
7531
|
+
settling = true;
|
|
7532
|
+
(async () => {
|
|
7533
|
+
const deadline = new Promise((resolve9) => {
|
|
7534
|
+
const timer = setTimeout(resolve9, timeoutMs);
|
|
7535
|
+
timer.unref?.();
|
|
7536
|
+
});
|
|
7537
|
+
await Promise.race([deps.killAllTrees().catch(() => {}), deadline]);
|
|
7538
|
+
deps.stop();
|
|
7539
|
+
deps.exit(0);
|
|
7540
|
+
})();
|
|
7541
|
+
};
|
|
7542
|
+
}
|
|
7543
|
+
var SHUTDOWN_TIMEOUT_MS = 5000;
|
|
7544
|
+
|
|
7287
7545
|
// src/trpc/context.ts
|
|
7288
7546
|
import { initTRPC } from "@trpc/server";
|
|
7289
7547
|
var t, errorMapping, router, publicProcedure, createCallerFactory;
|
|
@@ -7344,13 +7602,16 @@ function serverUrlFor(config2) {
|
|
|
7344
7602
|
function noCodeRule(docs) {
|
|
7345
7603
|
return `- **Talk sessions do not write code.** You may write this feature's docs under \`${docs}/\` ` + "and nothing else in this checkout. Every code change rides a ticket, burned by an " + "implementation agent in its own sandbox \u2014 even the one-line fix that is obviously faster " + "to just do. This is enforced by a hook, not left to your judgement: edits outside the docs " + "are denied.";
|
|
7346
7604
|
}
|
|
7347
|
-
function
|
|
7605
|
+
function conflictResolutionRule() {
|
|
7606
|
+
return "- **This session resolves a merge conflict, so it DOES write code here.** Edit the " + "conflicted files in this checkout, resolving them from the feature docs\u2019 intent, and " + "commit the merge. That is the exception and its whole extent: the edit guard exempts " + "writes only while the merge is in progress, so work the merge revealed but did not " + "cause still rides a ticket.";
|
|
7607
|
+
}
|
|
7608
|
+
function renderSystemPrompt(feature, kind, waypoint, lap, purpose) {
|
|
7348
7609
|
if (kind === "waypoint")
|
|
7349
7610
|
return renderWaypointPrompt(feature, waypoint);
|
|
7350
7611
|
if (kind === "converge")
|
|
7351
7612
|
return renderConvergePrompt(feature);
|
|
7352
7613
|
if (kind === "revisit")
|
|
7353
|
-
return renderRevisitPrompt(feature, lap);
|
|
7614
|
+
return renderRevisitPrompt(feature, lap, purpose);
|
|
7354
7615
|
const docs = featureDocsRel(feature.slug);
|
|
7355
7616
|
const entry = kind === "ideation" ? "Begin by invoking the `/runcastle:ideate` skill and drive the ideation session to completion." : "This is a Q&A session: invoke `/runcastle:qa`. Answer questions from the docs + code. Do NOT advance phases or emit tickets.";
|
|
7356
7617
|
return [
|
|
@@ -7479,7 +7740,7 @@ function renderConvergePrompt(feature) {
|
|
|
7479
7740
|
].join(`
|
|
7480
7741
|
`);
|
|
7481
7742
|
}
|
|
7482
|
-
function renderRevisitPrompt(feature, lap) {
|
|
7743
|
+
function renderRevisitPrompt(feature, lap, purpose) {
|
|
7483
7744
|
const docs = featureDocsRel(feature.slug);
|
|
7484
7745
|
const lapIteration = lap ? [
|
|
7485
7746
|
`## This is lap ${lap}`,
|
|
@@ -7549,7 +7810,7 @@ function renderRevisitPrompt(feature, lap) {
|
|
|
7549
7810
|
lap ? "- DO call `complete_phase` \u2014 this lap advances ideation \u2192 spec \u2192 tickets, and only you can." : "- Do NOT call `complete_phase` \u2014 a revisit never moves the pipeline.",
|
|
7550
7811
|
"- Do NOT touch `done`/`burning` tickets; if done work is now wrong, emit a new ticket that fixes it.",
|
|
7551
7812
|
"- Docs first, tickets second: capture the decision prose before any ticket surgery.",
|
|
7552
|
-
noCodeRule(docs),
|
|
7813
|
+
purpose === "resolve-conflict" ? conflictResolutionRule() : noCodeRule(docs),
|
|
7553
7814
|
"",
|
|
7554
7815
|
"## Your task",
|
|
7555
7816
|
"Invoke the `/runcastle:revisit` skill and work through what the human brings up.",
|
|
@@ -7707,7 +7968,8 @@ function renderProjectPrompt(brief) {
|
|
|
7707
7968
|
"- `get_project_context()` \u2014 the project, its charter, its live ADRs, and a one-line",
|
|
7708
7969
|
" index of every feature.",
|
|
7709
7970
|
"- `get_work_record({ featureSlug? | seam? })` \u2014 what features actually did: tickets by",
|
|
7710
|
-
" status, seams, commits, errors
|
|
7971
|
+
" status, seams, commits, errors, and each burner's digest of what it actually did,",
|
|
7972
|
+
" what surprised it and what it left undone. Facts, never intent.",
|
|
7711
7973
|
"- `record_event({ type, message })` \u2014 drop a note on the project timeline.",
|
|
7712
7974
|
"",
|
|
7713
7975
|
"Every merged feature's docs are already on disk in this worktree \u2014 read them with your",
|
|
@@ -7756,13 +8018,13 @@ function renderMcpConfig(session, config2) {
|
|
|
7756
8018
|
};
|
|
7757
8019
|
}
|
|
7758
8020
|
async function writeSessionArtifacts(input) {
|
|
7759
|
-
const { session, feature, config: config2, waypoint, prepare, projectBrief, lap } = input;
|
|
8021
|
+
const { session, feature, config: config2, waypoint, prepare, projectBrief, lap, purpose } = input;
|
|
7760
8022
|
const dir = sessionDir(session.id);
|
|
7761
8023
|
mkdirSync7(dir, { recursive: true });
|
|
7762
8024
|
const systemPromptPath = join13(dir, "system-prompt.md");
|
|
7763
8025
|
const settingsPath = join13(dir, "settings.json");
|
|
7764
8026
|
const mcpConfigPath = join13(dir, "mcp.json");
|
|
7765
|
-
const systemPrompt = feature ? renderSystemPrompt(feature, session.kind, waypoint, lap) : prepare ? renderPreparePrompt(prepare) : projectBrief ? renderProjectPrompt(projectBrief) : (() => {
|
|
8027
|
+
const systemPrompt = feature ? renderSystemPrompt(feature, session.kind, waypoint, lap, purpose) : prepare ? renderPreparePrompt(prepare) : projectBrief ? renderProjectPrompt(projectBrief) : (() => {
|
|
7766
8028
|
throw new Error(`session ${session.id} has no feature and no project-session brief`);
|
|
7767
8029
|
})();
|
|
7768
8030
|
writeFileSync3(systemPromptPath, systemPrompt, "utf8");
|
|
@@ -7885,8 +8147,10 @@ function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
|
7885
8147
|
}
|
|
7886
8148
|
}
|
|
7887
8149
|
function sessionFinished(ctx, feature, session) {
|
|
7888
|
-
if (session.kind
|
|
8150
|
+
if (session.kind === "ideation" || session.kind === "converge")
|
|
7889
8151
|
return feature.mapped;
|
|
8152
|
+
if (session.kind !== "waypoint")
|
|
8153
|
+
return false;
|
|
7890
8154
|
const own = listByFeature(ctx, feature.id).find((w) => w.lastSessionId === session.id);
|
|
7891
8155
|
return !!own && (own.status === "resolved" || own.status === "dropped");
|
|
7892
8156
|
}
|
|
@@ -7925,6 +8189,7 @@ async function ensureWorktree(ctx, project, feature) {
|
|
|
7925
8189
|
}
|
|
7926
8190
|
async function launchSession(ctx, input, opts = {}) {
|
|
7927
8191
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
8192
|
+
requireNotDraft(feature);
|
|
7928
8193
|
const project = projectForFeature(ctx, feature);
|
|
7929
8194
|
const worktreePath = await ensureWorktree(ctx, project, feature);
|
|
7930
8195
|
const session = createSessionRow(ctx, {
|
|
@@ -8007,7 +8272,8 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
8007
8272
|
project,
|
|
8008
8273
|
config: ctx.config,
|
|
8009
8274
|
waypoint,
|
|
8010
|
-
lap: plan.lap
|
|
8275
|
+
lap: plan.lap,
|
|
8276
|
+
purpose: input.purpose
|
|
8011
8277
|
});
|
|
8012
8278
|
const serverUrl = serverUrlFor(ctx.config);
|
|
8013
8279
|
const buildInput = {
|
|
@@ -8107,7 +8373,7 @@ async function launchProjectSession(ctx, input, opts = {}) {
|
|
|
8107
8373
|
if (live) {
|
|
8108
8374
|
throw new GateError(`a project session is already open for ${project.name} \u2014 resume or end it first`);
|
|
8109
8375
|
}
|
|
8110
|
-
const worktreePath = await ensureProjectWorktree(project);
|
|
8376
|
+
const worktreePath = await ensureProjectWorktree(project, (res) => reportProjectLanding(ctx, project, res, { retried: true }));
|
|
8111
8377
|
const session = createSessionRow(ctx, {
|
|
8112
8378
|
projectId: project.id,
|
|
8113
8379
|
kind: "project",
|
|
@@ -8178,6 +8444,7 @@ async function buildPrepareBrief(ctx, project) {
|
|
|
8178
8444
|
}
|
|
8179
8445
|
async function workWaypoint(ctx, input, opts = {}) {
|
|
8180
8446
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
8447
|
+
requireNotDraft(feature);
|
|
8181
8448
|
if (!feature.mapped) {
|
|
8182
8449
|
throw new GateError(`feature ${feature.slug} is not mapped \u2014 it has no waypoints to work`);
|
|
8183
8450
|
}
|
|
@@ -8198,6 +8465,7 @@ async function workWaypoint(ctx, input, opts = {}) {
|
|
|
8198
8465
|
}
|
|
8199
8466
|
async function converge(ctx, input, opts = {}) {
|
|
8200
8467
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
8468
|
+
requireNotDraft(feature);
|
|
8201
8469
|
if (!feature.mapped) {
|
|
8202
8470
|
throw new GateError(`feature ${feature.slug} is not mapped \u2014 convergence is only for mapped features`);
|
|
8203
8471
|
}
|
|
@@ -8296,6 +8564,8 @@ function spawnEmbeddedPty(ctx, feature, session, worktreePath, serverUrl, claude
|
|
|
8296
8564
|
opts: { cwd: worktreePath, env, cols: 80, rows: 24, useConpty: true },
|
|
8297
8565
|
onExit: ({ exitCode }) => handlePtyExit(ctx, feature, session, meta, exitCode)
|
|
8298
8566
|
});
|
|
8567
|
+
if (feature)
|
|
8568
|
+
startDocsWatch(ctx, feature);
|
|
8299
8569
|
emitForSession(ctx, session, {
|
|
8300
8570
|
type: "session.launched",
|
|
8301
8571
|
message: "embedded terminal spawned",
|
|
@@ -8322,10 +8592,12 @@ var init_launcher = __esm(() => {
|
|
|
8322
8592
|
init_errors();
|
|
8323
8593
|
init_end_session();
|
|
8324
8594
|
init_registry();
|
|
8595
|
+
init_docs_watch();
|
|
8325
8596
|
init_events();
|
|
8326
8597
|
init_gates();
|
|
8327
8598
|
init_git();
|
|
8328
8599
|
init_repo();
|
|
8600
|
+
init_features();
|
|
8329
8601
|
init_tickets();
|
|
8330
8602
|
init_waypoints();
|
|
8331
8603
|
init_runner();
|
|
@@ -8347,6 +8619,74 @@ var init_launcher = __esm(() => {
|
|
|
8347
8619
|
];
|
|
8348
8620
|
});
|
|
8349
8621
|
|
|
8622
|
+
// src/services/outcome.ts
|
|
8623
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
8624
|
+
import { join as join15 } from "path";
|
|
8625
|
+
function composeOutcomeDoc(feature, tickets2, shippedAt) {
|
|
8626
|
+
const blocks = [
|
|
8627
|
+
`# Outcome \u2014 ${feature.title}`,
|
|
8628
|
+
feature.oneLiner,
|
|
8629
|
+
[`- Shipped: ${shippedDate(shippedAt)}`, `- Lap: ${feature.lap}`].join(`
|
|
8630
|
+
`)
|
|
8631
|
+
];
|
|
8632
|
+
let oneLiners = [];
|
|
8633
|
+
const flush = () => {
|
|
8634
|
+
if (oneLiners.length > 0)
|
|
8635
|
+
blocks.push(oneLiners.join(`
|
|
8636
|
+
`));
|
|
8637
|
+
oneLiners = [];
|
|
8638
|
+
};
|
|
8639
|
+
for (const ticket of [...tickets2].sort((a, b) => a.seq - b.seq)) {
|
|
8640
|
+
if (ticket.status === "done") {
|
|
8641
|
+
flush();
|
|
8642
|
+
blocks.push(`## ${ticket.seq}. ${ticket.title}`, ticket.digest?.trim() || NO_DIGEST);
|
|
8643
|
+
} else {
|
|
8644
|
+
oneLiners.push(oneLineEntry(ticket));
|
|
8645
|
+
}
|
|
8646
|
+
}
|
|
8647
|
+
flush();
|
|
8648
|
+
return `${blocks.join(`
|
|
8649
|
+
|
|
8650
|
+
`)}
|
|
8651
|
+
`;
|
|
8652
|
+
}
|
|
8653
|
+
function shippedDate(shippedAt) {
|
|
8654
|
+
return new Date(shippedAt).toISOString().slice(0, 10);
|
|
8655
|
+
}
|
|
8656
|
+
function oneLineEntry(ticket) {
|
|
8657
|
+
const headline = errorHeadline3(ticket.error ?? "");
|
|
8658
|
+
return `- **${ticket.seq}. ${ticket.title}** \u2014 ${ticket.status}${headline ? `: ${headline}` : ""}`;
|
|
8659
|
+
}
|
|
8660
|
+
function errorHeadline3(error) {
|
|
8661
|
+
const lines = error.split(`
|
|
8662
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
8663
|
+
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
8664
|
+
const headline = causes.at(-1) ?? lines[0] ?? "";
|
|
8665
|
+
return headline.length > HEADLINE_MAX ? `${headline.slice(0, HEADLINE_MAX)}\u2026` : headline;
|
|
8666
|
+
}
|
|
8667
|
+
async function promoteOutcomeDoc(ctx, project, feature) {
|
|
8668
|
+
try {
|
|
8669
|
+
const worktreePath = await ensureTalkWorktree(project, feature);
|
|
8670
|
+
const docsDir = join15(worktreePath, ...featureDocsRel(feature.slug).split("/"));
|
|
8671
|
+
mkdirSync8(docsDir, { recursive: true });
|
|
8672
|
+
const doc = composeOutcomeDoc(feature, listByFeature2(ctx, feature.id), Date.now());
|
|
8673
|
+
writeFileSync4(join15(docsDir, "outcome.md"), doc, "utf8");
|
|
8674
|
+
await commitDocs(worktreePath, `runcastle: outcome for ${feature.slug}`);
|
|
8675
|
+
} catch (e) {
|
|
8676
|
+
emit(ctx, feature.id, {
|
|
8677
|
+
type: "docs.outcome_failed",
|
|
8678
|
+
message: `outcome.md not promoted: ${e instanceof Error ? e.message : String(e)}`
|
|
8679
|
+
});
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8682
|
+
var NO_DIGEST = "_no digest captured_", HEADLINE_MAX = 160;
|
|
8683
|
+
var init_outcome = __esm(() => {
|
|
8684
|
+
init_paths();
|
|
8685
|
+
init_events();
|
|
8686
|
+
init_git();
|
|
8687
|
+
init_tickets();
|
|
8688
|
+
});
|
|
8689
|
+
|
|
8350
8690
|
// src/trpc/routers/feature.ts
|
|
8351
8691
|
import * as z6 from "zod";
|
|
8352
8692
|
var gateId, featureRouter;
|
|
@@ -8358,6 +8698,7 @@ var init_feature = __esm(() => {
|
|
|
8358
8698
|
init_features();
|
|
8359
8699
|
init_gates();
|
|
8360
8700
|
init_git();
|
|
8701
|
+
init_outcome();
|
|
8361
8702
|
init_repo();
|
|
8362
8703
|
init_context();
|
|
8363
8704
|
gateId = z6.enum(["G1", "G2", "G3", "G4", "G5"]);
|
|
@@ -8366,8 +8707,10 @@ var init_feature = __esm(() => {
|
|
|
8366
8707
|
projectId: z6.string(),
|
|
8367
8708
|
title: z6.string().min(1),
|
|
8368
8709
|
oneLiner: z6.string(),
|
|
8369
|
-
baseBranch: z6.string().optional()
|
|
8710
|
+
baseBranch: z6.string().optional(),
|
|
8711
|
+
draft: z6.boolean().optional()
|
|
8370
8712
|
})).mutation(({ ctx, input }) => createFeature(ctx, input)),
|
|
8713
|
+
start: publicProcedure.input(z6.object({ featureId: z6.string(), baseBranch: z6.string().optional() })).mutation(({ ctx, input }) => startDraft(ctx, input.featureId, { baseBranch: input.baseBranch })),
|
|
8371
8714
|
quickChange: publicProcedure.input(z6.object({
|
|
8372
8715
|
projectId: z6.string(),
|
|
8373
8716
|
title: z6.string().min(1),
|
|
@@ -8401,6 +8744,7 @@ var init_feature = __esm(() => {
|
|
|
8401
8744
|
burn: publicProcedure.input(z6.object({ featureId: z6.string(), model: z6.string().min(1).optional() })).mutation(({ ctx, input }) => burn(ctx, input.featureId, { modelOverride: input.model })),
|
|
8402
8745
|
testDrive: publicProcedure.input(z6.object({ featureId: z6.string(), action: z6.enum(["start", "stop"]) })).mutation(async ({ ctx, input }) => {
|
|
8403
8746
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
8747
|
+
requireNotDraft(feature);
|
|
8404
8748
|
const project = projectForFeature(ctx, feature);
|
|
8405
8749
|
return testDrive(ctx, project, feature, input.action);
|
|
8406
8750
|
}),
|
|
@@ -8411,10 +8755,12 @@ var init_feature = __esm(() => {
|
|
|
8411
8755
|
}),
|
|
8412
8756
|
merge: publicProcedure.input(z6.object({ featureId: z6.string() })).mutation(async ({ ctx, input }) => {
|
|
8413
8757
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
8758
|
+
requireNotDraft(feature);
|
|
8414
8759
|
const project = projectForFeature(ctx, feature);
|
|
8415
8760
|
if (activeTestDriveFeatureId() === feature.id) {
|
|
8416
8761
|
await testDrive(ctx, project, feature, "stop");
|
|
8417
8762
|
}
|
|
8763
|
+
await promoteOutcomeDoc(ctx, project, feature);
|
|
8418
8764
|
const res = await mergeFeature(project, feature);
|
|
8419
8765
|
if (res.ok) {
|
|
8420
8766
|
setPhase(ctx, input.featureId, "shipped", "feature.shipped", `merged to ${res.target}`);
|
|
@@ -8434,7 +8780,7 @@ var init_feature = __esm(() => {
|
|
|
8434
8780
|
// src/services/fsbrowse.ts
|
|
8435
8781
|
import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
|
|
8436
8782
|
import { homedir as homedir2 } from "os";
|
|
8437
|
-
import { basename, dirname as dirname8, isAbsolute as isAbsolute2, join as
|
|
8783
|
+
import { basename, dirname as dirname8, isAbsolute as isAbsolute2, join as join16, resolve as resolve10, sep as sep3 } from "path";
|
|
8438
8784
|
function expandPath(input, home = homedir2()) {
|
|
8439
8785
|
const trimmed = input.trim();
|
|
8440
8786
|
if (trimmed === "")
|
|
@@ -8443,12 +8789,12 @@ function expandPath(input, home = homedir2()) {
|
|
|
8443
8789
|
if (out === "~")
|
|
8444
8790
|
out = home;
|
|
8445
8791
|
else if (out.startsWith("~/") || out.startsWith("~\\"))
|
|
8446
|
-
out =
|
|
8792
|
+
out = join16(home, out.slice(2));
|
|
8447
8793
|
return resolve10(out);
|
|
8448
8794
|
}
|
|
8449
8795
|
function looksLikeRepo(dir) {
|
|
8450
8796
|
try {
|
|
8451
|
-
return existsSync15(
|
|
8797
|
+
return existsSync15(join16(dir, ".git"));
|
|
8452
8798
|
} catch {
|
|
8453
8799
|
return false;
|
|
8454
8800
|
}
|
|
@@ -8470,7 +8816,7 @@ function listRoots(home = homedir2()) {
|
|
|
8470
8816
|
roots.push({ label: "Home", path: home, kind: "home" });
|
|
8471
8817
|
const seen = new Set(roots.map(pathKey));
|
|
8472
8818
|
for (const name of COMMON_DIRS) {
|
|
8473
|
-
const candidate =
|
|
8819
|
+
const candidate = join16(home, name);
|
|
8474
8820
|
const key = pathKey({ path: candidate });
|
|
8475
8821
|
if (seen.has(key) || !existsSync15(candidate))
|
|
8476
8822
|
continue;
|
|
@@ -8531,7 +8877,7 @@ function browseDir(input, showHidden = false, home = homedir2()) {
|
|
|
8531
8877
|
continue;
|
|
8532
8878
|
if (!showHidden && name.startsWith("."))
|
|
8533
8879
|
continue;
|
|
8534
|
-
const path =
|
|
8880
|
+
const path = join16(dir, name);
|
|
8535
8881
|
const isSymlink = dirent.isSymbolicLink();
|
|
8536
8882
|
if (isSymlink && !showHidden)
|
|
8537
8883
|
continue;
|
|
@@ -8569,7 +8915,7 @@ var init_fsbrowse = __esm(() => {
|
|
|
8569
8915
|
|
|
8570
8916
|
// src/services/projects.ts
|
|
8571
8917
|
import { existsSync as existsSync16 } from "fs";
|
|
8572
|
-
import { basename as basename2, join as
|
|
8918
|
+
import { basename as basename2, join as join17 } from "path";
|
|
8573
8919
|
import { eq as eq12, isNull } from "drizzle-orm";
|
|
8574
8920
|
function listProjects(ctx) {
|
|
8575
8921
|
return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
|
|
@@ -8593,7 +8939,7 @@ async function openProject(ctx, rawPath) {
|
|
|
8593
8939
|
const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
|
|
8594
8940
|
if (existing) {
|
|
8595
8941
|
ctx.db.update(projects).set({ mainBranch, repoPath, closedAt: null }).where(eq12(projects.id, existing.id)).run();
|
|
8596
|
-
const reopened = { ...existing, mainBranch, repoPath };
|
|
8942
|
+
const reopened = { ...existing, mainBranch, repoPath, closedAt: undefined };
|
|
8597
8943
|
emitProject(ctx, existing.id, {
|
|
8598
8944
|
type: "project.opened",
|
|
8599
8945
|
message: `project ${existing.name} re-opened at ${repoPath} (${mainBranch})`
|
|
@@ -8650,7 +8996,7 @@ async function assertRepoTolerant(ctx, repoPath) {
|
|
|
8650
8996
|
} catch (e) {
|
|
8651
8997
|
if (!isNotImplemented(e))
|
|
8652
8998
|
throw e;
|
|
8653
|
-
if (!existsSync16(
|
|
8999
|
+
if (!existsSync16(join17(repoPath, ".git"))) {
|
|
8654
9000
|
throw new InvalidInputError(`not a git repository: ${repoPath}`);
|
|
8655
9001
|
}
|
|
8656
9002
|
}
|
|
@@ -8715,7 +9061,8 @@ var init_run = __esm(() => {
|
|
|
8715
9061
|
runRouter = router({
|
|
8716
9062
|
get: publicProcedure.input(z8.object({ runId: z8.string() })).query(({ ctx, input }) => getRunRow(ctx, input.runId)),
|
|
8717
9063
|
agentTranscript: publicProcedure.input(z8.object({ ticketId: z8.string(), after: z8.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
|
|
8718
|
-
cancel: publicProcedure.input(z8.object({ runId: z8.string() })).mutation(({ input }) => {
|
|
9064
|
+
cancel: publicProcedure.input(z8.object({ runId: z8.string() })).mutation(({ ctx, input }) => {
|
|
9065
|
+
getRunRow(ctx, input.runId);
|
|
8719
9066
|
cancelRun(input.runId);
|
|
8720
9067
|
return { ok: true };
|
|
8721
9068
|
})
|
|
@@ -8723,7 +9070,7 @@ var init_run = __esm(() => {
|
|
|
8723
9070
|
});
|
|
8724
9071
|
|
|
8725
9072
|
// src/services/settings.ts
|
|
8726
|
-
import { existsSync as existsSync17, mkdirSync as
|
|
9073
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
8727
9074
|
import { dirname as dirname9 } from "path";
|
|
8728
9075
|
import * as z9 from "zod";
|
|
8729
9076
|
import { eq as eq13 } from "drizzle-orm";
|
|
@@ -8870,8 +9217,8 @@ function updateSettings(ctx, input, io = {}) {
|
|
|
8870
9217
|
function writeGlobal(configFile, configKey, value) {
|
|
8871
9218
|
const raw = readRawConfig(configFile);
|
|
8872
9219
|
raw[configKey] = value;
|
|
8873
|
-
|
|
8874
|
-
|
|
9220
|
+
mkdirSync9(dirname9(configFile), { recursive: true });
|
|
9221
|
+
writeFileSync5(configFile, `${JSON.stringify(raw, null, 2)}
|
|
8875
9222
|
`);
|
|
8876
9223
|
}
|
|
8877
9224
|
function updateStepModel(ctx, input, configFile, io) {
|
|
@@ -8909,8 +9256,8 @@ function writeStepModel(configFile, step, value) {
|
|
|
8909
9256
|
else
|
|
8910
9257
|
stepModels[step] = value;
|
|
8911
9258
|
raw.stepModels = stepModels;
|
|
8912
|
-
|
|
8913
|
-
|
|
9259
|
+
mkdirSync9(dirname9(configFile), { recursive: true });
|
|
9260
|
+
writeFileSync5(configFile, `${JSON.stringify(raw, null, 2)}
|
|
8914
9261
|
`);
|
|
8915
9262
|
}
|
|
8916
9263
|
function field(view, key) {
|
|
@@ -9147,11 +9494,11 @@ async function gitIdentityProbe(exec, cwd) {
|
|
|
9147
9494
|
async function containerRuntimeProbe(exec) {
|
|
9148
9495
|
const id = "container-runtime";
|
|
9149
9496
|
const label = "Container runtime (Docker / Podman)";
|
|
9150
|
-
const
|
|
9151
|
-
if (
|
|
9497
|
+
const docker2 = await exec("docker", ["--version"]);
|
|
9498
|
+
if (docker2.ok && docker2.code === 0) {
|
|
9152
9499
|
const info = await exec("docker", ["info"]);
|
|
9153
9500
|
if (info.ok && info.code === 0) {
|
|
9154
|
-
return { id, label, tier: 2, status: "ok", detail:
|
|
9501
|
+
return { id, label, tier: 2, status: "ok", detail: docker2.stdout.trim() || "docker healthy" };
|
|
9155
9502
|
}
|
|
9156
9503
|
return {
|
|
9157
9504
|
id,
|
|
@@ -9287,8 +9634,8 @@ var init_system_exec = __esm(() => {
|
|
|
9287
9634
|
});
|
|
9288
9635
|
|
|
9289
9636
|
// src/services/setup.ts
|
|
9290
|
-
import { copyFileSync, existsSync as existsSync18, mkdirSync as
|
|
9291
|
-
import { dirname as dirname10, join as
|
|
9637
|
+
import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync9, readdirSync as readdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
9638
|
+
import { dirname as dirname10, join as join18 } from "path";
|
|
9292
9639
|
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
9293
9640
|
async function writeGitIdentity(exec, identity) {
|
|
9294
9641
|
const name = identity.name.trim();
|
|
@@ -9366,11 +9713,11 @@ function resolveSandcastleBin() {
|
|
|
9366
9713
|
const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
|
|
9367
9714
|
let dir = dirname10(fileURLToPath7(mainUrl));
|
|
9368
9715
|
for (let hops = 0;hops < 6; hops++) {
|
|
9369
|
-
const manifestPath =
|
|
9716
|
+
const manifestPath = join18(dir, "package.json");
|
|
9370
9717
|
if (existsSync18(manifestPath)) {
|
|
9371
9718
|
const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
|
|
9372
9719
|
if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
|
|
9373
|
-
return
|
|
9720
|
+
return join18(dir, manifest.bin.sandcastle);
|
|
9374
9721
|
}
|
|
9375
9722
|
}
|
|
9376
9723
|
const parent = dirname10(dir);
|
|
@@ -9398,18 +9745,18 @@ function sandcastleTemplateDir() {
|
|
|
9398
9745
|
return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath7(new URL("../assets/sandcastle", import.meta.url)));
|
|
9399
9746
|
}
|
|
9400
9747
|
function scaffoldSandcastleConfig(templateDir, targetDir) {
|
|
9401
|
-
const dir =
|
|
9748
|
+
const dir = join18(targetDir, ".sandcastle");
|
|
9402
9749
|
if (existsSync18(dir))
|
|
9403
9750
|
return { scaffolded: false, dir };
|
|
9404
|
-
|
|
9751
|
+
mkdirSync10(dir, { recursive: true });
|
|
9405
9752
|
for (const name of readdirSync6(templateDir)) {
|
|
9406
|
-
copyFileSync(
|
|
9753
|
+
copyFileSync(join18(templateDir, name), join18(dir, name));
|
|
9407
9754
|
}
|
|
9408
9755
|
return { scaffolded: true, dir };
|
|
9409
9756
|
}
|
|
9410
9757
|
function prepareSandboxBuildContext() {
|
|
9411
9758
|
const target = sandboxBuildDir();
|
|
9412
|
-
|
|
9759
|
+
mkdirSync10(target, { recursive: true });
|
|
9413
9760
|
scaffoldSandcastleConfig(sandcastleTemplateDir(), target);
|
|
9414
9761
|
return target;
|
|
9415
9762
|
}
|
|
@@ -9447,8 +9794,8 @@ function fileAfkTokenIo(exec) {
|
|
|
9447
9794
|
return {
|
|
9448
9795
|
read: () => existsSync18(path) ? readFileSync9(path, "utf8") : "",
|
|
9449
9796
|
write: (content) => {
|
|
9450
|
-
|
|
9451
|
-
|
|
9797
|
+
mkdirSync10(dirname10(path), { recursive: true });
|
|
9798
|
+
writeFileSync6(path, content, "utf8");
|
|
9452
9799
|
},
|
|
9453
9800
|
verify: createTokenVerifier(exec)
|
|
9454
9801
|
};
|
|
@@ -9514,11 +9861,11 @@ var init_system = __esm(() => {
|
|
|
9514
9861
|
});
|
|
9515
9862
|
|
|
9516
9863
|
// src/services/test-notes.ts
|
|
9517
|
-
import { mkdirSync as
|
|
9864
|
+
import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync7 } from "fs";
|
|
9518
9865
|
import { dirname as dirname11 } from "path";
|
|
9519
9866
|
import { asc as asc4, eq as eq14 } from "drizzle-orm";
|
|
9520
9867
|
function rowToNote(row) {
|
|
9521
|
-
return {
|
|
9868
|
+
return TestNote.parse({
|
|
9522
9869
|
id: row.id,
|
|
9523
9870
|
featureId: row.featureId,
|
|
9524
9871
|
lap: row.lap,
|
|
@@ -9527,7 +9874,7 @@ function rowToNote(row) {
|
|
|
9527
9874
|
ticketId: row.ticketId ?? undefined,
|
|
9528
9875
|
createdAt: row.createdAt,
|
|
9529
9876
|
updatedAt: row.updatedAt
|
|
9530
|
-
};
|
|
9877
|
+
});
|
|
9531
9878
|
}
|
|
9532
9879
|
function getNote(ctx, id) {
|
|
9533
9880
|
const row = ctx.db.select().from(testNotes).where(eq14(testNotes.id, id)).get();
|
|
@@ -9660,8 +10007,8 @@ function renderTestNotes(ctx, feature) {
|
|
|
9660
10007
|
lines.push(noteLine(ctx, note));
|
|
9661
10008
|
}
|
|
9662
10009
|
const path = featureDocPath(projectForFeature(ctx, feature), feature, "test-notes.md");
|
|
9663
|
-
|
|
9664
|
-
|
|
10010
|
+
mkdirSync11(dirname11(path), { recursive: true });
|
|
10011
|
+
writeFileSync7(path, `${lines.join(`
|
|
9665
10012
|
`)}
|
|
9666
10013
|
`, "utf8");
|
|
9667
10014
|
}
|
|
@@ -9852,6 +10199,8 @@ async function startServer() {
|
|
|
9852
10199
|
}
|
|
9853
10200
|
const server = Bun.serve({
|
|
9854
10201
|
port: config2.serverPort,
|
|
10202
|
+
hostname: SERVE_HOSTNAME,
|
|
10203
|
+
idleTimeout: SERVE_IDLE_TIMEOUT_SECONDS,
|
|
9855
10204
|
fetch(req, srv) {
|
|
9856
10205
|
if (tryUpgradeTerminal(req, srv))
|
|
9857
10206
|
return;
|
|
@@ -9865,11 +10214,11 @@ async function startServer() {
|
|
|
9865
10214
|
const g = globalThis;
|
|
9866
10215
|
if (!g[SHUTDOWN_KEY]) {
|
|
9867
10216
|
g[SHUTDOWN_KEY] = true;
|
|
9868
|
-
const shutdown = (
|
|
9869
|
-
ptyRegistry().
|
|
9870
|
-
server.stop()
|
|
9871
|
-
process.exit(
|
|
9872
|
-
};
|
|
10217
|
+
const shutdown = createShutdown({
|
|
10218
|
+
killAllTrees: () => ptyRegistry().killAllTrees(),
|
|
10219
|
+
stop: () => server.stop(),
|
|
10220
|
+
exit: (code) => process.exit(code)
|
|
10221
|
+
});
|
|
9873
10222
|
process.on("SIGINT", shutdown);
|
|
9874
10223
|
process.on("SIGTERM", shutdown);
|
|
9875
10224
|
}
|