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