runcastle 1.0.24 → 1.0.25
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 +1800 -921
- package/drizzle/0010_misty_ken_ellis.sql +24 -0
- package/drizzle/meta/0010_snapshot.json +757 -0
- package/drizzle/meta/_journal.json +7 -0
- package/index.js +1794 -915
- package/package.json +1 -1
- package/skills/burner/prepare-project.md +92 -0
- package/skills/packs/README.md +1 -1
- package/skills/packs/runcastle/skills/tickets/SKILL.md +22 -6
- package/web/assets/index-B4LHcg8h.css +1 -0
- package/web/assets/{index-Cpwb7Nqn.js → index-DrjQZmZ4.js} +21 -21
- package/web/index.html +2 -2
- package/web/assets/index-DBVAMsFt.css +0 -1
package/index.js
CHANGED
|
@@ -68,6 +68,20 @@ import * as z from "zod";
|
|
|
68
68
|
function resolveSandboxImage(config) {
|
|
69
69
|
return config.sandboxImage ?? DEFAULT_SANDBOX_IMAGE;
|
|
70
70
|
}
|
|
71
|
+
function resolvePreparedSettings(config, project) {
|
|
72
|
+
const pick = (a, b) => {
|
|
73
|
+
const own = a?.trim();
|
|
74
|
+
if (own)
|
|
75
|
+
return own;
|
|
76
|
+
const inherited = b?.trim();
|
|
77
|
+
return inherited || undefined;
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
setupCommand: pick(project?.setupCommand, config.setupCommand),
|
|
81
|
+
verifyCommands: pick(project?.verifyCommands, config.verifyCommands),
|
|
82
|
+
knownFailures: pick(project?.knownFailures, config.knownFailures)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
71
85
|
function resolveModel(step, config, project, runOverride) {
|
|
72
86
|
return runOverride ?? config.stepModels[step] ?? project?.model ?? config.model;
|
|
73
87
|
}
|
|
@@ -93,6 +107,7 @@ var init_config = __esm(() => {
|
|
|
93
107
|
"revisit",
|
|
94
108
|
"research",
|
|
95
109
|
"implement",
|
|
110
|
+
"prepare",
|
|
96
111
|
"smoke"
|
|
97
112
|
];
|
|
98
113
|
ModelStep = z.enum(MODEL_STEPS);
|
|
@@ -109,6 +124,7 @@ var init_config = __esm(() => {
|
|
|
109
124
|
burnMaxIterations: z.number().int().min(1).max(10).default(3),
|
|
110
125
|
burnAttempts: z.number().int().min(1).max(5).default(3),
|
|
111
126
|
burnConflictAttempts: z.number().int().min(0).max(3).default(2),
|
|
127
|
+
autoPrepare: z.boolean().default(true),
|
|
112
128
|
setupCommand: z.string().optional(),
|
|
113
129
|
verifyCommands: z.string().optional(),
|
|
114
130
|
knownFailures: z.string().optional(),
|
|
@@ -202,7 +218,7 @@ var init_ids = () => {};
|
|
|
202
218
|
|
|
203
219
|
// ../core/src/schemas.ts
|
|
204
220
|
import * as z2 from "zod";
|
|
205
|
-
var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, Project, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
221
|
+
var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, PREPARED_KEYS, PreparedKey, FindingSource, Project, ProjectFinding, PrepStatus, PrepRun, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
206
222
|
var init_schemas = __esm(() => {
|
|
207
223
|
Phase = z2.enum([
|
|
208
224
|
"ideation",
|
|
@@ -255,13 +271,44 @@ var init_schemas = __esm(() => {
|
|
|
255
271
|
lastSessionId: z2.string().optional(),
|
|
256
272
|
summary: z2.string().optional()
|
|
257
273
|
});
|
|
274
|
+
PREPARED_KEYS = [
|
|
275
|
+
"setupCommand",
|
|
276
|
+
"verifyCommands",
|
|
277
|
+
"knownFailures",
|
|
278
|
+
"devCommand",
|
|
279
|
+
"dbResetCommand"
|
|
280
|
+
];
|
|
281
|
+
PreparedKey = z2.enum(PREPARED_KEYS);
|
|
282
|
+
FindingSource = z2.enum(["prep", "human"]);
|
|
258
283
|
Project = z2.object({
|
|
259
284
|
id: z2.string(),
|
|
260
285
|
name: z2.string(),
|
|
261
286
|
repoPath: z2.string(),
|
|
262
287
|
mainBranch: z2.string(),
|
|
263
288
|
devCommand: z2.string().optional(),
|
|
264
|
-
model: z2.string().optional()
|
|
289
|
+
model: z2.string().optional(),
|
|
290
|
+
setupCommand: z2.string().optional(),
|
|
291
|
+
verifyCommands: z2.string().optional(),
|
|
292
|
+
knownFailures: z2.string().optional(),
|
|
293
|
+
dbResetCommand: z2.string().optional()
|
|
294
|
+
});
|
|
295
|
+
ProjectFinding = z2.object({
|
|
296
|
+
key: PreparedKey,
|
|
297
|
+
source: FindingSource,
|
|
298
|
+
evidence: z2.string().optional(),
|
|
299
|
+
establishedAt: z2.number(),
|
|
300
|
+
establishedSha: z2.string().optional(),
|
|
301
|
+
staleCommits: z2.number().optional()
|
|
302
|
+
});
|
|
303
|
+
PrepStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
|
|
304
|
+
PrepRun = z2.object({
|
|
305
|
+
id: z2.string(),
|
|
306
|
+
projectId: z2.string(),
|
|
307
|
+
status: PrepStatus,
|
|
308
|
+
startedAt: z2.number(),
|
|
309
|
+
endedAt: z2.number().optional(),
|
|
310
|
+
summary: z2.string().optional(),
|
|
311
|
+
headSha: z2.string().optional()
|
|
265
312
|
});
|
|
266
313
|
Feature = z2.object({
|
|
267
314
|
id: z2.string(),
|
|
@@ -460,8 +507,8 @@ var init_pipeline = __esm(() => {
|
|
|
460
507
|
});
|
|
461
508
|
|
|
462
509
|
// ../core/src/db-schema.ts
|
|
463
|
-
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
464
|
-
var projects, features, sessions, tickets, waypoints, runs, events, gateOverrides;
|
|
510
|
+
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
511
|
+
var projects, projectFindings, projectPreps, features, sessions, tickets, waypoints, runs, events, gateOverrides;
|
|
465
512
|
var init_db_schema = __esm(() => {
|
|
466
513
|
projects = sqliteTable("projects", {
|
|
467
514
|
id: text("id").primaryKey(),
|
|
@@ -471,7 +518,28 @@ var init_db_schema = __esm(() => {
|
|
|
471
518
|
devCommand: text("dev_command"),
|
|
472
519
|
model: text("model"),
|
|
473
520
|
sandbox: text("sandbox"),
|
|
474
|
-
closedAt: integer("closed_at")
|
|
521
|
+
closedAt: integer("closed_at"),
|
|
522
|
+
setupCommand: text("setup_command"),
|
|
523
|
+
verifyCommands: text("verify_commands"),
|
|
524
|
+
knownFailures: text("known_failures"),
|
|
525
|
+
dbResetCommand: text("db_reset_command")
|
|
526
|
+
});
|
|
527
|
+
projectFindings = sqliteTable("project_findings", {
|
|
528
|
+
projectId: text("project_id").notNull(),
|
|
529
|
+
key: text("key").notNull(),
|
|
530
|
+
source: text("source").notNull().$type(),
|
|
531
|
+
evidence: text("evidence"),
|
|
532
|
+
establishedAt: integer("established_at").notNull(),
|
|
533
|
+
establishedSha: text("established_sha")
|
|
534
|
+
}, (t) => [primaryKey({ columns: [t.projectId, t.key] })]);
|
|
535
|
+
projectPreps = sqliteTable("project_preps", {
|
|
536
|
+
id: text("id").primaryKey(),
|
|
537
|
+
projectId: text("project_id").notNull(),
|
|
538
|
+
status: text("status").notNull().$type(),
|
|
539
|
+
startedAt: integer("started_at").notNull(),
|
|
540
|
+
endedAt: integer("ended_at"),
|
|
541
|
+
summary: text("summary"),
|
|
542
|
+
headSha: text("head_sha")
|
|
475
543
|
});
|
|
476
544
|
features = sqliteTable("features", {
|
|
477
545
|
id: text("id").primaryKey(),
|
|
@@ -575,7 +643,9 @@ var init_schema = __esm(() => {
|
|
|
575
643
|
waypoints,
|
|
576
644
|
runs,
|
|
577
645
|
events,
|
|
578
|
-
gateOverrides
|
|
646
|
+
gateOverrides,
|
|
647
|
+
projectFindings,
|
|
648
|
+
projectPreps
|
|
579
649
|
};
|
|
580
650
|
});
|
|
581
651
|
|
|
@@ -1340,7 +1410,11 @@ function rowToProject(row) {
|
|
|
1340
1410
|
repoPath: row.repoPath,
|
|
1341
1411
|
mainBranch: row.mainBranch,
|
|
1342
1412
|
devCommand: row.devCommand ?? undefined,
|
|
1343
|
-
model: row.model ?? undefined
|
|
1413
|
+
model: row.model ?? undefined,
|
|
1414
|
+
setupCommand: row.setupCommand ?? undefined,
|
|
1415
|
+
verifyCommands: row.verifyCommands ?? undefined,
|
|
1416
|
+
knownFailures: row.knownFailures ?? undefined,
|
|
1417
|
+
dbResetCommand: row.dbResetCommand ?? undefined
|
|
1344
1418
|
};
|
|
1345
1419
|
}
|
|
1346
1420
|
function rowToRun(row) {
|
|
@@ -1672,34 +1746,159 @@ function writeKickoffSequence(line, io, submitDelayMs = KICKOFF_SUBMIT_DELAY_MS)
|
|
|
1672
1746
|
}, submitDelayMs);
|
|
1673
1747
|
submit.unref?.();
|
|
1674
1748
|
}
|
|
1675
|
-
function
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
|
|
1749
|
+
function stopTimers(d) {
|
|
1750
|
+
for (const t of d.timers)
|
|
1751
|
+
clearTimeout(t);
|
|
1752
|
+
d.timers.clear();
|
|
1753
|
+
}
|
|
1754
|
+
function forgetKickoff(sessionId) {
|
|
1755
|
+
const d = deliveries.get(sessionId);
|
|
1756
|
+
if (d)
|
|
1757
|
+
stopTimers(d);
|
|
1758
|
+
deliveries.delete(sessionId);
|
|
1759
|
+
pendingKickoffOverrides.delete(sessionId);
|
|
1760
|
+
}
|
|
1761
|
+
function ptyIo(sessionId) {
|
|
1762
|
+
return {
|
|
1763
|
+
write: (data) => ptyRegistry().get(sessionId)?.pty.write(data),
|
|
1764
|
+
alive: () => {
|
|
1765
|
+
const entry = ptyRegistry().get(sessionId);
|
|
1766
|
+
return !!entry && !entry.exited;
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
function track(d, timer) {
|
|
1771
|
+
timer.unref?.();
|
|
1772
|
+
d.timers.add(timer);
|
|
1773
|
+
}
|
|
1774
|
+
function attemptKickoff(ctx, session, d, delayMs, clearFirst = false) {
|
|
1775
|
+
track(d, setTimeout(() => {
|
|
1776
|
+
if (d.settled)
|
|
1777
|
+
return;
|
|
1778
|
+
const io = ptyIo(session.id);
|
|
1779
|
+
if (!io.alive()) {
|
|
1780
|
+
d.settled = true;
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
d.attempts += 1;
|
|
1784
|
+
const attempt = d.attempts;
|
|
1679
1785
|
try {
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
data: { sessionId: session.id, kind: session.kind }
|
|
1691
|
-
});
|
|
1692
|
-
}
|
|
1786
|
+
if (clearFirst)
|
|
1787
|
+
io.write(CLEAR_INPUT);
|
|
1788
|
+
writeKickoffSequence(d.line, {
|
|
1789
|
+
write: io.write,
|
|
1790
|
+
alive: io.alive,
|
|
1791
|
+
onSubmitted: () => emit(ctx, session.featureId, {
|
|
1792
|
+
type: "session.kickoff",
|
|
1793
|
+
message: attempt === 1 ? `${session.kind} session kicked off automatically` : `${session.kind} kickoff re-sent (attempt ${attempt}) \u2014 the first was never acknowledged`,
|
|
1794
|
+
data: { sessionId: session.id, kind: session.kind, attempt }
|
|
1795
|
+
})
|
|
1693
1796
|
});
|
|
1694
1797
|
} catch {}
|
|
1695
|
-
|
|
1798
|
+
armConfirmation(ctx, session, d);
|
|
1799
|
+
}, delayMs));
|
|
1800
|
+
}
|
|
1801
|
+
function armConfirmation(ctx, session, d) {
|
|
1802
|
+
track(d, setTimeout(() => {
|
|
1803
|
+
if (d.settled || d.confirmed)
|
|
1804
|
+
return;
|
|
1805
|
+
if (d.attempts < KICKOFF_MAX_ATTEMPTS && ptyIo(session.id).alive()) {
|
|
1806
|
+
attemptKickoff(ctx, session, d, 0, true);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
settleUndelivered(ctx, session, d, "unacknowledged");
|
|
1810
|
+
}, KICKOFF_CONFIRM_MS));
|
|
1811
|
+
}
|
|
1812
|
+
function settleUndelivered(ctx, session, d, reason) {
|
|
1813
|
+
d.settled = true;
|
|
1814
|
+
stopTimers(d);
|
|
1815
|
+
emit(ctx, session.featureId, {
|
|
1816
|
+
type: "session.kickoff_undelivered",
|
|
1817
|
+
message: reason === "superseded" ? `the ${session.kind} briefing was never delivered \u2014 you typed first, so runcastle stopped injecting it` : `the ${session.kind} briefing was typed ${d.attempts}\xD7 but Claude Code never acknowledged it \u2014 send it again from the session strip`,
|
|
1818
|
+
data: { sessionId: session.id, kind: session.kind, reason, attempts: d.attempts, line: d.line }
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
function noteKickoffPrompt(ctx, sessionId, prompt) {
|
|
1822
|
+
const d = deliveries.get(sessionId);
|
|
1823
|
+
if (!d || d.confirmed)
|
|
1824
|
+
return;
|
|
1825
|
+
const session = getSessionRow(ctx, sessionId);
|
|
1826
|
+
if (!session)
|
|
1827
|
+
return;
|
|
1828
|
+
if (promptMatchesKickoff(d.line, prompt)) {
|
|
1829
|
+
d.confirmed = true;
|
|
1830
|
+
d.settled = true;
|
|
1831
|
+
stopTimers(d);
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
if (d.settled)
|
|
1835
|
+
return;
|
|
1836
|
+
settleUndelivered(ctx, session, d, "superseded");
|
|
1837
|
+
}
|
|
1838
|
+
function promptMatchesKickoff(line, prompt) {
|
|
1839
|
+
if (!prompt)
|
|
1840
|
+
return false;
|
|
1841
|
+
const norm = (s) => s.replace(/\s+/g, " ").trim().toLowerCase();
|
|
1842
|
+
const want = norm(line);
|
|
1843
|
+
const got = norm(prompt);
|
|
1844
|
+
if (!want || !got)
|
|
1845
|
+
return false;
|
|
1846
|
+
return got.includes(want.slice(0, MATCH_PREFIX));
|
|
1847
|
+
}
|
|
1848
|
+
function resendKickoff(ctx, sessionId) {
|
|
1849
|
+
const session = getSessionRow(ctx, sessionId);
|
|
1850
|
+
if (!session)
|
|
1851
|
+
throw new Error(`unknown session ${sessionId}`);
|
|
1852
|
+
if (session.status === "ended")
|
|
1853
|
+
throw new Error("that session has ended \u2014 open a new terminal");
|
|
1854
|
+
if (!ptyIo(sessionId).alive())
|
|
1855
|
+
throw new Error("the terminal for that session is no longer running");
|
|
1856
|
+
const existing = deliveries.get(sessionId);
|
|
1857
|
+
const d = existing ?? {
|
|
1858
|
+
line: kickoffLineFor(session.kind, pendingKickoffOverrides.get(sessionId)),
|
|
1859
|
+
attempts: 0,
|
|
1860
|
+
confirmed: false,
|
|
1861
|
+
settled: false,
|
|
1862
|
+
timers: new Set
|
|
1863
|
+
};
|
|
1864
|
+
stopTimers(d);
|
|
1865
|
+
d.attempts = 0;
|
|
1866
|
+
d.confirmed = false;
|
|
1867
|
+
d.settled = false;
|
|
1868
|
+
deliveries.set(sessionId, d);
|
|
1869
|
+
attemptKickoff(ctx, session, d, 0, true);
|
|
1870
|
+
return { line: d.line };
|
|
1871
|
+
}
|
|
1872
|
+
function armSessionReadyWatchdog(ctx, session) {
|
|
1873
|
+
const timer = setTimeout(() => {
|
|
1874
|
+
const row = getSessionRow(ctx, session.id);
|
|
1875
|
+
if (!row || row.status !== "launching")
|
|
1876
|
+
return;
|
|
1877
|
+
if (!ptyIo(session.id).alive())
|
|
1878
|
+
return;
|
|
1879
|
+
emit(ctx, session.featureId, {
|
|
1880
|
+
type: "session.not_ready",
|
|
1881
|
+
message: `the ${session.kind} terminal is open but Claude Code has not reported ready \u2014 check it for a prompt or dialog waiting on you, then send the briefing`,
|
|
1882
|
+
data: { sessionId: session.id, kind: session.kind }
|
|
1883
|
+
});
|
|
1884
|
+
}, SESSION_READY_TIMEOUT_MS);
|
|
1696
1885
|
timer.unref?.();
|
|
1697
1886
|
}
|
|
1887
|
+
function scheduleKickoff(ctx, session) {
|
|
1888
|
+
const line = kickoffLineFor(session.kind, pendingKickoffOverrides.get(session.id));
|
|
1889
|
+
pendingKickoffOverrides.delete(session.id);
|
|
1890
|
+
const existing = deliveries.get(session.id);
|
|
1891
|
+
if (existing)
|
|
1892
|
+
stopTimers(existing);
|
|
1893
|
+
const d = { line, attempts: 0, confirmed: false, settled: false, timers: new Set };
|
|
1894
|
+
deliveries.set(session.id, d);
|
|
1895
|
+
attemptKickoff(ctx, session, d, KICKOFF_DELAY_MS);
|
|
1896
|
+
}
|
|
1698
1897
|
function markSessionEnded(ctx, id) {
|
|
1699
1898
|
const existing = getSessionRow(ctx, id);
|
|
1700
1899
|
if (!existing)
|
|
1701
1900
|
return null;
|
|
1702
|
-
|
|
1901
|
+
forgetKickoff(id);
|
|
1703
1902
|
ctx.db.update(sessions).set({ status: "ended" }).where(eq4(sessions.id, id)).run();
|
|
1704
1903
|
return getSessionRow(ctx, id);
|
|
1705
1904
|
}
|
|
@@ -1711,7 +1910,7 @@ function mostRecentLiveSession(ctx) {
|
|
|
1711
1910
|
const row = ctx.db.select().from(sessions).where(eq4(sessions.status, "live")).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1712
1911
|
return row ? rowToSession(row) : null;
|
|
1713
1912
|
}
|
|
1714
|
-
var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides;
|
|
1913
|
+
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;
|
|
1715
1914
|
var init_sessions = __esm(() => {
|
|
1716
1915
|
init_src();
|
|
1717
1916
|
init_schema();
|
|
@@ -1729,6 +1928,7 @@ var init_sessions = __esm(() => {
|
|
|
1729
1928
|
};
|
|
1730
1929
|
RESUME_KICKOFF_PREFIX = "We are picking this conversation back up \u2014 runcastle restarted and closed the terminal, " + "but this conversation is intact. Do NOT start over: tell me in one or two lines where we " + "left off and what is next, then carry on. Your original instruction was: ";
|
|
1731
1930
|
pendingKickoffOverrides = new Map;
|
|
1931
|
+
deliveries = new Map;
|
|
1732
1932
|
});
|
|
1733
1933
|
|
|
1734
1934
|
// src/launcher/reconcile.ts
|
|
@@ -1764,339 +1964,104 @@ var init_reconcile = __esm(() => {
|
|
|
1764
1964
|
init_sessions();
|
|
1765
1965
|
});
|
|
1766
1966
|
|
|
1767
|
-
// src/launcher/
|
|
1768
|
-
function setRuntimeCtx(ctx) {
|
|
1769
|
-
current = ctx;
|
|
1770
|
-
}
|
|
1771
|
-
async function getRuntimeCtx() {
|
|
1772
|
-
if (current)
|
|
1773
|
-
return current;
|
|
1774
|
-
const [{ createDb: createDb2 }, { dbPath: dbPath2 }, { loadConfig: loadConfig2 }] = await Promise.all([
|
|
1775
|
-
Promise.resolve().then(() => (init_client(), exports_client)),
|
|
1776
|
-
Promise.resolve().then(() => (init_paths(), exports_paths)),
|
|
1777
|
-
Promise.resolve().then(() => (init_config_load(), exports_config_load))
|
|
1778
|
-
]);
|
|
1779
|
-
const db = createDb2(dbPath2());
|
|
1780
|
-
current = { db, config: loadConfig2() };
|
|
1781
|
-
return current;
|
|
1782
|
-
}
|
|
1783
|
-
var current = null;
|
|
1784
|
-
|
|
1785
|
-
// src/services/feature-docs.ts
|
|
1967
|
+
// src/launcher/skills-root.ts
|
|
1786
1968
|
import { existsSync as existsSync5 } from "fs";
|
|
1787
|
-
import { join as join5 } from "path";
|
|
1788
|
-
function
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1969
|
+
import { dirname, join as join5, resolve as resolve3 } from "path";
|
|
1970
|
+
function resolveSkillsRoot(fromDir) {
|
|
1971
|
+
const override = process.env[SKILLS_DIR_ENV];
|
|
1972
|
+
if (override) {
|
|
1973
|
+
const root = resolve3(override);
|
|
1974
|
+
if (existsSync5(join5(root, SKILLS_MARKER)))
|
|
1975
|
+
return root;
|
|
1976
|
+
throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
|
|
1977
|
+
}
|
|
1978
|
+
const searched = [];
|
|
1979
|
+
let dir = fromDir;
|
|
1980
|
+
for (let i = 0;i < 8; i += 1) {
|
|
1981
|
+
const candidate = join5(dir, "packages", "skills");
|
|
1982
|
+
searched.push(candidate);
|
|
1983
|
+
if (existsSync5(join5(candidate, SKILLS_MARKER)))
|
|
1984
|
+
return candidate;
|
|
1985
|
+
const parent = dirname(dir);
|
|
1986
|
+
if (parent === dir)
|
|
1987
|
+
break;
|
|
1988
|
+
dir = parent;
|
|
1989
|
+
}
|
|
1990
|
+
throw new Error(`runcastle skills root not found; searched:
|
|
1991
|
+
${searched.join(`
|
|
1992
|
+
`)}`);
|
|
1796
1993
|
}
|
|
1797
|
-
var
|
|
1798
|
-
|
|
1994
|
+
var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
|
|
1995
|
+
var init_skills_root = __esm(() => {
|
|
1996
|
+
SKILLS_MARKER = join5("packs", "runcastle");
|
|
1799
1997
|
});
|
|
1800
1998
|
|
|
1801
|
-
// src/
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
return {
|
|
1805
|
-
id: row.id,
|
|
1806
|
-
featureId: row.featureId,
|
|
1807
|
-
seq: row.seq,
|
|
1808
|
-
title: row.title,
|
|
1809
|
-
goal: row.goal,
|
|
1810
|
-
context: row.context,
|
|
1811
|
-
acceptanceCriteria: row.acceptanceCriteria,
|
|
1812
|
-
seams: row.seams,
|
|
1813
|
-
blockedBy: row.blockedBy,
|
|
1814
|
-
status: row.status,
|
|
1815
|
-
commits: row.commits,
|
|
1816
|
-
error: row.error ?? undefined,
|
|
1817
|
-
attemptBranch: row.attemptBranch ?? undefined,
|
|
1818
|
-
conflictFiles: row.conflictFiles ?? undefined
|
|
1819
|
-
};
|
|
1999
|
+
// src/pty/dev-pane.ts
|
|
2000
|
+
function drivePaneId(featureId) {
|
|
2001
|
+
return `${DRIVE_PREFIX}${featureId}`;
|
|
1820
2002
|
}
|
|
1821
|
-
function
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
} catch (e) {
|
|
1825
|
-
if (e instanceof BlockingEdgeError)
|
|
1826
|
-
throw new InvalidInputError(e.message);
|
|
1827
|
-
throw e;
|
|
1828
|
-
}
|
|
2003
|
+
function sniffDevUrl(text2) {
|
|
2004
|
+
const m = text2.match(/https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/[^\s"'<>`\x1b]*)?/i);
|
|
2005
|
+
return m ? m[0] : undefined;
|
|
1829
2006
|
}
|
|
1830
|
-
function
|
|
1831
|
-
|
|
2007
|
+
function devSpawnTarget(devCommand) {
|
|
2008
|
+
if (process.platform === "win32") {
|
|
2009
|
+
return { file: process.env.ComSpec ?? "cmd.exe", args: ["/d", "/s", "/c", devCommand] };
|
|
2010
|
+
}
|
|
2011
|
+
return { file: "/bin/sh", args: ["-c", devCommand] };
|
|
1832
2012
|
}
|
|
1833
|
-
function
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
const
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2013
|
+
function startDevPane(input) {
|
|
2014
|
+
const { ctx, featureId, repoPath, devCommand, onUrl } = input;
|
|
2015
|
+
const paneId = drivePaneId(featureId);
|
|
2016
|
+
const { file, args } = devSpawnTarget(devCommand);
|
|
2017
|
+
try {
|
|
2018
|
+
ptyRegistry().create({
|
|
2019
|
+
sessionId: paneId,
|
|
2020
|
+
cmd: file,
|
|
2021
|
+
args,
|
|
2022
|
+
opts: { cwd: repoPath, env: process.env, cols: 80, rows: 24, useConpty: true }
|
|
2023
|
+
});
|
|
2024
|
+
} catch (err) {
|
|
2025
|
+
emit(ctx, featureId, {
|
|
2026
|
+
type: "testdrive.dev_failed",
|
|
2027
|
+
message: `dev server failed to start: ${err instanceof Error ? err.message : String(err)}`,
|
|
2028
|
+
data: { paneId }
|
|
2029
|
+
});
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
let scanned = "";
|
|
2033
|
+
let found = false;
|
|
2034
|
+
const sniffer = {
|
|
2035
|
+
sendData(chunk) {
|
|
2036
|
+
if (found)
|
|
2037
|
+
return;
|
|
2038
|
+
scanned = (scanned + chunk.toString("utf8")).slice(-SNIFF_WINDOW);
|
|
2039
|
+
const url = sniffDevUrl(scanned);
|
|
2040
|
+
if (url) {
|
|
2041
|
+
found = true;
|
|
2042
|
+
onUrl(url);
|
|
2043
|
+
}
|
|
2044
|
+
},
|
|
2045
|
+
sendControl() {}
|
|
2046
|
+
};
|
|
2047
|
+
ptyRegistry().attach(paneId, sniffer);
|
|
1856
2048
|
emit(ctx, featureId, {
|
|
1857
|
-
type: "
|
|
1858
|
-
message:
|
|
1859
|
-
data: {
|
|
2049
|
+
type: "testdrive.dev_started",
|
|
2050
|
+
message: `dev server spawned: ${devCommand}`,
|
|
2051
|
+
data: { paneId }
|
|
1860
2052
|
});
|
|
1861
|
-
return
|
|
1862
|
-
}
|
|
1863
|
-
function getTicket(ctx, id) {
|
|
1864
|
-
const row = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
1865
|
-
if (!row)
|
|
1866
|
-
throw new NotFoundError(`ticket ${id} not found`);
|
|
1867
|
-
return rowToTicket(row);
|
|
1868
|
-
}
|
|
1869
|
-
function assertMutable(t, verb) {
|
|
1870
|
-
if (!MUTABLE_STATUSES.has(t.status)) {
|
|
1871
|
-
throw new InvalidInputError(`cannot ${verb} ticket ${t.seq} \u2014 it is ${t.status}; only pending or failed tickets can be ${verb}${verb.endsWith("l") ? "led" : "ed"}`);
|
|
1872
|
-
}
|
|
2053
|
+
return paneId;
|
|
1873
2054
|
}
|
|
1874
|
-
function
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
set.goal = patch.goal;
|
|
1882
|
-
if (patch.context !== undefined)
|
|
1883
|
-
set.context = patch.context;
|
|
1884
|
-
if (patch.acceptanceCriteria !== undefined)
|
|
1885
|
-
set.acceptanceCriteria = patch.acceptanceCriteria;
|
|
1886
|
-
if (patch.seams !== undefined)
|
|
1887
|
-
set.seams = patch.seams;
|
|
1888
|
-
if (Object.keys(set).length === 0) {
|
|
1889
|
-
throw new InvalidInputError("nothing to edit \u2014 pass at least one content field");
|
|
2055
|
+
function stopDevPane(paneId) {
|
|
2056
|
+
const reg = ptyRegistry();
|
|
2057
|
+
const entry = reg.get(paneId);
|
|
2058
|
+
if (entry && !entry.exited && process.platform !== "win32") {
|
|
2059
|
+
try {
|
|
2060
|
+
process.kill(-entry.pty.pid, "SIGTERM");
|
|
2061
|
+
} catch {}
|
|
1890
2062
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
type: "ticket.edited",
|
|
1894
|
-
message: `ticket ${current2.seq} content edited (${Object.keys(set).join(", ")})`,
|
|
1895
|
-
ticketId: id,
|
|
1896
|
-
data: { fields: Object.keys(set) }
|
|
1897
|
-
});
|
|
1898
|
-
return getTicket(ctx, id);
|
|
1899
|
-
}
|
|
1900
|
-
function cancelTicket(ctx, id, reason) {
|
|
1901
|
-
const current2 = getTicket(ctx, id);
|
|
1902
|
-
assertMutable(current2, "cancel");
|
|
1903
|
-
ctx.db.update(tickets).set({ status: "cancelled", error: reason?.trim() ? reason.trim() : null }).where(eq5(tickets.id, id)).run();
|
|
1904
|
-
emit(ctx, current2.featureId, {
|
|
1905
|
-
type: "ticket.cancelled",
|
|
1906
|
-
message: `ticket ${current2.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
|
|
1907
|
-
ticketId: id,
|
|
1908
|
-
data: { reason: reason ?? null }
|
|
1909
|
-
});
|
|
1910
|
-
return getTicket(ctx, id);
|
|
1911
|
-
}
|
|
1912
|
-
function sweepOrphanedBurning(ctx, featureId, reason) {
|
|
1913
|
-
const orphaned = ctx.db.select().from(tickets).where(and5(eq5(tickets.featureId, featureId), eq5(tickets.status, "burning"))).all().map(rowToTicket);
|
|
1914
|
-
for (const t of orphaned) {
|
|
1915
|
-
updateTicket(ctx, t.id, { status: "failed", error: reason });
|
|
1916
|
-
emit(ctx, featureId, {
|
|
1917
|
-
type: "ticket.failed",
|
|
1918
|
-
message: `ticket ${t.seq} failed: ${reason}`,
|
|
1919
|
-
ticketId: t.id,
|
|
1920
|
-
data: { error: reason, orphaned: true }
|
|
1921
|
-
});
|
|
1922
|
-
}
|
|
1923
|
-
return orphaned.map((t) => ({ ...t, status: "failed", error: reason }));
|
|
1924
|
-
}
|
|
1925
|
-
function updateTicket(ctx, id, patch) {
|
|
1926
|
-
const current2 = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
1927
|
-
if (!current2)
|
|
1928
|
-
throw new NotFoundError(`ticket ${id} not found`);
|
|
1929
|
-
const set = {};
|
|
1930
|
-
if (patch.status !== undefined)
|
|
1931
|
-
set.status = patch.status;
|
|
1932
|
-
if (patch.commits !== undefined)
|
|
1933
|
-
set.commits = patch.commits;
|
|
1934
|
-
if (patch.error !== undefined)
|
|
1935
|
-
set.error = patch.error;
|
|
1936
|
-
if (patch.attemptBranch !== undefined)
|
|
1937
|
-
set.attemptBranch = patch.attemptBranch;
|
|
1938
|
-
if (patch.conflictFiles !== undefined)
|
|
1939
|
-
set.conflictFiles = patch.conflictFiles;
|
|
1940
|
-
ctx.db.update(tickets).set(set).where(eq5(tickets.id, id)).run();
|
|
1941
|
-
emit(ctx, current2.featureId, {
|
|
1942
|
-
type: "ticket.updated",
|
|
1943
|
-
message: `ticket ${current2.seq} \u2192 ${patch.status ?? current2.status}`,
|
|
1944
|
-
ticketId: id,
|
|
1945
|
-
data: patch
|
|
1946
|
-
});
|
|
1947
|
-
const updated = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
1948
|
-
return rowToTicket(updated);
|
|
1949
|
-
}
|
|
1950
|
-
var MUTABLE_STATUSES;
|
|
1951
|
-
var init_tickets = __esm(() => {
|
|
1952
|
-
init_src();
|
|
1953
|
-
init_schema();
|
|
1954
|
-
init_errors();
|
|
1955
|
-
init_events();
|
|
1956
|
-
MUTABLE_STATUSES = new Set(["pending", "failed"]);
|
|
1957
|
-
});
|
|
1958
|
-
|
|
1959
|
-
// src/services/gates.ts
|
|
1960
|
-
import { existsSync as existsSync6 } from "fs";
|
|
1961
|
-
function checkGate(ctx, check, feature) {
|
|
1962
|
-
switch (check) {
|
|
1963
|
-
case "decisions-file-exists":
|
|
1964
|
-
return fileGate(ctx, feature, "decisions.md", "run the ideation session to capture decisions first");
|
|
1965
|
-
case "all-waypoints-terminal": {
|
|
1966
|
-
const wps = listByFeature(ctx, feature.id);
|
|
1967
|
-
if (wps.length === 0) {
|
|
1968
|
-
return { satisfied: false, reason: "no waypoints charted yet \u2014 chart the map before converging" };
|
|
1969
|
-
}
|
|
1970
|
-
const open = wps.filter((w) => w.status !== "resolved" && w.status !== "dropped");
|
|
1971
|
-
return open.length === 0 ? { satisfied: true } : {
|
|
1972
|
-
satisfied: false,
|
|
1973
|
-
reason: notYetTerminal("waypoint", open.map((w) => w.status), ["open", "claimed"])
|
|
1974
|
-
};
|
|
1975
|
-
}
|
|
1976
|
-
case "spec-file-exists":
|
|
1977
|
-
return fileGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
|
|
1978
|
-
case "tickets-approved": {
|
|
1979
|
-
const count = listByFeature2(ctx, feature.id).filter((t) => t.status !== "cancelled").length;
|
|
1980
|
-
return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
|
|
1981
|
-
}
|
|
1982
|
-
case "all-tickets-terminal": {
|
|
1983
|
-
const tickets2 = listByFeature2(ctx, feature.id);
|
|
1984
|
-
if (tickets2.length === 0)
|
|
1985
|
-
return { satisfied: false, reason: "no tickets have run yet" };
|
|
1986
|
-
const open = tickets2.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
1987
|
-
return open.length === 0 ? { satisfied: true } : {
|
|
1988
|
-
satisfied: false,
|
|
1989
|
-
reason: notYetTerminal("ticket", open.map((t) => t.status), ["pending", "burning"])
|
|
1990
|
-
};
|
|
1991
|
-
}
|
|
1992
|
-
case "human-merge":
|
|
1993
|
-
return { satisfied: false, reason: "use the Merge button to ship" };
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
function notYetTerminal(noun, statuses, order) {
|
|
1997
|
-
const counts = new Map;
|
|
1998
|
-
for (const s of statuses)
|
|
1999
|
-
counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
2000
|
-
const ordered = [
|
|
2001
|
-
...order.filter((s) => counts.has(s)),
|
|
2002
|
-
...[...counts.keys()].filter((s) => !order.includes(s))
|
|
2003
|
-
];
|
|
2004
|
-
const breakdown = ordered.map((s) => `${counts.get(s)} ${s}`).join(", ");
|
|
2005
|
-
return `${statuses.length} ${statuses.length === 1 ? noun : `${noun}s`} not yet terminal (${breakdown})`;
|
|
2006
|
-
}
|
|
2007
|
-
function fileGate(ctx, feature, fileName, reason) {
|
|
2008
|
-
const project = projectForFeature(ctx, feature);
|
|
2009
|
-
return existsSync6(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
|
|
2010
|
-
}
|
|
2011
|
-
function overrideGate(ctx, featureId, gate, reason) {
|
|
2012
|
-
const feature = getFeatureRow(ctx, featureId);
|
|
2013
|
-
ctx.db.insert(gateOverrides).values({ featureId, gate, reason, ts: Date.now() }).run();
|
|
2014
|
-
emit(ctx, featureId, {
|
|
2015
|
-
type: "gate.overridden",
|
|
2016
|
-
message: `gate ${gate} overridden: ${reason}`,
|
|
2017
|
-
data: { gate, reason }
|
|
2018
|
-
});
|
|
2019
|
-
const next = nextPhase(feature);
|
|
2020
|
-
if (next)
|
|
2021
|
-
return setPhase(ctx, featureId, next, "phase.advanced", `advanced to ${next} (override)`);
|
|
2022
|
-
return feature;
|
|
2023
|
-
}
|
|
2024
|
-
var init_gates = __esm(() => {
|
|
2025
|
-
init_src();
|
|
2026
|
-
init_schema();
|
|
2027
|
-
init_events();
|
|
2028
|
-
init_feature_docs();
|
|
2029
|
-
init_repo();
|
|
2030
|
-
init_tickets();
|
|
2031
|
-
init_waypoints();
|
|
2032
|
-
});
|
|
2033
|
-
|
|
2034
|
-
// src/pty/dev-pane.ts
|
|
2035
|
-
function drivePaneId(featureId) {
|
|
2036
|
-
return `${DRIVE_PREFIX}${featureId}`;
|
|
2037
|
-
}
|
|
2038
|
-
function sniffDevUrl(text2) {
|
|
2039
|
-
const m = text2.match(/https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/[^\s"'<>`\x1b]*)?/i);
|
|
2040
|
-
return m ? m[0] : undefined;
|
|
2041
|
-
}
|
|
2042
|
-
function devSpawnTarget(devCommand) {
|
|
2043
|
-
if (process.platform === "win32") {
|
|
2044
|
-
return { file: process.env.ComSpec ?? "cmd.exe", args: ["/d", "/s", "/c", devCommand] };
|
|
2045
|
-
}
|
|
2046
|
-
return { file: "/bin/sh", args: ["-c", devCommand] };
|
|
2047
|
-
}
|
|
2048
|
-
function startDevPane(input) {
|
|
2049
|
-
const { ctx, featureId, repoPath, devCommand, onUrl } = input;
|
|
2050
|
-
const paneId = drivePaneId(featureId);
|
|
2051
|
-
const { file, args } = devSpawnTarget(devCommand);
|
|
2052
|
-
try {
|
|
2053
|
-
ptyRegistry().create({
|
|
2054
|
-
sessionId: paneId,
|
|
2055
|
-
cmd: file,
|
|
2056
|
-
args,
|
|
2057
|
-
opts: { cwd: repoPath, env: process.env, cols: 80, rows: 24, useConpty: true }
|
|
2058
|
-
});
|
|
2059
|
-
} catch (err) {
|
|
2060
|
-
emit(ctx, featureId, {
|
|
2061
|
-
type: "testdrive.dev_failed",
|
|
2062
|
-
message: `dev server failed to start: ${err instanceof Error ? err.message : String(err)}`,
|
|
2063
|
-
data: { paneId }
|
|
2064
|
-
});
|
|
2065
|
-
return;
|
|
2066
|
-
}
|
|
2067
|
-
let scanned = "";
|
|
2068
|
-
let found = false;
|
|
2069
|
-
const sniffer = {
|
|
2070
|
-
sendData(chunk) {
|
|
2071
|
-
if (found)
|
|
2072
|
-
return;
|
|
2073
|
-
scanned = (scanned + chunk.toString("utf8")).slice(-SNIFF_WINDOW);
|
|
2074
|
-
const url = sniffDevUrl(scanned);
|
|
2075
|
-
if (url) {
|
|
2076
|
-
found = true;
|
|
2077
|
-
onUrl(url);
|
|
2078
|
-
}
|
|
2079
|
-
},
|
|
2080
|
-
sendControl() {}
|
|
2081
|
-
};
|
|
2082
|
-
ptyRegistry().attach(paneId, sniffer);
|
|
2083
|
-
emit(ctx, featureId, {
|
|
2084
|
-
type: "testdrive.dev_started",
|
|
2085
|
-
message: `dev server spawned: ${devCommand}`,
|
|
2086
|
-
data: { paneId }
|
|
2087
|
-
});
|
|
2088
|
-
return paneId;
|
|
2089
|
-
}
|
|
2090
|
-
function stopDevPane(paneId) {
|
|
2091
|
-
const reg = ptyRegistry();
|
|
2092
|
-
const entry = reg.get(paneId);
|
|
2093
|
-
if (entry && !entry.exited && process.platform !== "win32") {
|
|
2094
|
-
try {
|
|
2095
|
-
process.kill(-entry.pty.pid, "SIGTERM");
|
|
2096
|
-
} catch {}
|
|
2097
|
-
}
|
|
2098
|
-
reg.kill(paneId);
|
|
2099
|
-
reg.remove(paneId);
|
|
2063
|
+
reg.kill(paneId);
|
|
2064
|
+
reg.remove(paneId);
|
|
2100
2065
|
}
|
|
2101
2066
|
var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192;
|
|
2102
2067
|
var init_dev_pane = __esm(() => {
|
|
@@ -2105,10 +2070,13 @@ var init_dev_pane = __esm(() => {
|
|
|
2105
2070
|
});
|
|
2106
2071
|
|
|
2107
2072
|
// src/services/git.ts
|
|
2108
|
-
import { existsSync as
|
|
2073
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, mkdtempSync, realpathSync, rmSync } from "fs";
|
|
2109
2074
|
import { tmpdir } from "os";
|
|
2110
|
-
import { dirname, join as join6, resolve as
|
|
2075
|
+
import { dirname as dirname2, join as join6, resolve as resolve4 } from "path";
|
|
2111
2076
|
import { simpleGit } from "simple-git";
|
|
2077
|
+
function migrationPaths(paths) {
|
|
2078
|
+
return paths.filter((p) => MIGRATION_DIR_RE.test(p) || DRIZZLE_SQL_RE.test(p));
|
|
2079
|
+
}
|
|
2112
2080
|
function featureBranch(slug) {
|
|
2113
2081
|
return `feature/${slug}`;
|
|
2114
2082
|
}
|
|
@@ -2119,14 +2087,14 @@ function errMsg(e) {
|
|
|
2119
2087
|
return e instanceof Error ? e.message : String(e);
|
|
2120
2088
|
}
|
|
2121
2089
|
function canon(p) {
|
|
2122
|
-
let abs =
|
|
2090
|
+
let abs = resolve4(p);
|
|
2123
2091
|
try {
|
|
2124
2092
|
abs = realpathSync.native(abs);
|
|
2125
2093
|
} catch {}
|
|
2126
2094
|
return process.platform === "win32" ? abs.replace(/\\/g, "/").toLowerCase() : abs;
|
|
2127
2095
|
}
|
|
2128
2096
|
async function assertRepo(repoPath) {
|
|
2129
|
-
if (!
|
|
2097
|
+
if (!existsSync6(repoPath)) {
|
|
2130
2098
|
throw new InvalidInputError(`path does not exist: ${repoPath}`);
|
|
2131
2099
|
}
|
|
2132
2100
|
let isRepo = false;
|
|
@@ -2215,7 +2183,7 @@ async function ensureTalkWorktree(project, feature) {
|
|
|
2215
2183
|
await ensureBranchExists(g, branch, feature.baseBranch ?? project.mainBranch);
|
|
2216
2184
|
if (await worktreeIsValid(g, worktreePath, branch))
|
|
2217
2185
|
return worktreePath;
|
|
2218
|
-
mkdirSync2(
|
|
2186
|
+
mkdirSync2(dirname2(worktreePath), { recursive: true });
|
|
2219
2187
|
try {
|
|
2220
2188
|
await g.raw(["worktree", "add", worktreePath, branch]);
|
|
2221
2189
|
return worktreePath;
|
|
@@ -2238,7 +2206,7 @@ async function ensureBranchExists(g, branch, from) {
|
|
|
2238
2206
|
}
|
|
2239
2207
|
}
|
|
2240
2208
|
async function worktreeIsValid(g, worktreePath, branch) {
|
|
2241
|
-
if (!
|
|
2209
|
+
if (!existsSync6(worktreePath))
|
|
2242
2210
|
return false;
|
|
2243
2211
|
const registered = await registeredWorktrees(g);
|
|
2244
2212
|
if (!registered.has(canon(worktreePath)))
|
|
@@ -2284,7 +2252,7 @@ async function worktreesOnBranch(g, branch, mainRepoPath) {
|
|
|
2284
2252
|
return (await listWorktrees(g)).filter((e) => e.branch === branch && canon(e.path) !== main).map((e) => e.path);
|
|
2285
2253
|
}
|
|
2286
2254
|
async function detachWorktree(path) {
|
|
2287
|
-
if (!
|
|
2255
|
+
if (!existsSync6(path))
|
|
2288
2256
|
return false;
|
|
2289
2257
|
const g = git(path);
|
|
2290
2258
|
let head;
|
|
@@ -2299,12 +2267,27 @@ async function detachWorktree(path) {
|
|
|
2299
2267
|
return true;
|
|
2300
2268
|
}
|
|
2301
2269
|
async function reattachWorktree(path, branch) {
|
|
2302
|
-
if (!
|
|
2270
|
+
if (!existsSync6(path))
|
|
2303
2271
|
return;
|
|
2304
2272
|
try {
|
|
2305
2273
|
await git(path).checkout(branch);
|
|
2306
2274
|
} catch {}
|
|
2307
2275
|
}
|
|
2276
|
+
function prepBranchName(unique) {
|
|
2277
|
+
return `${PREP_BRANCH_PREFIX}${unique}`;
|
|
2278
|
+
}
|
|
2279
|
+
async function readFileAtRef(repoPath, ref, path) {
|
|
2280
|
+
try {
|
|
2281
|
+
return await git(repoPath).raw(["show", `${ref}:${path}`]);
|
|
2282
|
+
} catch {
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
async function deletePrepBranch(repoPath, branch) {
|
|
2287
|
+
try {
|
|
2288
|
+
await deleteBranchDetachingWorktrees(git(repoPath), repoPath, branch);
|
|
2289
|
+
} catch {}
|
|
2290
|
+
}
|
|
2308
2291
|
function tempBranchSlugSegment(slug) {
|
|
2309
2292
|
return slug.slice(0, TEMP_BRANCH_SLUG_MAX).replace(/-+$/, "");
|
|
2310
2293
|
}
|
|
@@ -2322,14 +2305,14 @@ async function cleanupBurnWorktree(repoPath, branch, opts = {}) {
|
|
|
2322
2305
|
const attempts = Math.max(1, opts.attempts ?? 3);
|
|
2323
2306
|
const delayMs = opts.delayMs ?? 750;
|
|
2324
2307
|
const g = git(repoPath);
|
|
2325
|
-
for (let attempt = 1;attempt <= attempts &&
|
|
2308
|
+
for (let attempt = 1;attempt <= attempts && existsSync6(path); attempt++) {
|
|
2326
2309
|
try {
|
|
2327
2310
|
await g.raw(["worktree", "remove", "--force", path]);
|
|
2328
2311
|
} catch {}
|
|
2329
|
-
if (
|
|
2312
|
+
if (existsSync6(path) && attempt < attempts)
|
|
2330
2313
|
await sleep(delayMs);
|
|
2331
2314
|
}
|
|
2332
|
-
if (
|
|
2315
|
+
if (existsSync6(path)) {
|
|
2333
2316
|
try {
|
|
2334
2317
|
rmSync(path, { recursive: true, force: true });
|
|
2335
2318
|
} catch {}
|
|
@@ -2337,7 +2320,7 @@ async function cleanupBurnWorktree(repoPath, branch, opts = {}) {
|
|
|
2337
2320
|
try {
|
|
2338
2321
|
await g.raw(["worktree", "prune"]);
|
|
2339
2322
|
} catch {}
|
|
2340
|
-
return !
|
|
2323
|
+
return !existsSync6(path);
|
|
2341
2324
|
}
|
|
2342
2325
|
function sleep(ms) {
|
|
2343
2326
|
return new Promise((done) => setTimeout(done, ms));
|
|
@@ -2351,6 +2334,32 @@ async function branchCommitsAhead(repoPath, base, branch) {
|
|
|
2351
2334
|
return [];
|
|
2352
2335
|
}
|
|
2353
2336
|
}
|
|
2337
|
+
async function headSha(repoPath, ref) {
|
|
2338
|
+
try {
|
|
2339
|
+
const out = (await git(repoPath).revparse([ref])).trim();
|
|
2340
|
+
return out.length > 0 ? out : undefined;
|
|
2341
|
+
} catch {
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
async function commitsSince(repoPath, sha, branch) {
|
|
2346
|
+
try {
|
|
2347
|
+
const out = (await git(repoPath).raw(["rev-list", "--count", `${sha}..${branch}`])).trim();
|
|
2348
|
+
const n = Number(out);
|
|
2349
|
+
return Number.isFinite(n) ? n : undefined;
|
|
2350
|
+
} catch {
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
async function diffPaths(repoPath, from, to) {
|
|
2355
|
+
try {
|
|
2356
|
+
const out = (await git(repoPath).raw(["diff", "--name-only", from, to])).trim();
|
|
2357
|
+
return out ? out.split(`
|
|
2358
|
+
`).map((s) => s.trim()).filter(Boolean) : [];
|
|
2359
|
+
} catch {
|
|
2360
|
+
return [];
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2354
2363
|
async function commitSummaries(repoPath, base, branch, limit = 20) {
|
|
2355
2364
|
try {
|
|
2356
2365
|
const out = await git(repoPath).raw([
|
|
@@ -2546,7 +2555,7 @@ async function removeTalkWorktree(repoPath, worktreePath) {
|
|
|
2546
2555
|
try {
|
|
2547
2556
|
await g.raw(["worktree", "prune"]);
|
|
2548
2557
|
} catch {}
|
|
2549
|
-
if (
|
|
2558
|
+
if (existsSync6(worktreePath)) {
|
|
2550
2559
|
throw new InvalidInputError(`could not remove talk worktree at ${worktreePath} \u2014 a file may be locked; ` + "close anything using it and retry the delete");
|
|
2551
2560
|
}
|
|
2552
2561
|
}
|
|
@@ -2573,7 +2582,7 @@ async function deleteFeatureBranches(repoPath, slug) {
|
|
|
2573
2582
|
return { deleted, kept };
|
|
2574
2583
|
}
|
|
2575
2584
|
async function commitDocs(worktreePath, message) {
|
|
2576
|
-
if (!
|
|
2585
|
+
if (!existsSync6(resolve4(worktreePath, DOCS_PATHSPEC)))
|
|
2577
2586
|
return;
|
|
2578
2587
|
const g = git(worktreePath);
|
|
2579
2588
|
await g.add([DOCS_PATHSPEC]);
|
|
@@ -2618,6 +2627,7 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2618
2627
|
const devPaneId = testDriveState.devPaneId;
|
|
2619
2628
|
if (devPaneId)
|
|
2620
2629
|
stopDevPane(devPaneId);
|
|
2630
|
+
const carriedChanges = await dirtyPaths(g);
|
|
2621
2631
|
await g.checkout(previousBranch2);
|
|
2622
2632
|
if (detachedWorktree2)
|
|
2623
2633
|
await reattachWorktree(detachedWorktree2, branch);
|
|
@@ -2627,7 +2637,20 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2627
2637
|
message: `test drive stopped \u2014 back on ${previousBranch2}`,
|
|
2628
2638
|
data: { branch: previousBranch2 }
|
|
2629
2639
|
});
|
|
2630
|
-
|
|
2640
|
+
if (carriedChanges.length > 0) {
|
|
2641
|
+
emit(ctx, feature.id, {
|
|
2642
|
+
type: "testdrive.carried_changes",
|
|
2643
|
+
message: `${carriedChanges.length} uncommitted file(s) came back with you onto ${previousBranch2}: ${carriedChanges.slice(0, 5).join(", ")}${carriedChanges.length > 5 ? ", \u2026" : ""}`,
|
|
2644
|
+
data: { branch: previousBranch2, files: carriedChanges }
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
const dbDrift = await detectDbDrift(ctx, project, feature, previousBranch2, branch);
|
|
2648
|
+
return {
|
|
2649
|
+
ok: true,
|
|
2650
|
+
branch: previousBranch2,
|
|
2651
|
+
...carriedChanges.length > 0 ? { carriedChanges } : {},
|
|
2652
|
+
...dbDrift ? { dbDrift } : {}
|
|
2653
|
+
};
|
|
2631
2654
|
}
|
|
2632
2655
|
const porcelain = (await g.raw(["status", "--porcelain"])).trim();
|
|
2633
2656
|
if (porcelain !== "")
|
|
@@ -2664,6 +2687,35 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2664
2687
|
}
|
|
2665
2688
|
return { ok: true, branch };
|
|
2666
2689
|
}
|
|
2690
|
+
async function dirtyPaths(g) {
|
|
2691
|
+
try {
|
|
2692
|
+
const out = (await g.raw(["status", "--porcelain"])).trim();
|
|
2693
|
+
if (!out)
|
|
2694
|
+
return [];
|
|
2695
|
+
return out.split(`
|
|
2696
|
+
`).map((line) => line.slice(3).trim()).map((p) => p.includes(" -> ") ? p.split(" -> ").at(-1) ?? p : p).map((p) => p.replace(/^"|"$/g, "")).filter(Boolean);
|
|
2697
|
+
} catch {
|
|
2698
|
+
return [];
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
async function detectDbDrift(ctx, project, feature, previousBranch, driveBranch) {
|
|
2702
|
+
let files;
|
|
2703
|
+
try {
|
|
2704
|
+
files = migrationPaths(await diffPaths(project.repoPath, previousBranch, driveBranch));
|
|
2705
|
+
} catch {
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
if (files.length === 0)
|
|
2709
|
+
return;
|
|
2710
|
+
const resetCommand = project.dbResetCommand?.trim() || undefined;
|
|
2711
|
+
const drift = { files, ...resetCommand ? { resetCommand } : {} };
|
|
2712
|
+
emit(ctx, feature.id, {
|
|
2713
|
+
type: "testdrive.db_drift",
|
|
2714
|
+
message: `${driveBranch} and ${previousBranch} differ by ${files.length} migration file(s) \u2014 ` + "anything you migrated during the drive is still applied to your dev database, so the next " + `migrate on ${previousBranch} may report drift. ` + (resetCommand ? `Rebuild it with: ${resetCommand}` : 'Set a "Database reset command" in project settings to get a one-click fix here.'),
|
|
2715
|
+
data: { files, previousBranch, driveBranch, resetCommand: resetCommand ?? null }
|
|
2716
|
+
});
|
|
2717
|
+
return drift;
|
|
2718
|
+
}
|
|
2667
2719
|
async function mergeFeature(project, feature) {
|
|
2668
2720
|
const target = feature.baseBranch ?? project.mainBranch;
|
|
2669
2721
|
if (testDriveState) {
|
|
@@ -2719,171 +2771,18 @@ async function mergeInProgress(g) {
|
|
|
2719
2771
|
return false;
|
|
2720
2772
|
}
|
|
2721
2773
|
}
|
|
2722
|
-
var DOCS_PATHSPEC = "docs/features", DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", TEMP_BRANCH_PREFIXES, TEMP_BRANCH_SLUG_MAX = 16, testDriveState;
|
|
2774
|
+
var DOCS_PATHSPEC = "docs/features", MIGRATION_DIR_RE, DRIZZLE_SQL_RE, DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", PREP_BRANCH_PREFIX = "runcastle/prep/", TEMP_BRANCH_PREFIXES, TEMP_BRANCH_SLUG_MAX = 16, testDriveState;
|
|
2723
2775
|
var init_git = __esm(() => {
|
|
2724
2776
|
init_paths();
|
|
2725
2777
|
init_errors();
|
|
2726
2778
|
init_dev_pane();
|
|
2727
2779
|
init_events();
|
|
2728
2780
|
init_repo();
|
|
2781
|
+
MIGRATION_DIR_RE = /(^|\/)(migrations|migrate)\//i;
|
|
2782
|
+
DRIZZLE_SQL_RE = /(^|\/)drizzle\/.*\.sql$/i;
|
|
2729
2783
|
TEMP_BRANCH_PREFIXES = [RESEARCH_BRANCH_PREFIX, TICKET_BRANCH_PREFIX];
|
|
2730
2784
|
});
|
|
2731
2785
|
|
|
2732
|
-
// src/services/knowledge.ts
|
|
2733
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync } from "fs";
|
|
2734
|
-
import { join as join7, relative, resolve as resolve4, sep } from "path";
|
|
2735
|
-
function scaffoldDocs(ctx, feature) {
|
|
2736
|
-
const project = projectForFeature(ctx, feature);
|
|
2737
|
-
const dir = featureDocsDir(project, feature);
|
|
2738
|
-
mkdirSync3(dir, { recursive: true });
|
|
2739
|
-
const briefPath = featureDocPath(project, feature, "brief.md");
|
|
2740
|
-
if (!existsSync8(briefPath)) {
|
|
2741
|
-
const created = new Date().toISOString();
|
|
2742
|
-
const brief = [
|
|
2743
|
-
`# ${feature.title}`,
|
|
2744
|
-
"",
|
|
2745
|
-
feature.oneLiner,
|
|
2746
|
-
"",
|
|
2747
|
-
`- Slug: ${feature.slug}`,
|
|
2748
|
-
`- Created: ${created}`,
|
|
2749
|
-
""
|
|
2750
|
-
].join(`
|
|
2751
|
-
`);
|
|
2752
|
-
writeFileSync(briefPath, brief, "utf8");
|
|
2753
|
-
}
|
|
2754
|
-
if (feature.mapped) {
|
|
2755
|
-
scaffoldMapDoc(project, feature);
|
|
2756
|
-
}
|
|
2757
|
-
emit(ctx, feature.id, {
|
|
2758
|
-
type: "docs.scaffolded",
|
|
2759
|
-
message: feature.mapped ? `scaffolded docs/features/${feature.slug}/{brief,map}.md` : `scaffolded docs/features/${feature.slug}/brief.md`
|
|
2760
|
-
});
|
|
2761
|
-
}
|
|
2762
|
-
function scaffoldMapDoc(project, feature, seed) {
|
|
2763
|
-
mkdirSync3(featureDocsDir(project, feature), { recursive: true });
|
|
2764
|
-
const mapPath = featureDocPath(project, feature, "map.md");
|
|
2765
|
-
if (existsSync8(mapPath))
|
|
2766
|
-
return;
|
|
2767
|
-
writeFileSync(mapPath, mapDocBody(feature, seed), "utf8");
|
|
2768
|
-
}
|
|
2769
|
-
function mapDocBody(feature, seed) {
|
|
2770
|
-
const seeded = {
|
|
2771
|
-
Destination: seed?.destination?.trim(),
|
|
2772
|
-
Notes: seed?.notes?.trim()
|
|
2773
|
-
};
|
|
2774
|
-
const lines = [`# ${feature.title} \u2014 map`, ""];
|
|
2775
|
-
for (const section of MAP_SECTIONS) {
|
|
2776
|
-
lines.push(`## ${section}`, "");
|
|
2777
|
-
const body = seeded[section];
|
|
2778
|
-
if (body)
|
|
2779
|
-
lines.push(body, "");
|
|
2780
|
-
}
|
|
2781
|
-
return lines.join(`
|
|
2782
|
-
`);
|
|
2783
|
-
}
|
|
2784
|
-
function listDocs(ctx, feature) {
|
|
2785
|
-
const project = projectForFeature(ctx, feature);
|
|
2786
|
-
const dir = featureDocsDir(project, feature);
|
|
2787
|
-
if (!existsSync8(dir))
|
|
2788
|
-
return [];
|
|
2789
|
-
return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join7(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
2790
|
-
}
|
|
2791
|
-
function readDoc(ctx, feature, relPath) {
|
|
2792
|
-
const project = projectForFeature(ctx, feature);
|
|
2793
|
-
const dir = featureDocsDir(project, feature);
|
|
2794
|
-
const root = resolve4(dir);
|
|
2795
|
-
const target = resolve4(dir, relPath);
|
|
2796
|
-
const rel = relative(root, target);
|
|
2797
|
-
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
2798
|
-
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
2799
|
-
}
|
|
2800
|
-
if (!existsSync8(target))
|
|
2801
|
-
throw new NotFoundError(`doc not found: ${relPath}`);
|
|
2802
|
-
return { content: readFileSync3(target, "utf8") };
|
|
2803
|
-
}
|
|
2804
|
-
function readTitle(path, fallback) {
|
|
2805
|
-
try {
|
|
2806
|
-
const firstLine = readFileSync3(path, "utf8").split(`
|
|
2807
|
-
`, 1)[0] ?? "";
|
|
2808
|
-
const heading = firstLine.match(/^#\s+(.+)$/);
|
|
2809
|
-
return heading ? heading[1].trim() : fallback;
|
|
2810
|
-
} catch {
|
|
2811
|
-
return fallback;
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
|
-
var MAP_SECTIONS;
|
|
2815
|
-
var init_knowledge = __esm(() => {
|
|
2816
|
-
init_errors();
|
|
2817
|
-
init_events();
|
|
2818
|
-
init_feature_docs();
|
|
2819
|
-
init_repo();
|
|
2820
|
-
MAP_SECTIONS = [
|
|
2821
|
-
"Destination",
|
|
2822
|
-
"Notes",
|
|
2823
|
-
"Not yet specified",
|
|
2824
|
-
"Out of scope"
|
|
2825
|
-
];
|
|
2826
|
-
});
|
|
2827
|
-
|
|
2828
|
-
// src/pty/end-session.ts
|
|
2829
|
-
function endSession(ctx, sessionId) {
|
|
2830
|
-
const registry = ptyRegistry();
|
|
2831
|
-
const killed = registry.kill(sessionId);
|
|
2832
|
-
registry.remove(sessionId);
|
|
2833
|
-
const session = markSessionEnded(ctx, sessionId);
|
|
2834
|
-
if (session) {
|
|
2835
|
-
releaseForSession(ctx, sessionId);
|
|
2836
|
-
emit(ctx, session.featureId, {
|
|
2837
|
-
type: "session.ended",
|
|
2838
|
-
message: "session ended by user",
|
|
2839
|
-
data: { sessionId, killed }
|
|
2840
|
-
});
|
|
2841
|
-
} else {
|
|
2842
|
-
const existing = getSessionRow(ctx, sessionId);
|
|
2843
|
-
if (!existing)
|
|
2844
|
-
return { sessionId, ok: false, killed };
|
|
2845
|
-
}
|
|
2846
|
-
return { sessionId, ok: true, killed };
|
|
2847
|
-
}
|
|
2848
|
-
var init_end_session = __esm(() => {
|
|
2849
|
-
init_sessions();
|
|
2850
|
-
init_events();
|
|
2851
|
-
init_waypoints();
|
|
2852
|
-
init_registry();
|
|
2853
|
-
});
|
|
2854
|
-
|
|
2855
|
-
// src/launcher/skills-root.ts
|
|
2856
|
-
import { existsSync as existsSync9 } from "fs";
|
|
2857
|
-
import { dirname as dirname2, join as join8, resolve as resolve5 } from "path";
|
|
2858
|
-
function resolveSkillsRoot(fromDir) {
|
|
2859
|
-
const override = process.env[SKILLS_DIR_ENV];
|
|
2860
|
-
if (override) {
|
|
2861
|
-
const root = resolve5(override);
|
|
2862
|
-
if (existsSync9(join8(root, SKILLS_MARKER)))
|
|
2863
|
-
return root;
|
|
2864
|
-
throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
|
|
2865
|
-
}
|
|
2866
|
-
const searched = [];
|
|
2867
|
-
let dir = fromDir;
|
|
2868
|
-
for (let i = 0;i < 8; i += 1) {
|
|
2869
|
-
const candidate = join8(dir, "packages", "skills");
|
|
2870
|
-
searched.push(candidate);
|
|
2871
|
-
if (existsSync9(join8(candidate, SKILLS_MARKER)))
|
|
2872
|
-
return candidate;
|
|
2873
|
-
const parent = dirname2(dir);
|
|
2874
|
-
if (parent === dir)
|
|
2875
|
-
break;
|
|
2876
|
-
dir = parent;
|
|
2877
|
-
}
|
|
2878
|
-
throw new Error(`runcastle skills root not found; searched:
|
|
2879
|
-
${searched.join(`
|
|
2880
|
-
`)}`);
|
|
2881
|
-
}
|
|
2882
|
-
var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
|
|
2883
|
-
var init_skills_root = __esm(() => {
|
|
2884
|
-
SKILLS_MARKER = join8("packs", "runcastle");
|
|
2885
|
-
});
|
|
2886
|
-
|
|
2887
2786
|
// src/services/agent-stream.ts
|
|
2888
2787
|
function beginTranscript(ticketId) {
|
|
2889
2788
|
transcripts.set(ticketId, { live: true, chunks: [], nextIndex: 0, bytes: 0 });
|
|
@@ -3012,8 +2911,8 @@ var init_burn_guard = __esm(() => {
|
|
|
3012
2911
|
});
|
|
3013
2912
|
|
|
3014
2913
|
// src/workflows/ticket-burner.ts
|
|
3015
|
-
import { existsSync as
|
|
3016
|
-
import { dirname as dirname3, join as
|
|
2914
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
2915
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
3017
2916
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3018
2917
|
import { claudeCode, run } from "@ai-hero/sandcastle";
|
|
3019
2918
|
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
@@ -3129,13 +3028,13 @@ function parseEnvFile(content) {
|
|
|
3129
3028
|
if (line.length === 0 || line.startsWith("#"))
|
|
3130
3029
|
continue;
|
|
3131
3030
|
const body = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
3132
|
-
const
|
|
3133
|
-
if (
|
|
3031
|
+
const eq5 = body.indexOf("=");
|
|
3032
|
+
if (eq5 === -1)
|
|
3134
3033
|
continue;
|
|
3135
|
-
const key = body.slice(0,
|
|
3034
|
+
const key = body.slice(0, eq5).trim();
|
|
3136
3035
|
if (key.length === 0)
|
|
3137
3036
|
continue;
|
|
3138
|
-
let value = body.slice(
|
|
3037
|
+
let value = body.slice(eq5 + 1).trim();
|
|
3139
3038
|
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
3140
3039
|
value = value.slice(1, -1);
|
|
3141
3040
|
}
|
|
@@ -3236,12 +3135,12 @@ function buildVerifyNotes(config2) {
|
|
|
3236
3135
|
`);
|
|
3237
3136
|
}
|
|
3238
3137
|
function readRepoToolchain(repoPath) {
|
|
3239
|
-
const pkgPath =
|
|
3240
|
-
const hasPackageJson =
|
|
3138
|
+
const pkgPath = join7(repoPath, "package.json");
|
|
3139
|
+
const hasPackageJson = existsSync7(pkgPath);
|
|
3241
3140
|
let packageManagerField;
|
|
3242
3141
|
if (hasPackageJson) {
|
|
3243
3142
|
try {
|
|
3244
|
-
const pkg = JSON.parse(
|
|
3143
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3245
3144
|
if (typeof pkg.packageManager === "string")
|
|
3246
3145
|
packageManagerField = pkg.packageManager;
|
|
3247
3146
|
} catch {}
|
|
@@ -3250,10 +3149,10 @@ function readRepoToolchain(repoPath) {
|
|
|
3250
3149
|
hasPackageJson,
|
|
3251
3150
|
...packageManagerField !== undefined ? { packageManagerField } : {},
|
|
3252
3151
|
lockfiles: {
|
|
3253
|
-
bun:
|
|
3254
|
-
pnpm:
|
|
3255
|
-
yarn:
|
|
3256
|
-
npm:
|
|
3152
|
+
bun: existsSync7(join7(repoPath, "bun.lock")) || existsSync7(join7(repoPath, "bun.lockb")),
|
|
3153
|
+
pnpm: existsSync7(join7(repoPath, "pnpm-lock.yaml")),
|
|
3154
|
+
yarn: existsSync7(join7(repoPath, "yarn.lock")),
|
|
3155
|
+
npm: existsSync7(join7(repoPath, "package-lock.json"))
|
|
3257
3156
|
}
|
|
3258
3157
|
};
|
|
3259
3158
|
}
|
|
@@ -3394,18 +3293,18 @@ function buildRetryNotes(input) {
|
|
|
3394
3293
|
`);
|
|
3395
3294
|
}
|
|
3396
3295
|
function delayUnlessAborted(ms, signal) {
|
|
3397
|
-
return new Promise((
|
|
3296
|
+
return new Promise((resolve5) => {
|
|
3398
3297
|
if (signal.aborted) {
|
|
3399
|
-
|
|
3298
|
+
resolve5();
|
|
3400
3299
|
return;
|
|
3401
3300
|
}
|
|
3402
3301
|
const onAbort = () => {
|
|
3403
3302
|
clearTimeout(t);
|
|
3404
|
-
|
|
3303
|
+
resolve5();
|
|
3405
3304
|
};
|
|
3406
3305
|
const t = setTimeout(() => {
|
|
3407
3306
|
signal.removeEventListener("abort", onAbort);
|
|
3408
|
-
|
|
3307
|
+
resolve5();
|
|
3409
3308
|
}, ms);
|
|
3410
3309
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
3411
3310
|
});
|
|
@@ -3428,38 +3327,38 @@ function createSerialQueue() {
|
|
|
3428
3327
|
};
|
|
3429
3328
|
}
|
|
3430
3329
|
async function landWithResolve(branch, deps) {
|
|
3431
|
-
let
|
|
3330
|
+
let current = branch;
|
|
3432
3331
|
for (let attempt = 1;; attempt++) {
|
|
3433
|
-
const merge = await deps.merge(
|
|
3332
|
+
const merge = await deps.merge(current);
|
|
3434
3333
|
if (merge.ok)
|
|
3435
|
-
return { status: "landed", branch:
|
|
3334
|
+
return { status: "landed", branch: current };
|
|
3436
3335
|
const detail = merge.error ?? "merge failed";
|
|
3437
3336
|
if (!merge.conflict)
|
|
3438
|
-
return { status: "failed", branch:
|
|
3337
|
+
return { status: "failed", branch: current, error: detail };
|
|
3439
3338
|
const files = merge.files ?? [];
|
|
3440
3339
|
if (attempt > deps.maxResolveAttempts) {
|
|
3441
|
-
return { status: "conflict", branch:
|
|
3340
|
+
return { status: "conflict", branch: current, files, error: detail };
|
|
3442
3341
|
}
|
|
3443
3342
|
const where = files.length > 0 ? `${files.length} file(s)` : "the merge";
|
|
3444
3343
|
deps.emit({
|
|
3445
3344
|
type: "merge.conflict.resolving",
|
|
3446
3345
|
message: `${deps.label}: conflicts with ${deps.featureBranch} on ${where} \u2014 resolving with an agent (pass ${attempt}/${deps.maxResolveAttempts})`,
|
|
3447
|
-
data: { branch:
|
|
3346
|
+
data: { branch: current, files, attempt, maxAttempts: deps.maxResolveAttempts }
|
|
3448
3347
|
});
|
|
3449
3348
|
const resolved = await deps.resolve({
|
|
3450
|
-
branch:
|
|
3349
|
+
branch: current,
|
|
3451
3350
|
files,
|
|
3452
3351
|
attempt,
|
|
3453
3352
|
maxAttempts: deps.maxResolveAttempts
|
|
3454
3353
|
});
|
|
3455
|
-
|
|
3354
|
+
current = resolved.branch;
|
|
3456
3355
|
if (!resolved.ok) {
|
|
3457
|
-
return { status: "conflict", branch:
|
|
3356
|
+
return { status: "conflict", branch: current, files, error: resolved.error ?? detail };
|
|
3458
3357
|
}
|
|
3459
3358
|
deps.emit({
|
|
3460
3359
|
type: "merge.conflict.resolved",
|
|
3461
|
-
message: `${deps.label}: conflict resolved on ${
|
|
3462
|
-
data: { branch:
|
|
3360
|
+
message: `${deps.label}: conflict resolved on ${current} \u2014 landing`,
|
|
3361
|
+
data: { branch: current, files, attempt }
|
|
3463
3362
|
});
|
|
3464
3363
|
}
|
|
3465
3364
|
}
|
|
@@ -3617,26 +3516,26 @@ function resolverTemplatePath() {
|
|
|
3617
3516
|
}
|
|
3618
3517
|
function burnerAssetPath(file) {
|
|
3619
3518
|
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
3620
|
-
return
|
|
3519
|
+
return join7(resolveSkillsRoot(here), "burner", file);
|
|
3621
3520
|
}
|
|
3622
3521
|
function readDocsDigestFromDisk(projectId, slug) {
|
|
3623
3522
|
const worktree = worktreeDir(projectId, slug);
|
|
3624
|
-
if (!
|
|
3523
|
+
if (!existsSync7(worktree))
|
|
3625
3524
|
return "_No talk worktree on disk \u2014 docs digest skipped._";
|
|
3626
|
-
const docsDir =
|
|
3627
|
-
if (!
|
|
3525
|
+
const docsDir = join7(worktree, ...featureDocsRel(slug).split("/"));
|
|
3526
|
+
if (!existsSync7(docsDir))
|
|
3628
3527
|
return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
|
|
3629
|
-
const files =
|
|
3528
|
+
const files = readdirSync2(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync3(join7(docsDir, e.name), "utf8") }));
|
|
3630
3529
|
return buildDocsDigest(files);
|
|
3631
3530
|
}
|
|
3632
3531
|
function readBlockedFile(dirs) {
|
|
3633
3532
|
for (const dir of dirs) {
|
|
3634
3533
|
if (!dir)
|
|
3635
3534
|
continue;
|
|
3636
|
-
const p =
|
|
3637
|
-
if (
|
|
3535
|
+
const p = join7(dir, "BLOCKED.md");
|
|
3536
|
+
if (existsSync7(p)) {
|
|
3638
3537
|
try {
|
|
3639
|
-
return
|
|
3538
|
+
return readFileSync3(p, "utf8");
|
|
3640
3539
|
} catch {}
|
|
3641
3540
|
}
|
|
3642
3541
|
}
|
|
@@ -3689,8 +3588,9 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3689
3588
|
const featureBrief = buildFeatureBrief(feature);
|
|
3690
3589
|
const docsDigest = readDocsDigestFromDisk(project.id, feature.slug);
|
|
3691
3590
|
const workspaceNotes = buildWorkspaceNotes(workspaceMode);
|
|
3692
|
-
const
|
|
3693
|
-
const
|
|
3591
|
+
const prepared = resolvePreparedSettings(config2, project);
|
|
3592
|
+
const verifyNotes = buildVerifyNotes(prepared);
|
|
3593
|
+
const basePrompt = renderTicketPrompt(readFileSync3(burnerTemplatePath(), "utf8"), {
|
|
3694
3594
|
TICKET_JSON: ticketJson,
|
|
3695
3595
|
FEATURE_BRIEF: featureBrief,
|
|
3696
3596
|
DOCS_DIGEST: docsDigest,
|
|
@@ -3700,19 +3600,19 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3700
3600
|
});
|
|
3701
3601
|
const toolchain = readRepoToolchain(project.repoPath);
|
|
3702
3602
|
const pm = detectPackageManager(toolchain);
|
|
3703
|
-
const setupCommand = resolveSetupCommand(toolchain,
|
|
3603
|
+
const setupCommand = resolveSetupCommand(toolchain, prepared.setupCommand);
|
|
3704
3604
|
const mounts = [];
|
|
3705
3605
|
if (config2.sandbox !== "noSandbox" && pm) {
|
|
3706
3606
|
const mount = cacheMountFor(pm, burnCacheDir(pm));
|
|
3707
3607
|
if (mount) {
|
|
3708
|
-
|
|
3608
|
+
mkdirSync3(mount.hostPath, { recursive: true });
|
|
3709
3609
|
mounts.push(mount);
|
|
3710
3610
|
}
|
|
3711
3611
|
}
|
|
3712
3612
|
const guardInstall = config2.burnGuard && config2.sandbox !== "noSandbox" ? buildGuardInstallCommand() : undefined;
|
|
3713
3613
|
const withGuard = (setup) => guardInstall === undefined ? setup : setup ? `${guardInstall} && ${setup}` : guardInstall;
|
|
3714
|
-
|
|
3715
|
-
const logFilePath =
|
|
3614
|
+
mkdirSync3(logsDir(), { recursive: true });
|
|
3615
|
+
const logFilePath = join7(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
|
|
3716
3616
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
3717
3617
|
beginTranscript(ticket.id);
|
|
3718
3618
|
const timer = createToolTimer();
|
|
@@ -3758,7 +3658,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3758
3658
|
const runResolver = async (input) => {
|
|
3759
3659
|
const resolveBranch = ticketBranchName(feature.slug, ticket.seq, newId("r").slice(2, 10));
|
|
3760
3660
|
const otherSide = await commitSummaries(project.repoPath, input.branch, feature.branch);
|
|
3761
|
-
const prompt = renderTemplate(
|
|
3661
|
+
const prompt = renderTemplate(readFileSync3(resolverTemplatePath(), "utf8"), {
|
|
3762
3662
|
TICKET_JSON: ticketJson,
|
|
3763
3663
|
FEATURE_BRIEF: featureBrief,
|
|
3764
3664
|
DOCS_DIGEST: docsDigest,
|
|
@@ -4045,108 +3945,997 @@ ${blocked.trim()}` };
|
|
|
4045
3945
|
}
|
|
4046
3946
|
}
|
|
4047
3947
|
}
|
|
4048
|
-
function resolveBurnDeps(ctx) {
|
|
4049
|
-
const config2 = loadConfig();
|
|
4050
|
-
const token = readTokenFromEnvFile(envPath());
|
|
4051
|
-
const model = resolveModel("implement", config2, ctx.project, ctx.modelOverride);
|
|
4052
|
-
const land = createSerialQueue();
|
|
4053
|
-
let pushTargetReady;
|
|
4054
|
-
const ensureIsolatedPushTarget = () => pushTargetReady ??= allowPushToCheckedOutBranches(ctx.project.repoPath);
|
|
4055
|
-
return {
|
|
4056
|
-
config: config2,
|
|
4057
|
-
hasAuthToken: token !== undefined,
|
|
4058
|
-
concurrency: config2.burnConcurrency,
|
|
4059
|
-
executeTicketRun: (c, ticket) => realExecuteTicketRun(c, ticket, config2, token, model, land, ensureIsolatedPushTarget)
|
|
3948
|
+
function resolveBurnDeps(ctx) {
|
|
3949
|
+
const config2 = loadConfig();
|
|
3950
|
+
const token = readTokenFromEnvFile(envPath());
|
|
3951
|
+
const model = resolveModel("implement", config2, ctx.project, ctx.modelOverride);
|
|
3952
|
+
const land = createSerialQueue();
|
|
3953
|
+
let pushTargetReady;
|
|
3954
|
+
const ensureIsolatedPushTarget = () => pushTargetReady ??= allowPushToCheckedOutBranches(ctx.project.repoPath);
|
|
3955
|
+
return {
|
|
3956
|
+
config: config2,
|
|
3957
|
+
hasAuthToken: token !== undefined,
|
|
3958
|
+
concurrency: config2.burnConcurrency,
|
|
3959
|
+
executeTicketRun: (c, ticket) => realExecuteTicketRun(c, ticket, config2, token, model, land, ensureIsolatedPushTarget)
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
function readTokenFromEnvFile(path) {
|
|
3963
|
+
let fromFile;
|
|
3964
|
+
if (existsSync7(path)) {
|
|
3965
|
+
try {
|
|
3966
|
+
fromFile = parseEnvFile(readFileSync3(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
|
|
3967
|
+
} catch {
|
|
3968
|
+
fromFile = undefined;
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
3972
|
+
return token && token.length > 0 ? token : undefined;
|
|
3973
|
+
}
|
|
3974
|
+
var AUTH_MISSING_EVENT = "auth.missing", AUTH_MISSING_MESSAGE = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
3975
|
+
var init_ticket_burner = __esm(() => {
|
|
3976
|
+
init_src();
|
|
3977
|
+
init_config_load();
|
|
3978
|
+
init_paths();
|
|
3979
|
+
init_skills_root();
|
|
3980
|
+
init_agent_stream();
|
|
3981
|
+
init_git();
|
|
3982
|
+
init_burn_guard();
|
|
3983
|
+
PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
3984
|
+
PM_CACHE_SANDBOX_PATHS = {
|
|
3985
|
+
bun: "~/.bun/install/cache",
|
|
3986
|
+
yarn: "~/.cache/yarn",
|
|
3987
|
+
npm: "~/.npm"
|
|
3988
|
+
};
|
|
3989
|
+
RUN = String.raw`(?![\w./-])`;
|
|
3990
|
+
BASH_PATTERNS = [
|
|
3991
|
+
["tests", new RegExp(String.raw`\b(vitest|jest|pytest|go test|cargo test|bun test|playwright|maestro)${RUN}|\b(pnpm|npm|yarn|bun|npx)\b[^&;|]*?\s(run\s+)?test${RUN}|--testPathPatterns`)],
|
|
3992
|
+
["typecheck", /\btsc\b|\btypecheck\b|\bmypy\b|\bcargo check\b/],
|
|
3993
|
+
["build", new RegExp(String.raw`\bbuild${RUN}|\bprisma\s+(generate|migrate)\b|\bcodegen${RUN}`)],
|
|
3994
|
+
["lint", new RegExp(String.raw`\b(eslint|prettier|ruff|biome|lint|format)${RUN}`)],
|
|
3995
|
+
["install", /\b(pnpm|npm|yarn|bun|corepack)\s+\w*\s*install\b|\bnpm ci\b/],
|
|
3996
|
+
["git", /\bgit\b/],
|
|
3997
|
+
["search", /\b(grep|rg|ag|find)\b/],
|
|
3998
|
+
["file-read", /\b(cat|sed|head|tail|less|wc|ls)\b/],
|
|
3999
|
+
["file-edit", /<<\s*['"]?(PY|EOF|SH|JS|TS)\b|\bpython3?\s+-\s*<</]
|
|
4000
|
+
];
|
|
4001
|
+
TOOL_NAME_CATEGORY = {
|
|
4002
|
+
Read: "file-read",
|
|
4003
|
+
NotebookRead: "file-read",
|
|
4004
|
+
Grep: "search",
|
|
4005
|
+
Glob: "search",
|
|
4006
|
+
Edit: "file-edit",
|
|
4007
|
+
Write: "file-edit",
|
|
4008
|
+
NotebookEdit: "file-edit"
|
|
4009
|
+
};
|
|
4010
|
+
MAX_ATTRIBUTABLE_GAP_MS = 20 * 60000;
|
|
4011
|
+
BURN_WORKTREE_PATH = /[\\/]\.sandcastle[\\/]worktrees[\\/]/i;
|
|
4012
|
+
WORKTREE_REMOVAL_FAILURES = [
|
|
4013
|
+
/failed to delete/i,
|
|
4014
|
+
/directory not empty|enotempty/i,
|
|
4015
|
+
/unable to (unlink|delete)/i,
|
|
4016
|
+
/is not a working tree/i,
|
|
4017
|
+
/resource busy|ebusy|eperm|eacces/i
|
|
4018
|
+
];
|
|
4019
|
+
FATAL_ERROR_PATTERNS = [
|
|
4020
|
+
/invalid (api key|x-api-key)/i,
|
|
4021
|
+
/authentication|unauthorized|permission denied/i,
|
|
4022
|
+
/credit balance|billing/i,
|
|
4023
|
+
/oauth token|setup-token/i,
|
|
4024
|
+
/issue with the selected model|model not found|unknown model/i,
|
|
4025
|
+
/does not support resumeSession|resumeSession .* not found/i
|
|
4026
|
+
];
|
|
4027
|
+
RETRYABLE_ERROR_PATTERNS = [
|
|
4028
|
+
/exited with code \d+/i,
|
|
4029
|
+
/idle timeout|AgentIdleTimeout/i,
|
|
4030
|
+
/connection (closed|error|refused|reset)|socket hang up|fetch failed/i,
|
|
4031
|
+
/econnreset|etimedout|econnrefused|epipe|eai_again/i,
|
|
4032
|
+
/overloaded|rate.?limit|too many requests/i,
|
|
4033
|
+
/internal server error|service unavailable|bad gateway|gateway timeout/i,
|
|
4034
|
+
/\bapi error\b/i,
|
|
4035
|
+
/session capture failed/i
|
|
4036
|
+
];
|
|
4037
|
+
activeTicketAborts = new Map;
|
|
4038
|
+
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
4039
|
+
ticketBurner = {
|
|
4040
|
+
id: "ticket-burner",
|
|
4041
|
+
run: (ctx) => burnRun(ctx, resolveBurnDeps(ctx))
|
|
4042
|
+
};
|
|
4043
|
+
});
|
|
4044
|
+
|
|
4045
|
+
// src/workflows/project-prep.ts
|
|
4046
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "fs";
|
|
4047
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
4048
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4049
|
+
import { run as run2 } from "@ai-hero/sandcastle";
|
|
4050
|
+
import * as z3 from "zod";
|
|
4051
|
+
function parsePrepFindings(raw) {
|
|
4052
|
+
let json;
|
|
4053
|
+
try {
|
|
4054
|
+
json = JSON.parse(raw);
|
|
4055
|
+
} catch (e) {
|
|
4056
|
+
throw new Error(`findings file is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
4057
|
+
}
|
|
4058
|
+
const outer = RawFindings.safeParse(json);
|
|
4059
|
+
if (!outer.success)
|
|
4060
|
+
throw new Error("findings file is not a JSON object");
|
|
4061
|
+
const warnings = [];
|
|
4062
|
+
const values = {};
|
|
4063
|
+
const known = new Set(PREPARED_KEYS);
|
|
4064
|
+
for (const [key, rawValue] of Object.entries(outer.data)) {
|
|
4065
|
+
if (key === "notes")
|
|
4066
|
+
continue;
|
|
4067
|
+
if (!known.has(key)) {
|
|
4068
|
+
warnings.push(`ignored unknown key "${key}"`);
|
|
4069
|
+
continue;
|
|
4070
|
+
}
|
|
4071
|
+
const parsed = RawFinding.safeParse(rawValue);
|
|
4072
|
+
if (!parsed.success) {
|
|
4073
|
+
warnings.push(`ignored "${key}": not a string or {value, evidence} object`);
|
|
4074
|
+
continue;
|
|
4075
|
+
}
|
|
4076
|
+
const entry = parsed.data;
|
|
4077
|
+
const value = (typeof entry === "string" ? entry : entry.value ?? "").trim();
|
|
4078
|
+
if (value === "")
|
|
4079
|
+
continue;
|
|
4080
|
+
const evidence = typeof entry === "string" ? undefined : entry.evidence?.trim();
|
|
4081
|
+
values[key] = { value, ...evidence ? { evidence } : {} };
|
|
4082
|
+
}
|
|
4083
|
+
const notes = outer.data.notes?.trim();
|
|
4084
|
+
return { findings: { values, ...notes ? { notes } : {} }, warnings };
|
|
4085
|
+
}
|
|
4086
|
+
function renderPrepPrompt(template, values) {
|
|
4087
|
+
let out = template;
|
|
4088
|
+
for (const key of PLACEHOLDERS) {
|
|
4089
|
+
out = out.split(`{{${key}}}`).join(values[key]);
|
|
4090
|
+
}
|
|
4091
|
+
return out;
|
|
4092
|
+
}
|
|
4093
|
+
function buildRequestedKeysBlock(keys) {
|
|
4094
|
+
if (keys.length === 0) {
|
|
4095
|
+
return "_Everything is already established. Report an empty findings object and stop._";
|
|
4096
|
+
}
|
|
4097
|
+
const order = new Map(PREPARED_KEYS.map((k, i) => [k, i]));
|
|
4098
|
+
return [...keys].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0)).map((k) => `- ${KEY_BRIEF[k]}`).join(`
|
|
4099
|
+
`);
|
|
4100
|
+
}
|
|
4101
|
+
async function prepRun(ctx, deps) {
|
|
4102
|
+
if (ctx.keys.length === 0) {
|
|
4103
|
+
ctx.emitEvent({
|
|
4104
|
+
type: "prep.skipped",
|
|
4105
|
+
message: "nothing to prepare \u2014 every field is already set"
|
|
4106
|
+
});
|
|
4107
|
+
return { status: "succeeded", summary: "nothing to prepare" };
|
|
4108
|
+
}
|
|
4109
|
+
if (deps.config.sandbox !== "noSandbox" && !deps.hasAuthToken) {
|
|
4110
|
+
ctx.emitEvent({ type: AUTH_MISSING_EVENT2, message: AUTH_MISSING_MESSAGE2 });
|
|
4111
|
+
return { status: "failed", summary: "preparation aborted: auth token missing" };
|
|
4112
|
+
}
|
|
4113
|
+
ctx.emitEvent({
|
|
4114
|
+
type: "prep.started",
|
|
4115
|
+
message: `preparing ${ctx.project.name} \u2014 establishing ${ctx.keys.join(", ")}`,
|
|
4116
|
+
data: { keys: [...ctx.keys] }
|
|
4117
|
+
});
|
|
4118
|
+
const outcome = await deps.executePrepRun(ctx);
|
|
4119
|
+
if (outcome.status === "failed") {
|
|
4120
|
+
ctx.emitEvent({
|
|
4121
|
+
type: "prep.failed",
|
|
4122
|
+
message: `preparation failed: ${errorHeadline2(outcome.error)}`,
|
|
4123
|
+
data: { error: outcome.error }
|
|
4124
|
+
});
|
|
4125
|
+
return { status: "failed", summary: outcome.error };
|
|
4126
|
+
}
|
|
4127
|
+
for (const warning of outcome.warnings) {
|
|
4128
|
+
ctx.emitEvent({ type: "prep.warning", message: `preparation: ${warning}` });
|
|
4129
|
+
}
|
|
4130
|
+
const found = Object.keys(outcome.findings.values);
|
|
4131
|
+
const missed = ctx.keys.filter((k) => !found.includes(k));
|
|
4132
|
+
const summary = found.length === 0 ? "preparation established nothing" : `established ${found.join(", ")}${missed.length > 0 ? ` (could not establish ${missed.join(", ")})` : ""}`;
|
|
4133
|
+
return { status: "succeeded", summary, findings: outcome.findings };
|
|
4134
|
+
}
|
|
4135
|
+
function errorHeadline2(s) {
|
|
4136
|
+
const lines = s.split(`
|
|
4137
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
4138
|
+
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
4139
|
+
return causes.at(-1) ?? lines[0] ?? "";
|
|
4140
|
+
}
|
|
4141
|
+
function createPrepStreamThrottle(emit2, opts = {}) {
|
|
4142
|
+
return createStreamThrottle((e) => emit2({ ...e, type: e.type.replace(/^burn\./, "prep.") }), opts);
|
|
4143
|
+
}
|
|
4144
|
+
function prepTemplatePath() {
|
|
4145
|
+
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
4146
|
+
return join8(resolveSkillsRoot(here), "burner", "prepare-project.md");
|
|
4147
|
+
}
|
|
4148
|
+
async function realExecutePrepRun(ctx, config2, token, model) {
|
|
4149
|
+
const { project } = ctx;
|
|
4150
|
+
const branch = prepBranchName(newId("b").slice(2, 10));
|
|
4151
|
+
const toolchain = readRepoToolchain(project.repoPath);
|
|
4152
|
+
const pm = detectPackageManager(toolchain);
|
|
4153
|
+
const prepared = resolvePreparedSettings(config2, project);
|
|
4154
|
+
const setupCommand = resolveSetupCommand(toolchain, prepared.setupCommand);
|
|
4155
|
+
const workspaceMode = resolveBurnWorkspaceMode(config2);
|
|
4156
|
+
const mounts = [];
|
|
4157
|
+
if (config2.sandbox !== "noSandbox" && pm) {
|
|
4158
|
+
const mount = cacheMountFor(pm, burnCacheDir(pm));
|
|
4159
|
+
if (mount) {
|
|
4160
|
+
mkdirSync4(mount.hostPath, { recursive: true });
|
|
4161
|
+
mounts.push(mount);
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
const template = readFileSync4(prepTemplatePath(), "utf8");
|
|
4165
|
+
const prompt = renderPrepPrompt(template, {
|
|
4166
|
+
REQUESTED_KEYS: buildRequestedKeysBlock(ctx.keys),
|
|
4167
|
+
SETUP_COMMAND: setupCommand ?? "(no install step detected)"
|
|
4168
|
+
});
|
|
4169
|
+
mkdirSync4(logsDir(), { recursive: true });
|
|
4170
|
+
const logFilePath = join8(logsDir(), `prep-${project.id}.log`);
|
|
4171
|
+
const throttle = createPrepStreamThrottle((e) => ctx.emitEvent(e));
|
|
4172
|
+
if (setupCommand) {
|
|
4173
|
+
ctx.emitEvent({
|
|
4174
|
+
type: "prep.setup",
|
|
4175
|
+
message: `installing deps before the agent starts: ${setupCommand}`
|
|
4176
|
+
});
|
|
4177
|
+
}
|
|
4178
|
+
const hookCommand = workspaceMode === "isolated" ? buildIsolatedSetupCommand(branch, setupCommand, pm) : setupCommand;
|
|
4179
|
+
const runOptions = {
|
|
4180
|
+
agent: buildBurnAgent(config2, token, model),
|
|
4181
|
+
sandbox: selectSandbox(config2, mounts),
|
|
4182
|
+
cwd: project.repoPath,
|
|
4183
|
+
prompt,
|
|
4184
|
+
branchStrategy: { type: "branch", branch, baseBranch: project.mainBranch },
|
|
4185
|
+
signal: ctx.signal,
|
|
4186
|
+
name: "prepare",
|
|
4187
|
+
maxIterations: 1,
|
|
4188
|
+
...hookCommand ? {
|
|
4189
|
+
hooks: {
|
|
4190
|
+
sandbox: {
|
|
4191
|
+
onSandboxReady: [{ command: hookCommand, timeoutMs: PREP_SETUP_TIMEOUT_MS }]
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
} : {},
|
|
4195
|
+
logging: { type: "file", path: logFilePath, onAgentStreamEvent: throttle.onEvent }
|
|
4196
|
+
};
|
|
4197
|
+
try {
|
|
4198
|
+
let result;
|
|
4199
|
+
try {
|
|
4200
|
+
result = await run2(runOptions);
|
|
4201
|
+
} catch (err) {
|
|
4202
|
+
throttle.flush();
|
|
4203
|
+
if (ctx.signal.aborted)
|
|
4204
|
+
throw err;
|
|
4205
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4206
|
+
const salvaged = await readFindings(project.repoPath, branch);
|
|
4207
|
+
if (!salvaged)
|
|
4208
|
+
return { status: "failed", error: msg };
|
|
4209
|
+
ctx.emitEvent({
|
|
4210
|
+
type: "prep.salvaged",
|
|
4211
|
+
message: `the agent finished but sandcastle errored on teardown (${errorHeadline2(msg)}) \u2014 reading the findings it committed anyway`
|
|
4212
|
+
});
|
|
4213
|
+
return salvaged;
|
|
4214
|
+
}
|
|
4215
|
+
throttle.flush();
|
|
4216
|
+
if (result.commits.length === 0) {
|
|
4217
|
+
return {
|
|
4218
|
+
status: "failed",
|
|
4219
|
+
error: `agent produced no commits \u2014 no ${PREP_FINDINGS_PATH} landed`
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
const read = await readFindings(project.repoPath, branch);
|
|
4223
|
+
return read ?? {
|
|
4224
|
+
status: "failed",
|
|
4225
|
+
error: `agent committed, but ${PREP_FINDINGS_PATH} is missing from ${branch}`
|
|
4226
|
+
};
|
|
4227
|
+
} finally {
|
|
4228
|
+
await deletePrepBranch(project.repoPath, branch);
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
async function readFindings(repoPath, branch) {
|
|
4232
|
+
const raw = await readFileAtRef(repoPath, branch, PREP_FINDINGS_PATH);
|
|
4233
|
+
if (raw === undefined)
|
|
4234
|
+
return;
|
|
4235
|
+
try {
|
|
4236
|
+
const { findings, warnings } = parsePrepFindings(raw);
|
|
4237
|
+
return { status: "done", findings, warnings };
|
|
4238
|
+
} catch (e) {
|
|
4239
|
+
return { status: "failed", error: e instanceof Error ? e.message : String(e) };
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
function resolvePrepDeps(project) {
|
|
4243
|
+
const config2 = loadConfig();
|
|
4244
|
+
const token = readTokenFromEnvFile(envPath());
|
|
4245
|
+
const model = resolveModel("prepare", config2, project);
|
|
4246
|
+
return {
|
|
4247
|
+
config: config2,
|
|
4248
|
+
hasAuthToken: token !== undefined,
|
|
4249
|
+
executePrepRun: (c) => realExecutePrepRun(c, config2, token, model)
|
|
4250
|
+
};
|
|
4251
|
+
}
|
|
4252
|
+
async function prepHeadSha(project) {
|
|
4253
|
+
return existsSync8(project.repoPath) ? headSha(project.repoPath, project.mainBranch) : undefined;
|
|
4254
|
+
}
|
|
4255
|
+
var AUTH_MISSING_EVENT2 = "auth.missing", AUTH_MISSING_MESSAGE2 = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PREP_FINDINGS_PATH = ".runcastle/prep.json", PREP_SETUP_TIMEOUT_MS, RawFinding, RawFindings, PLACEHOLDERS, KEY_BRIEF;
|
|
4256
|
+
var init_project_prep = __esm(() => {
|
|
4257
|
+
init_src();
|
|
4258
|
+
init_config_load();
|
|
4259
|
+
init_paths();
|
|
4260
|
+
init_skills_root();
|
|
4261
|
+
init_git();
|
|
4262
|
+
init_ticket_burner();
|
|
4263
|
+
PREP_SETUP_TIMEOUT_MS = 15 * 60000;
|
|
4264
|
+
RawFinding = z3.union([
|
|
4265
|
+
z3.string(),
|
|
4266
|
+
z3.object({
|
|
4267
|
+
value: z3.string().nullish(),
|
|
4268
|
+
evidence: z3.string().nullish()
|
|
4269
|
+
})
|
|
4270
|
+
]);
|
|
4271
|
+
RawFindings = z3.object({ notes: z3.string().nullish() }).catchall(z3.unknown());
|
|
4272
|
+
PLACEHOLDERS = ["REQUESTED_KEYS", "SETUP_COMMAND"];
|
|
4273
|
+
KEY_BRIEF = {
|
|
4274
|
+
setupCommand: "**`setupCommand`** \u2014 the command that takes a clean checkout to a state where the code builds and tests run: dependency install, plus any codegen or contract build that every task would otherwise discover it needed mid-flight (ORM client generation, protobuf, a `contracts:build` step). Run it and report what worked.",
|
|
4275
|
+
verifyCommands: "**`verifyCommands`** \u2014 the exact typecheck / test / lint commands for this repo, one per line. In a monorepo these must include the correct workspace filter names. Run each one and confirm it executes the thing it claims to.",
|
|
4276
|
+
knownFailures: '**`knownFailures`** \u2014 which tests are ALREADY failing on this checkout, before anything is changed. Run the full suite once and report the count plus the failing suite names. "0 known failures" is a real, useful answer.',
|
|
4277
|
+
devCommand: "**`devCommand`** \u2014 the command that starts this project's dev server on a developer's own machine. Read it from config; do not run it here.",
|
|
4278
|
+
dbResetCommand: "**`dbResetCommand`** \u2014 the command that rebuilds the dev database from the migrations currently in the working tree. Read it from config; do not run it here. Omit it entirely if this repo has no database."
|
|
4279
|
+
};
|
|
4280
|
+
});
|
|
4281
|
+
|
|
4282
|
+
// src/services/findings.ts
|
|
4283
|
+
import { and as and5, eq as eq5 } from "drizzle-orm";
|
|
4284
|
+
function isPreparedKey(key) {
|
|
4285
|
+
return PREPARED_SET.has(key);
|
|
4286
|
+
}
|
|
4287
|
+
function findingRow(ctx, projectId, key) {
|
|
4288
|
+
return ctx.db.select({
|
|
4289
|
+
source: projectFindings.source,
|
|
4290
|
+
evidence: projectFindings.evidence,
|
|
4291
|
+
establishedAt: projectFindings.establishedAt,
|
|
4292
|
+
establishedSha: projectFindings.establishedSha
|
|
4293
|
+
}).from(projectFindings).where(and5(eq5(projectFindings.projectId, projectId), eq5(projectFindings.key, key))).get();
|
|
4294
|
+
}
|
|
4295
|
+
function isOverwritable(ctx, projectId, key) {
|
|
4296
|
+
return findingRow(ctx, projectId, key)?.source !== "human";
|
|
4297
|
+
}
|
|
4298
|
+
function recordFinding(ctx, projectId, input) {
|
|
4299
|
+
const { key, value, source, evidence, establishedSha } = input;
|
|
4300
|
+
ctx.db.transaction((tx) => {
|
|
4301
|
+
tx.update(projects).set({ [COLUMN_NAME[key]]: value }).where(eq5(projects.id, projectId)).run();
|
|
4302
|
+
if (value === null) {
|
|
4303
|
+
tx.delete(projectFindings).where(and5(eq5(projectFindings.projectId, projectId), eq5(projectFindings.key, key))).run();
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4306
|
+
tx.insert(projectFindings).values({
|
|
4307
|
+
projectId,
|
|
4308
|
+
key,
|
|
4309
|
+
source,
|
|
4310
|
+
evidence: evidence ?? null,
|
|
4311
|
+
establishedAt: Date.now(),
|
|
4312
|
+
establishedSha: establishedSha ?? null
|
|
4313
|
+
}).onConflictDoUpdate({
|
|
4314
|
+
target: [projectFindings.projectId, projectFindings.key],
|
|
4315
|
+
set: {
|
|
4316
|
+
source,
|
|
4317
|
+
evidence: evidence ?? null,
|
|
4318
|
+
establishedAt: Date.now(),
|
|
4319
|
+
establishedSha: establishedSha ?? null
|
|
4320
|
+
}
|
|
4321
|
+
}).run();
|
|
4322
|
+
});
|
|
4323
|
+
}
|
|
4324
|
+
function recordHuman(ctx, projectId, key, value) {
|
|
4325
|
+
if (!isPreparedKey(key))
|
|
4326
|
+
return;
|
|
4327
|
+
const existing = findingRow(ctx, projectId, key);
|
|
4328
|
+
if (value === null) {
|
|
4329
|
+
if (existing) {
|
|
4330
|
+
ctx.db.delete(projectFindings).where(and5(eq5(projectFindings.projectId, projectId), eq5(projectFindings.key, key))).run();
|
|
4331
|
+
}
|
|
4332
|
+
return;
|
|
4333
|
+
}
|
|
4334
|
+
ctx.db.insert(projectFindings).values({
|
|
4335
|
+
projectId,
|
|
4336
|
+
key,
|
|
4337
|
+
source: "human",
|
|
4338
|
+
evidence: null,
|
|
4339
|
+
establishedAt: Date.now(),
|
|
4340
|
+
establishedSha: null
|
|
4341
|
+
}).onConflictDoUpdate({
|
|
4342
|
+
target: [projectFindings.projectId, projectFindings.key],
|
|
4343
|
+
set: { source: "human", evidence: null, establishedAt: Date.now(), establishedSha: null }
|
|
4344
|
+
}).run();
|
|
4345
|
+
}
|
|
4346
|
+
async function listFindings(ctx, project) {
|
|
4347
|
+
const rows = ctx.db.select().from(projectFindings).where(eq5(projectFindings.projectId, project.id)).all();
|
|
4348
|
+
const distances = new Map;
|
|
4349
|
+
for (const row of rows) {
|
|
4350
|
+
const sha = row.establishedSha;
|
|
4351
|
+
if (!sha || distances.has(sha))
|
|
4352
|
+
continue;
|
|
4353
|
+
distances.set(sha, await commitsSince(project.repoPath, sha, project.mainBranch));
|
|
4354
|
+
}
|
|
4355
|
+
const order = new Map(PREPARED_KEYS.map((k, i) => [k, i]));
|
|
4356
|
+
return rows.filter((row) => isPreparedKey(row.key)).sort((a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0)).map((row) => {
|
|
4357
|
+
const staleCommits = row.establishedSha ? distances.get(row.establishedSha) : undefined;
|
|
4358
|
+
return {
|
|
4359
|
+
key: row.key,
|
|
4360
|
+
source: row.source,
|
|
4361
|
+
...row.evidence ? { evidence: row.evidence } : {},
|
|
4362
|
+
establishedAt: row.establishedAt,
|
|
4363
|
+
...row.establishedSha ? { establishedSha: row.establishedSha } : {},
|
|
4364
|
+
...staleCommits !== undefined ? { staleCommits } : {}
|
|
4365
|
+
};
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
function unsetPreparedKeys(project) {
|
|
4369
|
+
return PREPARED_KEYS.filter((key) => {
|
|
4370
|
+
const value = project[key];
|
|
4371
|
+
return value === undefined || value.trim() === "";
|
|
4372
|
+
});
|
|
4373
|
+
}
|
|
4374
|
+
var VALUE_COLUMN, COLUMN_NAME, PREPARED_SET;
|
|
4375
|
+
var init_findings = __esm(() => {
|
|
4376
|
+
init_src();
|
|
4377
|
+
init_schema();
|
|
4378
|
+
init_git();
|
|
4379
|
+
VALUE_COLUMN = {
|
|
4380
|
+
setupCommand: projects.setupCommand,
|
|
4381
|
+
verifyCommands: projects.verifyCommands,
|
|
4382
|
+
knownFailures: projects.knownFailures,
|
|
4383
|
+
devCommand: projects.devCommand,
|
|
4384
|
+
dbResetCommand: projects.dbResetCommand
|
|
4385
|
+
};
|
|
4386
|
+
COLUMN_NAME = {
|
|
4387
|
+
setupCommand: "setupCommand",
|
|
4388
|
+
verifyCommands: "verifyCommands",
|
|
4389
|
+
knownFailures: "knownFailures",
|
|
4390
|
+
devCommand: "devCommand",
|
|
4391
|
+
dbResetCommand: "dbResetCommand"
|
|
4392
|
+
};
|
|
4393
|
+
PREPARED_SET = new Set(PREPARED_KEYS);
|
|
4394
|
+
});
|
|
4395
|
+
|
|
4396
|
+
// src/services/prep.ts
|
|
4397
|
+
import { desc as desc3, eq as eq6 } from "drizzle-orm";
|
|
4398
|
+
function isPreparing(projectId) {
|
|
4399
|
+
return controllers.has(projectId);
|
|
4400
|
+
}
|
|
4401
|
+
function keysToPrepare(ctx, project, opts = {}) {
|
|
4402
|
+
const unset = new Set(unsetPreparedKeys(project));
|
|
4403
|
+
const candidates = opts.keys ?? (opts.refresh ? PREPARED_KEYS : [...unset]);
|
|
4404
|
+
return candidates.filter((key) => (opts.refresh || unset.has(key)) && isOverwritable(ctx, project.id, key));
|
|
4405
|
+
}
|
|
4406
|
+
async function startPrep(ctx, projectId, opts = {}) {
|
|
4407
|
+
const project = requireProjectById(ctx, projectId);
|
|
4408
|
+
if (controllers.has(projectId)) {
|
|
4409
|
+
throw new InvalidInputError(`${project.name} is already being prepared`);
|
|
4410
|
+
}
|
|
4411
|
+
const keys = keysToPrepare(ctx, project, opts);
|
|
4412
|
+
const prepId = newId("prep");
|
|
4413
|
+
const headSha2 = await prepHeadSha(project);
|
|
4414
|
+
ctx.db.insert(projectPreps).values({
|
|
4415
|
+
id: prepId,
|
|
4416
|
+
projectId,
|
|
4417
|
+
status: "running",
|
|
4418
|
+
startedAt: Date.now(),
|
|
4419
|
+
endedAt: null,
|
|
4420
|
+
summary: null,
|
|
4421
|
+
headSha: headSha2 ?? null
|
|
4422
|
+
}).run();
|
|
4423
|
+
emitProject(ctx, projectId, {
|
|
4424
|
+
type: "prep.run.started",
|
|
4425
|
+
message: keys.length === 0 ? `preparing ${project.name} \u2014 nothing left to establish` : `preparing ${project.name} \u2014 ${keys.length} field(s) to establish`,
|
|
4426
|
+
data: { prepId, keys }
|
|
4427
|
+
});
|
|
4428
|
+
const controller = new AbortController;
|
|
4429
|
+
controllers.set(projectId, { runId: prepId, controller });
|
|
4430
|
+
const prepCtx = {
|
|
4431
|
+
project,
|
|
4432
|
+
keys,
|
|
4433
|
+
emitEvent: (e) => {
|
|
4434
|
+
emitProject(ctx, projectId, { type: e.type, message: e.message, data: e.data });
|
|
4435
|
+
},
|
|
4436
|
+
signal: controller.signal
|
|
4437
|
+
};
|
|
4438
|
+
const deps = opts.deps ?? resolvePrepDeps(project);
|
|
4439
|
+
const done = finalize(ctx, project, prepId, prepCtx, deps, controller, headSha2);
|
|
4440
|
+
return { prepId, keys, done };
|
|
4441
|
+
}
|
|
4442
|
+
function cancelPrep(projectId) {
|
|
4443
|
+
controllers.get(projectId)?.controller.abort();
|
|
4444
|
+
}
|
|
4445
|
+
async function finalize(ctx, project, prepId, prepCtx, deps, controller, headSha2) {
|
|
4446
|
+
let status = "failed";
|
|
4447
|
+
let summary = "preparation failed";
|
|
4448
|
+
let findings;
|
|
4449
|
+
try {
|
|
4450
|
+
const result = await prepRun(prepCtx, deps);
|
|
4451
|
+
status = result.status === "succeeded" ? "succeeded" : "failed";
|
|
4452
|
+
summary = result.summary;
|
|
4453
|
+
findings = result.findings;
|
|
4454
|
+
} catch (e) {
|
|
4455
|
+
if (controller.signal.aborted) {
|
|
4456
|
+
status = "cancelled";
|
|
4457
|
+
summary = "preparation cancelled";
|
|
4458
|
+
} else {
|
|
4459
|
+
status = "failed";
|
|
4460
|
+
summary = e instanceof Error ? e.message : "preparation failed";
|
|
4461
|
+
}
|
|
4462
|
+
} finally {
|
|
4463
|
+
controllers.delete(project.id);
|
|
4464
|
+
}
|
|
4465
|
+
const applied = findings ? applyFindings(ctx, project, findings, headSha2) : [];
|
|
4466
|
+
ctx.db.update(projectPreps).set({ status, endedAt: Date.now(), summary }).where(eq6(projectPreps.id, prepId)).run();
|
|
4467
|
+
emitProject(ctx, project.id, {
|
|
4468
|
+
type: "prep.run.finished",
|
|
4469
|
+
message: `preparation ${status}: ${summary}`,
|
|
4470
|
+
data: { prepId, status, summary, applied }
|
|
4471
|
+
});
|
|
4472
|
+
}
|
|
4473
|
+
function applyFindings(ctx, project, findings, headSha2) {
|
|
4474
|
+
const applied = [];
|
|
4475
|
+
for (const [key, finding] of Object.entries(findings.values)) {
|
|
4476
|
+
if (!isOverwritable(ctx, project.id, key)) {
|
|
4477
|
+
emitProject(ctx, project.id, {
|
|
4478
|
+
type: "prep.skipped_field",
|
|
4479
|
+
message: `kept your ${key} \u2014 preparation proposed "${truncate(finding.value)}" but you set this by hand`,
|
|
4480
|
+
data: { key, proposed: finding.value }
|
|
4481
|
+
});
|
|
4482
|
+
continue;
|
|
4483
|
+
}
|
|
4484
|
+
recordFinding(ctx, project.id, {
|
|
4485
|
+
key,
|
|
4486
|
+
value: finding.value,
|
|
4487
|
+
source: "prep",
|
|
4488
|
+
...finding.evidence ? { evidence: finding.evidence } : {},
|
|
4489
|
+
...headSha2 ? { establishedSha: headSha2 } : {}
|
|
4490
|
+
});
|
|
4491
|
+
applied.push(key);
|
|
4492
|
+
emitProject(ctx, project.id, {
|
|
4493
|
+
type: "prep.established",
|
|
4494
|
+
message: `${key}: ${truncate(finding.value)}`,
|
|
4495
|
+
data: { key, value: finding.value, evidence: finding.evidence }
|
|
4496
|
+
});
|
|
4497
|
+
}
|
|
4498
|
+
if (findings.notes) {
|
|
4499
|
+
emitProject(ctx, project.id, {
|
|
4500
|
+
type: "prep.notes",
|
|
4501
|
+
message: `preparation notes: ${findings.notes}`,
|
|
4502
|
+
data: { notes: findings.notes }
|
|
4503
|
+
});
|
|
4504
|
+
}
|
|
4505
|
+
return applied;
|
|
4506
|
+
}
|
|
4507
|
+
function truncate(value, max = 120) {
|
|
4508
|
+
const oneLine = value.replace(/\s*\n\s*/g, " \xB7 ").trim();
|
|
4509
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}\u2026` : oneLine;
|
|
4510
|
+
}
|
|
4511
|
+
function latestPrep(ctx, projectId) {
|
|
4512
|
+
const row = ctx.db.select().from(projectPreps).where(eq6(projectPreps.projectId, projectId)).orderBy(desc3(projectPreps.startedAt)).get();
|
|
4513
|
+
if (!row)
|
|
4514
|
+
return null;
|
|
4515
|
+
return {
|
|
4516
|
+
id: row.id,
|
|
4517
|
+
projectId: row.projectId,
|
|
4518
|
+
status: row.status,
|
|
4519
|
+
startedAt: row.startedAt,
|
|
4520
|
+
...row.endedAt !== null ? { endedAt: row.endedAt } : {},
|
|
4521
|
+
...row.summary !== null ? { summary: row.summary } : {},
|
|
4522
|
+
...row.headSha !== null ? { headSha: row.headSha } : {}
|
|
4523
|
+
};
|
|
4524
|
+
}
|
|
4525
|
+
function reconcilePreps(ctx) {
|
|
4526
|
+
const stale = ctx.db.select({ id: projectPreps.id, projectId: projectPreps.projectId }).from(projectPreps).where(eq6(projectPreps.status, "running")).all().filter((row) => !controllers.has(row.projectId));
|
|
4527
|
+
for (const row of stale) {
|
|
4528
|
+
ctx.db.update(projectPreps).set({ status: "failed", endedAt: Date.now(), summary: "server stopped while preparing" }).where(eq6(projectPreps.id, row.id)).run();
|
|
4529
|
+
}
|
|
4530
|
+
return stale.length;
|
|
4531
|
+
}
|
|
4532
|
+
var controllers;
|
|
4533
|
+
var init_prep = __esm(() => {
|
|
4534
|
+
init_src();
|
|
4535
|
+
init_schema();
|
|
4536
|
+
init_errors();
|
|
4537
|
+
init_project_prep();
|
|
4538
|
+
init_events();
|
|
4539
|
+
init_findings();
|
|
4540
|
+
init_repo();
|
|
4541
|
+
controllers = new Map;
|
|
4542
|
+
});
|
|
4543
|
+
|
|
4544
|
+
// src/launcher/runtime.ts
|
|
4545
|
+
function setRuntimeCtx(ctx) {
|
|
4546
|
+
current = ctx;
|
|
4547
|
+
}
|
|
4548
|
+
async function getRuntimeCtx() {
|
|
4549
|
+
if (current)
|
|
4550
|
+
return current;
|
|
4551
|
+
const [{ createDb: createDb2 }, { dbPath: dbPath2 }, { loadConfig: loadConfig2 }] = await Promise.all([
|
|
4552
|
+
Promise.resolve().then(() => (init_client(), exports_client)),
|
|
4553
|
+
Promise.resolve().then(() => (init_paths(), exports_paths)),
|
|
4554
|
+
Promise.resolve().then(() => (init_config_load(), exports_config_load))
|
|
4555
|
+
]);
|
|
4556
|
+
const db = createDb2(dbPath2());
|
|
4557
|
+
current = { db, config: loadConfig2() };
|
|
4558
|
+
return current;
|
|
4559
|
+
}
|
|
4560
|
+
var current = null;
|
|
4561
|
+
|
|
4562
|
+
// src/services/feature-docs.ts
|
|
4563
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4564
|
+
import { join as join9 } from "path";
|
|
4565
|
+
function featureDocsDir(project, feature) {
|
|
4566
|
+
const relSegments = featureDocsRel(feature.slug).split("/");
|
|
4567
|
+
const worktree = worktreeDir(project.id, feature.slug);
|
|
4568
|
+
const base = existsSync9(worktree) ? worktree : project.repoPath;
|
|
4569
|
+
return join9(base, ...relSegments);
|
|
4570
|
+
}
|
|
4571
|
+
function featureDocPath(project, feature, fileName) {
|
|
4572
|
+
return join9(featureDocsDir(project, feature), fileName);
|
|
4573
|
+
}
|
|
4574
|
+
var init_feature_docs = __esm(() => {
|
|
4575
|
+
init_paths();
|
|
4576
|
+
});
|
|
4577
|
+
|
|
4578
|
+
// src/services/tickets.ts
|
|
4579
|
+
import { and as and6, asc as asc3, eq as eq7 } from "drizzle-orm";
|
|
4580
|
+
function rowToTicket(row) {
|
|
4581
|
+
return {
|
|
4582
|
+
id: row.id,
|
|
4583
|
+
featureId: row.featureId,
|
|
4584
|
+
seq: row.seq,
|
|
4585
|
+
title: row.title,
|
|
4586
|
+
goal: row.goal,
|
|
4587
|
+
context: row.context,
|
|
4588
|
+
acceptanceCriteria: row.acceptanceCriteria,
|
|
4589
|
+
seams: row.seams,
|
|
4590
|
+
blockedBy: row.blockedBy,
|
|
4591
|
+
status: row.status,
|
|
4592
|
+
commits: row.commits,
|
|
4593
|
+
error: row.error ?? undefined,
|
|
4594
|
+
attemptBranch: row.attemptBranch ?? undefined,
|
|
4595
|
+
conflictFiles: row.conflictFiles ?? undefined
|
|
4596
|
+
};
|
|
4597
|
+
}
|
|
4598
|
+
function resolveBlocking(inputs, startSeq) {
|
|
4599
|
+
try {
|
|
4600
|
+
return resolveBatchBlocking(inputs, { startSeq });
|
|
4601
|
+
} catch (e) {
|
|
4602
|
+
if (e instanceof BlockingEdgeError)
|
|
4603
|
+
throw new InvalidInputError(e.message);
|
|
4604
|
+
throw e;
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
function listByFeature2(ctx, featureId) {
|
|
4608
|
+
return ctx.db.select().from(tickets).where(eq7(tickets.featureId, featureId)).orderBy(asc3(tickets.seq)).all().map(rowToTicket);
|
|
4609
|
+
}
|
|
4610
|
+
function storeTickets(ctx, featureId, inputs) {
|
|
4611
|
+
if (inputs.length === 0)
|
|
4612
|
+
return [];
|
|
4613
|
+
const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq7(tickets.featureId, featureId)).all();
|
|
4614
|
+
const startSeq = existing.reduce((max, r) => Math.max(max, r.seq), 0) + 1;
|
|
4615
|
+
const resolved = resolveBlocking(inputs, startSeq);
|
|
4616
|
+
const rows = inputs.map((t, i) => ({
|
|
4617
|
+
id: newId("tkt"),
|
|
4618
|
+
featureId,
|
|
4619
|
+
seq: resolved[i].seq,
|
|
4620
|
+
title: t.title,
|
|
4621
|
+
goal: t.goal,
|
|
4622
|
+
context: t.context,
|
|
4623
|
+
acceptanceCriteria: t.acceptanceCriteria,
|
|
4624
|
+
seams: t.seams,
|
|
4625
|
+
blockedBy: resolved[i].blockedBy,
|
|
4626
|
+
status: "pending",
|
|
4627
|
+
commits: [],
|
|
4628
|
+
error: null,
|
|
4629
|
+
attemptBranch: null,
|
|
4630
|
+
conflictFiles: null
|
|
4631
|
+
}));
|
|
4632
|
+
ctx.db.insert(tickets).values(rows).run();
|
|
4633
|
+
emit(ctx, featureId, {
|
|
4634
|
+
type: "tickets.stored",
|
|
4635
|
+
message: `${rows.length} ticket(s) stored`,
|
|
4636
|
+
data: { count: rows.length, seqs: rows.map((r) => r.seq) }
|
|
4637
|
+
});
|
|
4638
|
+
return rows.map(rowToTicket);
|
|
4639
|
+
}
|
|
4640
|
+
function getTicket(ctx, id) {
|
|
4641
|
+
const row = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
4642
|
+
if (!row)
|
|
4643
|
+
throw new NotFoundError(`ticket ${id} not found`);
|
|
4644
|
+
return rowToTicket(row);
|
|
4645
|
+
}
|
|
4646
|
+
function assertMutable(t, verb) {
|
|
4647
|
+
if (!MUTABLE_STATUSES.has(t.status)) {
|
|
4648
|
+
throw new InvalidInputError(`cannot ${verb} ticket ${t.seq} \u2014 it is ${t.status}; only pending or failed tickets can be ${verb}${verb.endsWith("l") ? "led" : "ed"}`);
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
function editTicket(ctx, id, patch) {
|
|
4652
|
+
const current2 = getTicket(ctx, id);
|
|
4653
|
+
assertMutable(current2, "edit");
|
|
4654
|
+
const set = {};
|
|
4655
|
+
if (patch.title !== undefined)
|
|
4656
|
+
set.title = patch.title;
|
|
4657
|
+
if (patch.goal !== undefined)
|
|
4658
|
+
set.goal = patch.goal;
|
|
4659
|
+
if (patch.context !== undefined)
|
|
4660
|
+
set.context = patch.context;
|
|
4661
|
+
if (patch.acceptanceCriteria !== undefined)
|
|
4662
|
+
set.acceptanceCriteria = patch.acceptanceCriteria;
|
|
4663
|
+
if (patch.seams !== undefined)
|
|
4664
|
+
set.seams = patch.seams;
|
|
4665
|
+
if (Object.keys(set).length === 0) {
|
|
4666
|
+
throw new InvalidInputError("nothing to edit \u2014 pass at least one content field");
|
|
4667
|
+
}
|
|
4668
|
+
ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
|
|
4669
|
+
emit(ctx, current2.featureId, {
|
|
4670
|
+
type: "ticket.edited",
|
|
4671
|
+
message: `ticket ${current2.seq} content edited (${Object.keys(set).join(", ")})`,
|
|
4672
|
+
ticketId: id,
|
|
4673
|
+
data: { fields: Object.keys(set) }
|
|
4674
|
+
});
|
|
4675
|
+
return getTicket(ctx, id);
|
|
4676
|
+
}
|
|
4677
|
+
function cancelTicket(ctx, id, reason) {
|
|
4678
|
+
const current2 = getTicket(ctx, id);
|
|
4679
|
+
assertMutable(current2, "cancel");
|
|
4680
|
+
ctx.db.update(tickets).set({ status: "cancelled", error: reason?.trim() ? reason.trim() : null }).where(eq7(tickets.id, id)).run();
|
|
4681
|
+
emit(ctx, current2.featureId, {
|
|
4682
|
+
type: "ticket.cancelled",
|
|
4683
|
+
message: `ticket ${current2.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
|
|
4684
|
+
ticketId: id,
|
|
4685
|
+
data: { reason: reason ?? null }
|
|
4686
|
+
});
|
|
4687
|
+
return getTicket(ctx, id);
|
|
4688
|
+
}
|
|
4689
|
+
function sweepOrphanedBurning(ctx, featureId, reason) {
|
|
4690
|
+
const orphaned = ctx.db.select().from(tickets).where(and6(eq7(tickets.featureId, featureId), eq7(tickets.status, "burning"))).all().map(rowToTicket);
|
|
4691
|
+
for (const t of orphaned) {
|
|
4692
|
+
updateTicket(ctx, t.id, { status: "failed", error: reason });
|
|
4693
|
+
emit(ctx, featureId, {
|
|
4694
|
+
type: "ticket.failed",
|
|
4695
|
+
message: `ticket ${t.seq} failed: ${reason}`,
|
|
4696
|
+
ticketId: t.id,
|
|
4697
|
+
data: { error: reason, orphaned: true }
|
|
4698
|
+
});
|
|
4699
|
+
}
|
|
4700
|
+
return orphaned.map((t) => ({ ...t, status: "failed", error: reason }));
|
|
4701
|
+
}
|
|
4702
|
+
function updateTicket(ctx, id, patch) {
|
|
4703
|
+
const current2 = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
4704
|
+
if (!current2)
|
|
4705
|
+
throw new NotFoundError(`ticket ${id} not found`);
|
|
4706
|
+
const set = {};
|
|
4707
|
+
if (patch.status !== undefined)
|
|
4708
|
+
set.status = patch.status;
|
|
4709
|
+
if (patch.commits !== undefined)
|
|
4710
|
+
set.commits = patch.commits;
|
|
4711
|
+
if (patch.error !== undefined)
|
|
4712
|
+
set.error = patch.error;
|
|
4713
|
+
if (patch.attemptBranch !== undefined)
|
|
4714
|
+
set.attemptBranch = patch.attemptBranch;
|
|
4715
|
+
if (patch.conflictFiles !== undefined)
|
|
4716
|
+
set.conflictFiles = patch.conflictFiles;
|
|
4717
|
+
ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
|
|
4718
|
+
emit(ctx, current2.featureId, {
|
|
4719
|
+
type: "ticket.updated",
|
|
4720
|
+
message: `ticket ${current2.seq} \u2192 ${patch.status ?? current2.status}`,
|
|
4721
|
+
ticketId: id,
|
|
4722
|
+
data: patch
|
|
4723
|
+
});
|
|
4724
|
+
const updated = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
4725
|
+
return rowToTicket(updated);
|
|
4726
|
+
}
|
|
4727
|
+
var MUTABLE_STATUSES;
|
|
4728
|
+
var init_tickets = __esm(() => {
|
|
4729
|
+
init_src();
|
|
4730
|
+
init_schema();
|
|
4731
|
+
init_errors();
|
|
4732
|
+
init_events();
|
|
4733
|
+
MUTABLE_STATUSES = new Set(["pending", "failed"]);
|
|
4734
|
+
});
|
|
4735
|
+
|
|
4736
|
+
// src/services/gates.ts
|
|
4737
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4738
|
+
function checkGate(ctx, check, feature) {
|
|
4739
|
+
switch (check) {
|
|
4740
|
+
case "decisions-file-exists":
|
|
4741
|
+
return fileGate(ctx, feature, "decisions.md", "run the ideation session to capture decisions first");
|
|
4742
|
+
case "all-waypoints-terminal": {
|
|
4743
|
+
const wps = listByFeature(ctx, feature.id);
|
|
4744
|
+
if (wps.length === 0) {
|
|
4745
|
+
return { satisfied: false, reason: "no waypoints charted yet \u2014 chart the map before converging" };
|
|
4746
|
+
}
|
|
4747
|
+
const open = wps.filter((w) => w.status !== "resolved" && w.status !== "dropped");
|
|
4748
|
+
return open.length === 0 ? { satisfied: true } : {
|
|
4749
|
+
satisfied: false,
|
|
4750
|
+
reason: notYetTerminal("waypoint", open.map((w) => w.status), ["open", "claimed"])
|
|
4751
|
+
};
|
|
4752
|
+
}
|
|
4753
|
+
case "spec-file-exists":
|
|
4754
|
+
return fileGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
|
|
4755
|
+
case "tickets-approved": {
|
|
4756
|
+
const count = listByFeature2(ctx, feature.id).filter((t) => t.status !== "cancelled").length;
|
|
4757
|
+
return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
|
|
4758
|
+
}
|
|
4759
|
+
case "all-tickets-terminal": {
|
|
4760
|
+
const tickets2 = listByFeature2(ctx, feature.id);
|
|
4761
|
+
if (tickets2.length === 0)
|
|
4762
|
+
return { satisfied: false, reason: "no tickets have run yet" };
|
|
4763
|
+
const open = tickets2.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
4764
|
+
return open.length === 0 ? { satisfied: true } : {
|
|
4765
|
+
satisfied: false,
|
|
4766
|
+
reason: notYetTerminal("ticket", open.map((t) => t.status), ["pending", "burning"])
|
|
4767
|
+
};
|
|
4768
|
+
}
|
|
4769
|
+
case "human-merge":
|
|
4770
|
+
return { satisfied: false, reason: "use the Merge button to ship" };
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
function notYetTerminal(noun, statuses, order) {
|
|
4774
|
+
const counts = new Map;
|
|
4775
|
+
for (const s of statuses)
|
|
4776
|
+
counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
4777
|
+
const ordered = [
|
|
4778
|
+
...order.filter((s) => counts.has(s)),
|
|
4779
|
+
...[...counts.keys()].filter((s) => !order.includes(s))
|
|
4780
|
+
];
|
|
4781
|
+
const breakdown = ordered.map((s) => `${counts.get(s)} ${s}`).join(", ");
|
|
4782
|
+
return `${statuses.length} ${statuses.length === 1 ? noun : `${noun}s`} not yet terminal (${breakdown})`;
|
|
4783
|
+
}
|
|
4784
|
+
function fileGate(ctx, feature, fileName, reason) {
|
|
4785
|
+
const project = projectForFeature(ctx, feature);
|
|
4786
|
+
return existsSync10(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
|
|
4787
|
+
}
|
|
4788
|
+
function overrideGate(ctx, featureId, gate, reason) {
|
|
4789
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
4790
|
+
ctx.db.insert(gateOverrides).values({ featureId, gate, reason, ts: Date.now() }).run();
|
|
4791
|
+
emit(ctx, featureId, {
|
|
4792
|
+
type: "gate.overridden",
|
|
4793
|
+
message: `gate ${gate} overridden: ${reason}`,
|
|
4794
|
+
data: { gate, reason }
|
|
4795
|
+
});
|
|
4796
|
+
const next = nextPhase(feature);
|
|
4797
|
+
if (next)
|
|
4798
|
+
return setPhase(ctx, featureId, next, "phase.advanced", `advanced to ${next} (override)`);
|
|
4799
|
+
return feature;
|
|
4800
|
+
}
|
|
4801
|
+
var init_gates = __esm(() => {
|
|
4802
|
+
init_src();
|
|
4803
|
+
init_schema();
|
|
4804
|
+
init_events();
|
|
4805
|
+
init_feature_docs();
|
|
4806
|
+
init_repo();
|
|
4807
|
+
init_tickets();
|
|
4808
|
+
init_waypoints();
|
|
4809
|
+
});
|
|
4810
|
+
|
|
4811
|
+
// src/services/knowledge.ts
|
|
4812
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, writeFileSync } from "fs";
|
|
4813
|
+
import { join as join10, relative, resolve as resolve5, sep } from "path";
|
|
4814
|
+
function scaffoldDocs(ctx, feature) {
|
|
4815
|
+
const project = projectForFeature(ctx, feature);
|
|
4816
|
+
const dir = featureDocsDir(project, feature);
|
|
4817
|
+
mkdirSync5(dir, { recursive: true });
|
|
4818
|
+
const briefPath = featureDocPath(project, feature, "brief.md");
|
|
4819
|
+
if (!existsSync11(briefPath)) {
|
|
4820
|
+
const created = new Date().toISOString();
|
|
4821
|
+
const brief = [
|
|
4822
|
+
`# ${feature.title}`,
|
|
4823
|
+
"",
|
|
4824
|
+
feature.oneLiner,
|
|
4825
|
+
"",
|
|
4826
|
+
`- Slug: ${feature.slug}`,
|
|
4827
|
+
`- Created: ${created}`,
|
|
4828
|
+
""
|
|
4829
|
+
].join(`
|
|
4830
|
+
`);
|
|
4831
|
+
writeFileSync(briefPath, brief, "utf8");
|
|
4832
|
+
}
|
|
4833
|
+
if (feature.mapped) {
|
|
4834
|
+
scaffoldMapDoc(project, feature);
|
|
4835
|
+
}
|
|
4836
|
+
emit(ctx, feature.id, {
|
|
4837
|
+
type: "docs.scaffolded",
|
|
4838
|
+
message: feature.mapped ? `scaffolded docs/features/${feature.slug}/{brief,map}.md` : `scaffolded docs/features/${feature.slug}/brief.md`
|
|
4839
|
+
});
|
|
4840
|
+
}
|
|
4841
|
+
function scaffoldMapDoc(project, feature, seed) {
|
|
4842
|
+
mkdirSync5(featureDocsDir(project, feature), { recursive: true });
|
|
4843
|
+
const mapPath = featureDocPath(project, feature, "map.md");
|
|
4844
|
+
if (existsSync11(mapPath))
|
|
4845
|
+
return;
|
|
4846
|
+
writeFileSync(mapPath, mapDocBody(feature, seed), "utf8");
|
|
4847
|
+
}
|
|
4848
|
+
function mapDocBody(feature, seed) {
|
|
4849
|
+
const seeded = {
|
|
4850
|
+
Destination: seed?.destination?.trim(),
|
|
4851
|
+
Notes: seed?.notes?.trim()
|
|
4060
4852
|
};
|
|
4853
|
+
const lines = [`# ${feature.title} \u2014 map`, ""];
|
|
4854
|
+
for (const section of MAP_SECTIONS) {
|
|
4855
|
+
lines.push(`## ${section}`, "");
|
|
4856
|
+
const body = seeded[section];
|
|
4857
|
+
if (body)
|
|
4858
|
+
lines.push(body, "");
|
|
4859
|
+
}
|
|
4860
|
+
return lines.join(`
|
|
4861
|
+
`);
|
|
4061
4862
|
}
|
|
4062
|
-
function
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4863
|
+
function listDocs(ctx, feature) {
|
|
4864
|
+
const project = projectForFeature(ctx, feature);
|
|
4865
|
+
const dir = featureDocsDir(project, feature);
|
|
4866
|
+
if (!existsSync11(dir))
|
|
4867
|
+
return [];
|
|
4868
|
+
return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join10(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
4869
|
+
}
|
|
4870
|
+
function readDoc(ctx, feature, relPath) {
|
|
4871
|
+
const project = projectForFeature(ctx, feature);
|
|
4872
|
+
const dir = featureDocsDir(project, feature);
|
|
4873
|
+
const root = resolve5(dir);
|
|
4874
|
+
const target = resolve5(dir, relPath);
|
|
4875
|
+
const rel = relative(root, target);
|
|
4876
|
+
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
4877
|
+
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
4070
4878
|
}
|
|
4071
|
-
|
|
4072
|
-
|
|
4879
|
+
if (!existsSync11(target))
|
|
4880
|
+
throw new NotFoundError(`doc not found: ${relPath}`);
|
|
4881
|
+
return { content: readFileSync5(target, "utf8") };
|
|
4073
4882
|
}
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
["install", /\b(pnpm|npm|yarn|bun|corepack)\s+\w*\s*install\b|\bnpm ci\b/],
|
|
4096
|
-
["git", /\bgit\b/],
|
|
4097
|
-
["search", /\b(grep|rg|ag|find)\b/],
|
|
4098
|
-
["file-read", /\b(cat|sed|head|tail|less|wc|ls)\b/],
|
|
4099
|
-
["file-edit", /<<\s*['"]?(PY|EOF|SH|JS|TS)\b|\bpython3?\s+-\s*<</]
|
|
4100
|
-
];
|
|
4101
|
-
TOOL_NAME_CATEGORY = {
|
|
4102
|
-
Read: "file-read",
|
|
4103
|
-
NotebookRead: "file-read",
|
|
4104
|
-
Grep: "search",
|
|
4105
|
-
Glob: "search",
|
|
4106
|
-
Edit: "file-edit",
|
|
4107
|
-
Write: "file-edit",
|
|
4108
|
-
NotebookEdit: "file-edit"
|
|
4109
|
-
};
|
|
4110
|
-
MAX_ATTRIBUTABLE_GAP_MS = 20 * 60000;
|
|
4111
|
-
BURN_WORKTREE_PATH = /[\\/]\.sandcastle[\\/]worktrees[\\/]/i;
|
|
4112
|
-
WORKTREE_REMOVAL_FAILURES = [
|
|
4113
|
-
/failed to delete/i,
|
|
4114
|
-
/directory not empty|enotempty/i,
|
|
4115
|
-
/unable to (unlink|delete)/i,
|
|
4116
|
-
/is not a working tree/i,
|
|
4117
|
-
/resource busy|ebusy|eperm|eacces/i
|
|
4118
|
-
];
|
|
4119
|
-
FATAL_ERROR_PATTERNS = [
|
|
4120
|
-
/invalid (api key|x-api-key)/i,
|
|
4121
|
-
/authentication|unauthorized|permission denied/i,
|
|
4122
|
-
/credit balance|billing/i,
|
|
4123
|
-
/oauth token|setup-token/i,
|
|
4124
|
-
/issue with the selected model|model not found|unknown model/i,
|
|
4125
|
-
/does not support resumeSession|resumeSession .* not found/i
|
|
4126
|
-
];
|
|
4127
|
-
RETRYABLE_ERROR_PATTERNS = [
|
|
4128
|
-
/exited with code \d+/i,
|
|
4129
|
-
/idle timeout|AgentIdleTimeout/i,
|
|
4130
|
-
/connection (closed|error|refused|reset)|socket hang up|fetch failed/i,
|
|
4131
|
-
/econnreset|etimedout|econnrefused|epipe|eai_again/i,
|
|
4132
|
-
/overloaded|rate.?limit|too many requests/i,
|
|
4133
|
-
/internal server error|service unavailable|bad gateway|gateway timeout/i,
|
|
4134
|
-
/\bapi error\b/i,
|
|
4135
|
-
/session capture failed/i
|
|
4883
|
+
function readTitle(path, fallback) {
|
|
4884
|
+
try {
|
|
4885
|
+
const firstLine = readFileSync5(path, "utf8").split(`
|
|
4886
|
+
`, 1)[0] ?? "";
|
|
4887
|
+
const heading = firstLine.match(/^#\s+(.+)$/);
|
|
4888
|
+
return heading ? heading[1].trim() : fallback;
|
|
4889
|
+
} catch {
|
|
4890
|
+
return fallback;
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4893
|
+
var MAP_SECTIONS;
|
|
4894
|
+
var init_knowledge = __esm(() => {
|
|
4895
|
+
init_errors();
|
|
4896
|
+
init_events();
|
|
4897
|
+
init_feature_docs();
|
|
4898
|
+
init_repo();
|
|
4899
|
+
MAP_SECTIONS = [
|
|
4900
|
+
"Destination",
|
|
4901
|
+
"Notes",
|
|
4902
|
+
"Not yet specified",
|
|
4903
|
+
"Out of scope"
|
|
4136
4904
|
];
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4905
|
+
});
|
|
4906
|
+
|
|
4907
|
+
// src/pty/end-session.ts
|
|
4908
|
+
function endSession(ctx, sessionId) {
|
|
4909
|
+
const registry = ptyRegistry();
|
|
4910
|
+
const killed = registry.kill(sessionId);
|
|
4911
|
+
registry.remove(sessionId);
|
|
4912
|
+
const session = markSessionEnded(ctx, sessionId);
|
|
4913
|
+
if (session) {
|
|
4914
|
+
releaseForSession(ctx, sessionId);
|
|
4915
|
+
emit(ctx, session.featureId, {
|
|
4916
|
+
type: "session.ended",
|
|
4917
|
+
message: "session ended by user",
|
|
4918
|
+
data: { sessionId, killed }
|
|
4919
|
+
});
|
|
4920
|
+
} else {
|
|
4921
|
+
const existing = getSessionRow(ctx, sessionId);
|
|
4922
|
+
if (!existing)
|
|
4923
|
+
return { sessionId, ok: false, killed };
|
|
4924
|
+
}
|
|
4925
|
+
return { sessionId, ok: true, killed };
|
|
4926
|
+
}
|
|
4927
|
+
var init_end_session = __esm(() => {
|
|
4928
|
+
init_sessions();
|
|
4929
|
+
init_events();
|
|
4930
|
+
init_waypoints();
|
|
4931
|
+
init_registry();
|
|
4143
4932
|
});
|
|
4144
4933
|
|
|
4145
4934
|
// src/workflows/research.ts
|
|
4146
|
-
import { existsSync as
|
|
4147
|
-
import { dirname as
|
|
4148
|
-
import { fileURLToPath as
|
|
4149
|
-
import { run as
|
|
4935
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readdirSync as readdirSync4 } from "fs";
|
|
4936
|
+
import { dirname as dirname5, join as join11 } from "path";
|
|
4937
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
4938
|
+
import { run as run3 } from "@ai-hero/sandcastle";
|
|
4150
4939
|
import { docker as docker2 } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
4151
4940
|
import { noSandbox as noSandbox2 } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
|
|
4152
4941
|
function waypointSlug(waypoint) {
|
|
@@ -4163,7 +4952,7 @@ async function researchRun(ctx, deps) {
|
|
|
4163
4952
|
return { status: "failed", summary: "no waypoint to research" };
|
|
4164
4953
|
}
|
|
4165
4954
|
if (deps.config.sandbox === "docker" && !deps.hasAuthToken) {
|
|
4166
|
-
ctx.emitEvent({ type:
|
|
4955
|
+
ctx.emitEvent({ type: AUTH_MISSING_EVENT3, message: AUTH_MISSING_MESSAGE3 });
|
|
4167
4956
|
return { status: "failed", summary: "research aborted: auth token missing" };
|
|
4168
4957
|
}
|
|
4169
4958
|
ctx.emitEvent({
|
|
@@ -4184,12 +4973,12 @@ async function researchRun(ctx, deps) {
|
|
|
4184
4973
|
}
|
|
4185
4974
|
ctx.emitEvent({
|
|
4186
4975
|
type: "research.failed",
|
|
4187
|
-
message: `waypoint ${waypoint.seq} research failed: ${
|
|
4976
|
+
message: `waypoint ${waypoint.seq} research failed: ${errorHeadline3(outcome.error)}`,
|
|
4188
4977
|
data: { waypointId: waypoint.id, error: outcome.error }
|
|
4189
4978
|
});
|
|
4190
4979
|
return { status: "failed", summary: outcome.error };
|
|
4191
4980
|
}
|
|
4192
|
-
function
|
|
4981
|
+
function errorHeadline3(s) {
|
|
4193
4982
|
const lines = s.split(`
|
|
4194
4983
|
`).map((l) => l.trim()).filter(Boolean);
|
|
4195
4984
|
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
@@ -4200,7 +4989,7 @@ function createResearchStreamThrottle(emit2, opts = {}) {
|
|
|
4200
4989
|
}
|
|
4201
4990
|
function renderResearchPrompt(template, values) {
|
|
4202
4991
|
let out = template;
|
|
4203
|
-
for (const key of
|
|
4992
|
+
for (const key of PLACEHOLDERS2) {
|
|
4204
4993
|
out = out.split(`{{${key}}}`).join(values[key]);
|
|
4205
4994
|
}
|
|
4206
4995
|
return out;
|
|
@@ -4215,32 +5004,32 @@ function interpretResearchResult(result, docRelPath) {
|
|
|
4215
5004
|
return { status: "failed", error: "agent produced no commits \u2014 no research summary landed" };
|
|
4216
5005
|
}
|
|
4217
5006
|
function researchTemplatePath() {
|
|
4218
|
-
const here =
|
|
4219
|
-
return
|
|
5007
|
+
const here = dirname5(fileURLToPath4(import.meta.url));
|
|
5008
|
+
return join11(resolveSkillsRoot(here), "burner", "research-waypoint.md");
|
|
4220
5009
|
}
|
|
4221
5010
|
function readDocsDigestFromDisk2(projectId, slug) {
|
|
4222
5011
|
const worktree = worktreeDir(projectId, slug);
|
|
4223
|
-
if (!
|
|
5012
|
+
if (!existsSync12(worktree))
|
|
4224
5013
|
return "_No talk worktree on disk \u2014 docs digest skipped._";
|
|
4225
|
-
const docsDir =
|
|
4226
|
-
if (!
|
|
5014
|
+
const docsDir = join11(worktree, ...featureDocsRel(slug).split("/"));
|
|
5015
|
+
if (!existsSync12(docsDir))
|
|
4227
5016
|
return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
|
|
4228
|
-
const files = readdirSync4(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content:
|
|
5017
|
+
const files = readdirSync4(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync6(join11(docsDir, e.name), "utf8") }));
|
|
4229
5018
|
return buildDocsDigest(files);
|
|
4230
5019
|
}
|
|
4231
5020
|
async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
4232
5021
|
const { project, feature } = ctx;
|
|
4233
5022
|
const docRelPath = researchDocRel(feature.slug, waypoint);
|
|
4234
5023
|
const tempBranch = researchBranchName(feature.slug, waypoint.seq, newId("b").slice(2, 10));
|
|
4235
|
-
const template =
|
|
5024
|
+
const template = readFileSync6(researchTemplatePath(), "utf8");
|
|
4236
5025
|
const prompt = renderResearchPrompt(template, {
|
|
4237
5026
|
WAYPOINT_JSON: buildWaypointJson(waypoint),
|
|
4238
5027
|
FEATURE_BRIEF: buildFeatureBrief(feature),
|
|
4239
5028
|
DOCS_DIGEST: readDocsDigestFromDisk2(project.id, feature.slug),
|
|
4240
5029
|
RESEARCH_DOC_PATH: docRelPath
|
|
4241
5030
|
});
|
|
4242
|
-
|
|
4243
|
-
const logFilePath =
|
|
5031
|
+
mkdirSync6(logsDir(), { recursive: true });
|
|
5032
|
+
const logFilePath = join11(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
|
|
4244
5033
|
const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
|
|
4245
5034
|
const runOptions = {
|
|
4246
5035
|
agent: buildBurnAgent(config2, token, model),
|
|
@@ -4255,7 +5044,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
4255
5044
|
let result;
|
|
4256
5045
|
let salvaged = [];
|
|
4257
5046
|
try {
|
|
4258
|
-
result = await
|
|
5047
|
+
result = await run3(runOptions);
|
|
4259
5048
|
} catch (err) {
|
|
4260
5049
|
throttle.flush();
|
|
4261
5050
|
if (ctx.signal.aborted)
|
|
@@ -4267,7 +5056,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
4267
5056
|
const removed = await cleanupBurnWorktree(project.repoPath, tempBranch);
|
|
4268
5057
|
ctx.emitEvent({
|
|
4269
5058
|
type: "burn.worktree.teardown-failed",
|
|
4270
|
-
message: `waypoint ${waypoint.seq}: agent finished but sandcastle could not remove its worktree (${
|
|
5059
|
+
message: `waypoint ${waypoint.seq}: agent finished but sandcastle could not remove its worktree (${errorHeadline3(msg)})${removed ? " \u2014 cleaned up" : " \u2014 left on disk"}; landing the ${salvaged.length} commit(s) anyway`,
|
|
4271
5060
|
data: { tempBranch, error: msg, cleanedUp: removed }
|
|
4272
5061
|
});
|
|
4273
5062
|
}
|
|
@@ -4280,7 +5069,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
4280
5069
|
const detail = merge.error ?? "merge failed";
|
|
4281
5070
|
ctx.emitEvent({
|
|
4282
5071
|
type: merge.conflict ? "merge.conflict.needs-human" : "research.merge_failed",
|
|
4283
|
-
message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${
|
|
5072
|
+
message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${errorHeadline3(detail)} \u2014 the branch is preserved for manual recovery`,
|
|
4284
5073
|
data: { tempBranch, featureBranch: feature.branch, conflict: merge.conflict ?? false, error: detail }
|
|
4285
5074
|
});
|
|
4286
5075
|
return {
|
|
@@ -4302,9 +5091,9 @@ function resolveResearchDeps(ctx) {
|
|
|
4302
5091
|
}
|
|
4303
5092
|
function readTokenFromEnvFile2(path) {
|
|
4304
5093
|
let fromFile;
|
|
4305
|
-
if (
|
|
5094
|
+
if (existsSync12(path)) {
|
|
4306
5095
|
try {
|
|
4307
|
-
fromFile = parseEnvFile(
|
|
5096
|
+
fromFile = parseEnvFile(readFileSync6(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
|
|
4308
5097
|
} catch {
|
|
4309
5098
|
fromFile = undefined;
|
|
4310
5099
|
}
|
|
@@ -4312,7 +5101,7 @@ function readTokenFromEnvFile2(path) {
|
|
|
4312
5101
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
4313
5102
|
return token && token.length > 0 ? token : undefined;
|
|
4314
5103
|
}
|
|
4315
|
-
var
|
|
5104
|
+
var AUTH_MISSING_EVENT3 = "auth.missing", AUTH_MISSING_MESSAGE3 = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PLACEHOLDERS2, research;
|
|
4316
5105
|
var init_research = __esm(() => {
|
|
4317
5106
|
init_src();
|
|
4318
5107
|
init_config_load();
|
|
@@ -4320,7 +5109,7 @@ var init_research = __esm(() => {
|
|
|
4320
5109
|
init_skills_root();
|
|
4321
5110
|
init_git();
|
|
4322
5111
|
init_ticket_burner();
|
|
4323
|
-
|
|
5112
|
+
PLACEHOLDERS2 = ["WAYPOINT_JSON", "FEATURE_BRIEF", "DOCS_DIGEST", "RESEARCH_DOC_PATH"];
|
|
4324
5113
|
research = {
|
|
4325
5114
|
id: "research",
|
|
4326
5115
|
run: (ctx) => researchRun(ctx, resolveResearchDeps(ctx))
|
|
@@ -4342,12 +5131,12 @@ var init_registry2 = __esm(() => {
|
|
|
4342
5131
|
});
|
|
4343
5132
|
|
|
4344
5133
|
// src/workflows/runner.ts
|
|
4345
|
-
import { eq as
|
|
5134
|
+
import { eq as eq8 } from "drizzle-orm";
|
|
4346
5135
|
function workflowClaimsFeatureBranch(workflowId) {
|
|
4347
5136
|
return BRANCH_CLAIMING.has(workflowId);
|
|
4348
5137
|
}
|
|
4349
5138
|
function isRunActive(runId) {
|
|
4350
|
-
return
|
|
5139
|
+
return controllers2.has(runId);
|
|
4351
5140
|
}
|
|
4352
5141
|
async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
4353
5142
|
const feature = getFeatureRow(ctx, featureId);
|
|
@@ -4371,7 +5160,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
4371
5160
|
claim(ctx, opts.claimWaypointId, runId);
|
|
4372
5161
|
} catch (e) {
|
|
4373
5162
|
const summary = e instanceof Error ? e.message : "claim failed";
|
|
4374
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(
|
|
5163
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(eq8(runs.id, runId)).run();
|
|
4375
5164
|
throw e;
|
|
4376
5165
|
}
|
|
4377
5166
|
}
|
|
@@ -4382,7 +5171,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
4382
5171
|
data: { workflow: workflowId }
|
|
4383
5172
|
});
|
|
4384
5173
|
const controller = new AbortController;
|
|
4385
|
-
|
|
5174
|
+
controllers2.set(runId, controller);
|
|
4386
5175
|
const talkWorktree = worktreeDir(project.id, feature.slug);
|
|
4387
5176
|
const talkDetached = workflowClaimsFeatureBranch(workflowId) ? await detachWorktree(talkWorktree) : false;
|
|
4388
5177
|
const wctx = {
|
|
@@ -4415,7 +5204,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
4415
5204
|
return { runId, done };
|
|
4416
5205
|
}
|
|
4417
5206
|
function cancelRun(runId) {
|
|
4418
|
-
|
|
5207
|
+
controllers2.get(runId)?.abort();
|
|
4419
5208
|
}
|
|
4420
5209
|
async function executeRun(ctx, runId, featureId, workflow2, runPromise, controller, cleanup) {
|
|
4421
5210
|
let status = "failed";
|
|
@@ -4434,9 +5223,9 @@ async function executeRun(ctx, runId, featureId, workflow2, runPromise, controll
|
|
|
4434
5223
|
}
|
|
4435
5224
|
emit(ctx, featureId, { type: "run.error", message: summary, runId });
|
|
4436
5225
|
} finally {
|
|
4437
|
-
|
|
5226
|
+
controllers2.delete(runId);
|
|
4438
5227
|
}
|
|
4439
|
-
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary }).where(
|
|
5228
|
+
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary }).where(eq8(runs.id, runId)).run();
|
|
4440
5229
|
releaseForSession(ctx, runId);
|
|
4441
5230
|
if (workflowClaimsFeatureBranch(workflow2)) {
|
|
4442
5231
|
sweepOrphanedBurning(ctx, featureId, `orphaned \u2014 the run ended (${status}) while it was burning`);
|
|
@@ -4466,7 +5255,7 @@ function maybeAutoAdvance(ctx, featureId) {
|
|
|
4466
5255
|
if (next)
|
|
4467
5256
|
setPhase(ctx, featureId, next, "phase.advanced", "auto-advanced to review after successful run");
|
|
4468
5257
|
}
|
|
4469
|
-
var
|
|
5258
|
+
var controllers2, BRANCH_CLAIMING;
|
|
4470
5259
|
var init_runner = __esm(() => {
|
|
4471
5260
|
init_src();
|
|
4472
5261
|
init_paths();
|
|
@@ -4479,12 +5268,12 @@ var init_runner = __esm(() => {
|
|
|
4479
5268
|
init_tickets();
|
|
4480
5269
|
init_waypoints();
|
|
4481
5270
|
init_registry2();
|
|
4482
|
-
|
|
5271
|
+
controllers2 = new Map;
|
|
4483
5272
|
BRANCH_CLAIMING = new Set(["ticket-burner"]);
|
|
4484
5273
|
});
|
|
4485
5274
|
|
|
4486
5275
|
// src/services/features.ts
|
|
4487
|
-
import { desc as
|
|
5276
|
+
import { desc as desc4, eq as eq9 } from "drizzle-orm";
|
|
4488
5277
|
import { rmSync as rmSync2 } from "fs";
|
|
4489
5278
|
async function createFeature(ctx, input) {
|
|
4490
5279
|
const project = requireProjectById(ctx, input.projectId);
|
|
@@ -4547,7 +5336,7 @@ function getFeatureFull(ctx, id) {
|
|
|
4547
5336
|
};
|
|
4548
5337
|
}
|
|
4549
5338
|
function list(ctx, projectId) {
|
|
4550
|
-
const rows = ctx.db.select().from(features).where(
|
|
5339
|
+
const rows = ctx.db.select().from(features).where(eq9(features.projectId, projectId)).orderBy(desc4(features.createdAt)).all();
|
|
4551
5340
|
return rows.map((row) => {
|
|
4552
5341
|
const feature = rowToFeature(row);
|
|
4553
5342
|
const tickets2 = listByFeature2(ctx, feature.id);
|
|
@@ -4692,7 +5481,7 @@ function escalateToMap(ctx, featureId, input) {
|
|
|
4692
5481
|
return { ok: true, warning: `feature ${feature.slug} is already mapped \u2014 no changes made` };
|
|
4693
5482
|
}
|
|
4694
5483
|
const project = projectForFeature(ctx, feature);
|
|
4695
|
-
ctx.db.update(features).set({ mapped: true }).where(
|
|
5484
|
+
ctx.db.update(features).set({ mapped: true }).where(eq9(features.id, featureId)).run();
|
|
4696
5485
|
scaffoldMapDoc(project, { ...feature, mapped: true }, input);
|
|
4697
5486
|
emit(ctx, featureId, {
|
|
4698
5487
|
type: "feature.escalated",
|
|
@@ -4709,7 +5498,7 @@ function archiveFeature(ctx, featureId) {
|
|
|
4709
5498
|
const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
|
|
4710
5499
|
if (live)
|
|
4711
5500
|
endSession(ctx, live.id);
|
|
4712
|
-
ctx.db.update(features).set({ status: "archived" }).where(
|
|
5501
|
+
ctx.db.update(features).set({ status: "archived" }).where(eq9(features.id, featureId)).run();
|
|
4713
5502
|
emit(ctx, featureId, {
|
|
4714
5503
|
type: "feature.archived",
|
|
4715
5504
|
message: `feature ${feature.slug} archived`,
|
|
@@ -4723,7 +5512,7 @@ function unarchiveFeature(ctx, featureId) {
|
|
|
4723
5512
|
throw new GateError(`feature ${feature.slug} is not archived`);
|
|
4724
5513
|
}
|
|
4725
5514
|
const restored = feature.phase === "shipped" ? "shipped" : "active";
|
|
4726
|
-
ctx.db.update(features).set({ status: restored }).where(
|
|
5515
|
+
ctx.db.update(features).set({ status: restored }).where(eq9(features.id, featureId)).run();
|
|
4727
5516
|
emit(ctx, featureId, {
|
|
4728
5517
|
type: "feature.unarchived",
|
|
4729
5518
|
message: `feature ${feature.slug} unarchived (${restored})`,
|
|
@@ -4737,9 +5526,9 @@ async function deleteFeature(ctx, featureId) {
|
|
|
4737
5526
|
throw new GateError(`feature ${feature.slug} is shipped \u2014 archive it instead (delete is for non-shipped features)`);
|
|
4738
5527
|
}
|
|
4739
5528
|
const project = projectForFeature(ctx, feature);
|
|
4740
|
-
for (const
|
|
4741
|
-
if (
|
|
4742
|
-
cancelRun(
|
|
5529
|
+
for (const run4 of listRunsByFeature(ctx, featureId)) {
|
|
5530
|
+
if (run4.status === "running")
|
|
5531
|
+
cancelRun(run4.id);
|
|
4743
5532
|
}
|
|
4744
5533
|
const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
|
|
4745
5534
|
if (live)
|
|
@@ -4763,13 +5552,13 @@ async function deleteFeature(ctx, featureId) {
|
|
|
4763
5552
|
return { ok: true, slug: feature.slug };
|
|
4764
5553
|
}
|
|
4765
5554
|
function deleteFeatureRows(ctx, featureId) {
|
|
4766
|
-
ctx.db.delete(tickets).where(
|
|
4767
|
-
ctx.db.delete(sessions).where(
|
|
4768
|
-
ctx.db.delete(runs).where(
|
|
4769
|
-
ctx.db.delete(events).where(
|
|
4770
|
-
ctx.db.delete(gateOverrides).where(
|
|
4771
|
-
ctx.db.delete(waypoints).where(
|
|
4772
|
-
ctx.db.delete(features).where(
|
|
5555
|
+
ctx.db.delete(tickets).where(eq9(tickets.featureId, featureId)).run();
|
|
5556
|
+
ctx.db.delete(sessions).where(eq9(sessions.featureId, featureId)).run();
|
|
5557
|
+
ctx.db.delete(runs).where(eq9(runs.featureId, featureId)).run();
|
|
5558
|
+
ctx.db.delete(events).where(eq9(events.featureId, featureId)).run();
|
|
5559
|
+
ctx.db.delete(gateOverrides).where(eq9(gateOverrides.featureId, featureId)).run();
|
|
5560
|
+
ctx.db.delete(waypoints).where(eq9(waypoints.featureId, featureId)).run();
|
|
5561
|
+
ctx.db.delete(features).where(eq9(features.id, featureId)).run();
|
|
4773
5562
|
}
|
|
4774
5563
|
function gateState(ctx, feature) {
|
|
4775
5564
|
const gate = nextGate(feature);
|
|
@@ -4780,7 +5569,7 @@ function gateState(ctx, feature) {
|
|
|
4780
5569
|
}
|
|
4781
5570
|
function uniqueSlug(ctx, projectId, title) {
|
|
4782
5571
|
const base = slugify(title);
|
|
4783
|
-
const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(
|
|
5572
|
+
const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(eq9(features.projectId, projectId)).all().map((r) => r.slug));
|
|
4784
5573
|
if (!existing.has(base))
|
|
4785
5574
|
return base;
|
|
4786
5575
|
let n = 2;
|
|
@@ -4812,7 +5601,7 @@ var init_features = __esm(() => {
|
|
|
4812
5601
|
import { StreamableHTTPTransport } from "@hono/mcp";
|
|
4813
5602
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4814
5603
|
import { Hono } from "hono";
|
|
4815
|
-
import * as
|
|
5604
|
+
import * as z4 from "zod";
|
|
4816
5605
|
function headerSessionId(extra) {
|
|
4817
5606
|
const headers = extra.requestInfo?.headers;
|
|
4818
5607
|
if (!headers)
|
|
@@ -4966,7 +5755,7 @@ function buildMcpServer() {
|
|
|
4966
5755
|
server.registerTool("emit_tickets", {
|
|
4967
5756
|
title: "Emit tickets",
|
|
4968
5757
|
description: "Store the ideation session's ticket batch. Each ticket: title, goal, context, acceptanceCriteria[], seams[], blockedBy[] (1-based positions within THIS batch).",
|
|
4969
|
-
inputSchema: { tickets:
|
|
5758
|
+
inputSchema: { tickets: z4.array(TicketInput) }
|
|
4970
5759
|
}, async (args, extra) => {
|
|
4971
5760
|
const rs = await resolveCtxSession(extra);
|
|
4972
5761
|
if (!rs)
|
|
@@ -4979,12 +5768,12 @@ function buildMcpServer() {
|
|
|
4979
5768
|
title: "Update ticket",
|
|
4980
5769
|
description: "Rewrite a stored ticket's content \u2014 title, goal, context, acceptanceCriteria, seams (any subset). Only pending or failed tickets can be edited; done/cancelled tickets are history and burning tickets are already running. Use during a revisit when a decision change makes a ticket stale. Get ids from get_feature_context.",
|
|
4981
5770
|
inputSchema: {
|
|
4982
|
-
id:
|
|
4983
|
-
title:
|
|
4984
|
-
goal:
|
|
4985
|
-
context:
|
|
4986
|
-
acceptanceCriteria:
|
|
4987
|
-
seams:
|
|
5771
|
+
id: z4.string(),
|
|
5772
|
+
title: z4.string().optional(),
|
|
5773
|
+
goal: z4.string().optional(),
|
|
5774
|
+
context: z4.string().optional(),
|
|
5775
|
+
acceptanceCriteria: z4.array(z4.string()).optional(),
|
|
5776
|
+
seams: z4.array(z4.string()).optional()
|
|
4988
5777
|
}
|
|
4989
5778
|
}, async (args, extra) => {
|
|
4990
5779
|
const rs = await resolveCtxSession(extra);
|
|
@@ -4997,7 +5786,7 @@ function buildMcpServer() {
|
|
|
4997
5786
|
server.registerTool("cancel_ticket", {
|
|
4998
5787
|
title: "Cancel ticket",
|
|
4999
5788
|
description: "Cancel a ticket that is no longer needed (terminal state; only pending or failed tickets). The burner skips cancelled tickets, and tickets blocked by a cancelled one still burn \u2014 cancellation counts as satisfied. Pass a short reason so the timeline explains why.",
|
|
5000
|
-
inputSchema: { id:
|
|
5789
|
+
inputSchema: { id: z4.string(), reason: z4.string().optional() }
|
|
5001
5790
|
}, async (args, extra) => {
|
|
5002
5791
|
const rs = await resolveCtxSession(extra);
|
|
5003
5792
|
if (!rs)
|
|
@@ -5009,7 +5798,7 @@ function buildMcpServer() {
|
|
|
5009
5798
|
server.registerTool("escalate_to_map", {
|
|
5010
5799
|
title: "Escalate to map",
|
|
5011
5800
|
description: "Escalate this grilling session into a map when the feature outgrows one context window. Flips the feature to mapped and scaffolds docs/features/<slug>/map.md, seeding Destination + Notes from your arguments (Not-yet-specified and Out-of-scope start empty). Idempotent: calling it on an already-mapped feature warns and changes nothing.",
|
|
5012
|
-
inputSchema: { destination:
|
|
5801
|
+
inputSchema: { destination: z4.string(), notes: z4.string().optional() }
|
|
5013
5802
|
}, async (args, extra) => {
|
|
5014
5803
|
const rs = await resolveCtxSession(extra);
|
|
5015
5804
|
if (!rs)
|
|
@@ -5023,7 +5812,7 @@ function buildMcpServer() {
|
|
|
5023
5812
|
server.registerTool("emit_waypoints", {
|
|
5024
5813
|
title: "Emit waypoints",
|
|
5025
5814
|
description: "Batch-create waypoints on the map (the feature must already be mapped \u2014 call escalate_to_map first). Each waypoint: title, type (grilling|research|prototype|task), question, blockedBy[] (1-based positions within THIS batch, and/or ids of already-stored waypoints). Available from any session once mapped \u2014 any session may branch the map.",
|
|
5026
|
-
inputSchema: { waypoints:
|
|
5815
|
+
inputSchema: { waypoints: z4.array(WaypointInput) }
|
|
5027
5816
|
}, async (args, extra) => {
|
|
5028
5817
|
const rs = await resolveCtxSession(extra);
|
|
5029
5818
|
if (!rs)
|
|
@@ -5033,7 +5822,7 @@ function buildMcpServer() {
|
|
|
5033
5822
|
server.registerTool("resolve_waypoint", {
|
|
5034
5823
|
title: "Resolve waypoint",
|
|
5035
5824
|
description: "End the current waypoint: `resolved` (its question is answered) or `dropped` (no longer needed). Write the decision prose to decisions.md (or the gist to map.md Out-of-scope for a drop) FIRST \u2014 this tool flips machinery only (marks the waypoint terminal, frees any dependents on the frontier). `summary` is the one-line gist shown in the UI. Call this exactly once, as the last thing you do.",
|
|
5036
|
-
inputSchema: { id:
|
|
5825
|
+
inputSchema: { id: z4.string(), disposition: WaypointDisposition, summary: z4.string() }
|
|
5037
5826
|
}, async (args, extra) => {
|
|
5038
5827
|
const rs = await resolveCtxSession(extra);
|
|
5039
5828
|
if (!rs)
|
|
@@ -5045,7 +5834,7 @@ function buildMcpServer() {
|
|
|
5045
5834
|
server.registerTool("record_event", {
|
|
5046
5835
|
title: "Record event",
|
|
5047
5836
|
description: "Add a note to the feature timeline (decisions recorded, spec saved, milestones).",
|
|
5048
|
-
inputSchema: { type:
|
|
5837
|
+
inputSchema: { type: z4.string(), message: z4.string() }
|
|
5049
5838
|
}, async (args, extra) => {
|
|
5050
5839
|
const rs = await resolveCtxSession(extra);
|
|
5051
5840
|
if (!rs)
|
|
@@ -5168,12 +5957,16 @@ import { Hono as Hono2 } from "hono";
|
|
|
5168
5957
|
function handleSessionStart(ctx, sessionId, feature, payload) {
|
|
5169
5958
|
const ccSessionId = typeof payload?.session_id === "string" ? payload.session_id : undefined;
|
|
5170
5959
|
const transcriptPath = typeof payload?.transcript_path === "string" ? payload.transcript_path : undefined;
|
|
5960
|
+
const source = typeof payload?.source === "string" ? payload.source : undefined;
|
|
5961
|
+
const wasLive = getSessionRow(ctx, sessionId)?.status === "live";
|
|
5171
5962
|
markSessionLive(ctx, sessionId, { ccSessionId, transcriptPath });
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5963
|
+
if (!wasLive) {
|
|
5964
|
+
emit(ctx, feature.id, {
|
|
5965
|
+
type: "session.started",
|
|
5966
|
+
message: source === "resume" ? "session live (conversation resumed)" : "session live",
|
|
5967
|
+
data: { sessionId, ccSessionId, transcriptPath, source: source ?? null }
|
|
5968
|
+
});
|
|
5969
|
+
}
|
|
5177
5970
|
return {
|
|
5178
5971
|
hookSpecificOutput: {
|
|
5179
5972
|
hookEventName: "SessionStart",
|
|
@@ -5181,7 +5974,9 @@ function handleSessionStart(ctx, sessionId, feature, payload) {
|
|
|
5181
5974
|
}
|
|
5182
5975
|
};
|
|
5183
5976
|
}
|
|
5184
|
-
function handleUserPrompt(ctx, feature) {
|
|
5977
|
+
function handleUserPrompt(ctx, sessionId, feature, payload) {
|
|
5978
|
+
const prompt = typeof payload?.prompt === "string" ? payload.prompt : undefined;
|
|
5979
|
+
noteKickoffPrompt(ctx, sessionId, prompt);
|
|
5185
5980
|
const tickets2 = listByFeature2(ctx, feature.id).length;
|
|
5186
5981
|
return {
|
|
5187
5982
|
hookSpecificOutput: {
|
|
@@ -5248,7 +6043,7 @@ var init_hooks = __esm(() => {
|
|
|
5248
6043
|
case "session-start":
|
|
5249
6044
|
return c.json(handleSessionStart(ctx, sessionId, feature, body.payload));
|
|
5250
6045
|
case "user-prompt":
|
|
5251
|
-
return c.json(handleUserPrompt(ctx, feature));
|
|
6046
|
+
return c.json(handleUserPrompt(ctx, sessionId, feature, body.payload));
|
|
5252
6047
|
case "session-end":
|
|
5253
6048
|
return c.json(handleSessionEnd(ctx, sessionId, feature));
|
|
5254
6049
|
default:
|
|
@@ -5313,9 +6108,9 @@ var init_stream = __esm(() => {
|
|
|
5313
6108
|
});
|
|
5314
6109
|
|
|
5315
6110
|
// src/routes/web.ts
|
|
5316
|
-
import { existsSync as
|
|
6111
|
+
import { existsSync as existsSync13, statSync } from "fs";
|
|
5317
6112
|
import { readFile } from "fs/promises";
|
|
5318
|
-
import { extname, join as
|
|
6113
|
+
import { extname, join as join12, normalize, resolve as resolve6, sep as sep2 } from "path";
|
|
5319
6114
|
function contentType(path) {
|
|
5320
6115
|
return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
5321
6116
|
}
|
|
@@ -5333,7 +6128,7 @@ function safeJoin(root, pathname) {
|
|
|
5333
6128
|
} catch {
|
|
5334
6129
|
return null;
|
|
5335
6130
|
}
|
|
5336
|
-
const joined = normalize(
|
|
6131
|
+
const joined = normalize(join12(root, decoded));
|
|
5337
6132
|
if (joined !== root && !joined.startsWith(root + sep2))
|
|
5338
6133
|
return null;
|
|
5339
6134
|
return joined;
|
|
@@ -5346,7 +6141,7 @@ function resolveWebDist() {
|
|
|
5346
6141
|
}
|
|
5347
6142
|
function mountWebApp(app, distDir) {
|
|
5348
6143
|
const root = resolve6(distDir);
|
|
5349
|
-
const indexHtml =
|
|
6144
|
+
const indexHtml = join12(root, "index.html");
|
|
5350
6145
|
app.get("*", async (c, next) => {
|
|
5351
6146
|
const { pathname } = new URL(c.req.url);
|
|
5352
6147
|
if (/^\/(api|mcp|ws|health)(\/|$)/.test(pathname))
|
|
@@ -5366,7 +6161,7 @@ function mountWebApp(app, distDir) {
|
|
|
5366
6161
|
}
|
|
5367
6162
|
function mountWebAppIfBuilt(app) {
|
|
5368
6163
|
const dist = resolveWebDist();
|
|
5369
|
-
if (
|
|
6164
|
+
if (existsSync13(join12(dist, "index.html")))
|
|
5370
6165
|
mountWebApp(app, dist);
|
|
5371
6166
|
}
|
|
5372
6167
|
var MIME;
|
|
@@ -5413,14 +6208,14 @@ var init_context = __esm(() => {
|
|
|
5413
6208
|
});
|
|
5414
6209
|
|
|
5415
6210
|
// src/trpc/routers/docs.ts
|
|
5416
|
-
import * as
|
|
6211
|
+
import * as z5 from "zod";
|
|
5417
6212
|
var docsRouter;
|
|
5418
6213
|
var init_docs = __esm(() => {
|
|
5419
6214
|
init_knowledge();
|
|
5420
6215
|
init_repo();
|
|
5421
6216
|
init_context();
|
|
5422
6217
|
docsRouter = router({
|
|
5423
|
-
read: publicProcedure.input(
|
|
6218
|
+
read: publicProcedure.input(z5.object({ featureId: z5.string(), relPath: z5.string() })).query(({ ctx, input }) => {
|
|
5424
6219
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
5425
6220
|
return readDoc(ctx, feature, input.relPath);
|
|
5426
6221
|
})
|
|
@@ -5428,23 +6223,23 @@ var init_docs = __esm(() => {
|
|
|
5428
6223
|
});
|
|
5429
6224
|
|
|
5430
6225
|
// src/trpc/routers/events.ts
|
|
5431
|
-
import * as
|
|
6226
|
+
import * as z6 from "zod";
|
|
5432
6227
|
var eventsRouter;
|
|
5433
6228
|
var init_events2 = __esm(() => {
|
|
5434
6229
|
init_events();
|
|
5435
6230
|
init_context();
|
|
5436
6231
|
eventsRouter = router({
|
|
5437
|
-
list: publicProcedure.input(
|
|
5438
|
-
listByProject: publicProcedure.input(
|
|
6232
|
+
list: publicProcedure.input(z6.object({ featureId: z6.string(), afterId: z6.number().optional() })).query(({ ctx, input }) => listAfter(ctx, input.featureId, input.afterId)),
|
|
6233
|
+
listByProject: publicProcedure.input(z6.object({ projectId: z6.string(), afterId: z6.number().optional() })).query(({ ctx, input }) => listByProject(ctx, input.projectId, input.afterId))
|
|
5439
6234
|
});
|
|
5440
6235
|
});
|
|
5441
6236
|
|
|
5442
6237
|
// src/launcher/artifacts.ts
|
|
5443
|
-
import { mkdirSync as
|
|
5444
|
-
import { fileURLToPath as
|
|
5445
|
-
import { join as
|
|
6238
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
6239
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
6240
|
+
import { join as join13 } from "path";
|
|
5446
6241
|
function hookClientPath() {
|
|
5447
|
-
return resolveAsset(ASSET_ENV.hookClient,
|
|
6242
|
+
return resolveAsset(ASSET_ENV.hookClient, fileURLToPath5(new URL("./hook-client.ts", import.meta.url)));
|
|
5448
6243
|
}
|
|
5449
6244
|
function serverUrlFor(config2) {
|
|
5450
6245
|
return `http://localhost:${config2.serverPort}`;
|
|
@@ -5645,7 +6440,10 @@ function renderSettings(hookClient) {
|
|
|
5645
6440
|
return {
|
|
5646
6441
|
permissions: { allow: [...RUNCASTLE_MCP_ALLOW_RULES, ...SESSION_BASH_ALLOW_RULES] },
|
|
5647
6442
|
hooks: {
|
|
5648
|
-
SessionStart:
|
|
6443
|
+
SessionStart: SESSION_START_SOURCES.map((source) => ({
|
|
6444
|
+
matcher: source,
|
|
6445
|
+
hooks: [cmd("session-start")]
|
|
6446
|
+
})),
|
|
5649
6447
|
UserPromptSubmit: [{ hooks: [cmd("user-prompt")] }],
|
|
5650
6448
|
SessionEnd: [{ hooks: [cmd("session-end")] }]
|
|
5651
6449
|
}
|
|
@@ -5665,16 +6463,16 @@ function renderMcpConfig(session, config2) {
|
|
|
5665
6463
|
async function writeSessionArtifacts(input) {
|
|
5666
6464
|
const { session, feature, config: config2, waypoint } = input;
|
|
5667
6465
|
const dir = sessionDir(session.id);
|
|
5668
|
-
|
|
5669
|
-
const systemPromptPath =
|
|
5670
|
-
const settingsPath =
|
|
5671
|
-
const mcpConfigPath =
|
|
6466
|
+
mkdirSync7(dir, { recursive: true });
|
|
6467
|
+
const systemPromptPath = join13(dir, "system-prompt.md");
|
|
6468
|
+
const settingsPath = join13(dir, "settings.json");
|
|
6469
|
+
const mcpConfigPath = join13(dir, "mcp.json");
|
|
5672
6470
|
writeFileSync2(systemPromptPath, renderSystemPrompt(feature, session.kind, waypoint), "utf8");
|
|
5673
6471
|
writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath()), null, 2), "utf8");
|
|
5674
6472
|
writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
|
|
5675
6473
|
return { systemPromptPath, settingsPath, mcpConfigPath };
|
|
5676
6474
|
}
|
|
5677
|
-
var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_ALLOW_RULES;
|
|
6475
|
+
var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_ALLOW_RULES, SESSION_START_SOURCES;
|
|
5678
6476
|
var init_artifacts = __esm(() => {
|
|
5679
6477
|
init_paths();
|
|
5680
6478
|
init_asset_paths();
|
|
@@ -5699,13 +6497,14 @@ var init_artifacts = __esm(() => {
|
|
|
5699
6497
|
"Bash(git branch:*)",
|
|
5700
6498
|
"Bash(git show:*)"
|
|
5701
6499
|
];
|
|
6500
|
+
SESSION_START_SOURCES = ["startup", "resume", "clear", "compact", "fork"];
|
|
5702
6501
|
});
|
|
5703
6502
|
|
|
5704
6503
|
// src/launcher/launcher.ts
|
|
5705
|
-
import { existsSync as
|
|
5706
|
-
import { dirname as
|
|
5707
|
-
import { fileURLToPath as
|
|
5708
|
-
import { and as
|
|
6504
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6505
|
+
import { dirname as dirname6, join as join14, resolve as resolve7 } from "path";
|
|
6506
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
6507
|
+
import { and as and7, eq as eq10 } from "drizzle-orm";
|
|
5709
6508
|
function ptyExitMessage(exitCode) {
|
|
5710
6509
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
5711
6510
|
return `terminal exited (code ${label})`;
|
|
@@ -5736,23 +6535,23 @@ function resolveClaudeExecutable() {
|
|
|
5736
6535
|
function claudeSpawnTarget(claudeArgs) {
|
|
5737
6536
|
return spawnTargetFor(resolveClaudeExecutable(), claudeArgs);
|
|
5738
6537
|
}
|
|
5739
|
-
function resolvePluginDir(fromDir =
|
|
5740
|
-
const rel =
|
|
6538
|
+
function resolvePluginDir(fromDir = dirname6(fileURLToPath6(import.meta.url))) {
|
|
6539
|
+
const rel = join14("packages", "skills", "packs", "runcastle");
|
|
5741
6540
|
const override = process.env[SKILLS_DIR_ENV];
|
|
5742
6541
|
if (override) {
|
|
5743
|
-
const dir2 =
|
|
5744
|
-
if (
|
|
6542
|
+
const dir2 = join14(resolve7(override), "packs", "runcastle");
|
|
6543
|
+
if (existsSync14(dir2))
|
|
5745
6544
|
return dir2;
|
|
5746
6545
|
throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
|
|
5747
6546
|
}
|
|
5748
6547
|
const searched = [];
|
|
5749
6548
|
let dir = fromDir;
|
|
5750
6549
|
for (let i = 0;i < 8; i += 1) {
|
|
5751
|
-
const candidate =
|
|
6550
|
+
const candidate = join14(dir, rel);
|
|
5752
6551
|
searched.push(candidate);
|
|
5753
|
-
if (
|
|
6552
|
+
if (existsSync14(candidate))
|
|
5754
6553
|
return candidate;
|
|
5755
|
-
const parent =
|
|
6554
|
+
const parent = dirname6(dir);
|
|
5756
6555
|
if (parent === dir)
|
|
5757
6556
|
break;
|
|
5758
6557
|
dir = parent;
|
|
@@ -5762,7 +6561,7 @@ function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
|
|
|
5762
6561
|
`)}`);
|
|
5763
6562
|
}
|
|
5764
6563
|
function activeRunFor(ctx, featureId) {
|
|
5765
|
-
const row = ctx.db.select().from(runs).where(
|
|
6564
|
+
const row = ctx.db.select().from(runs).where(and7(eq10(runs.featureId, featureId), eq10(runs.status, "running"))).limit(1).get();
|
|
5766
6565
|
return row ? rowToRun(row) : null;
|
|
5767
6566
|
}
|
|
5768
6567
|
function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
@@ -6013,6 +6812,7 @@ function spawnEmbeddedPty(ctx, feature, session, worktreePath, serverUrl, claude
|
|
|
6013
6812
|
message: "embedded terminal spawned",
|
|
6014
6813
|
data: { sessionId: session.id, mode: "embedded", pid: entry.pty.pid }
|
|
6015
6814
|
});
|
|
6815
|
+
armSessionReadyWatchdog(ctx, session);
|
|
6016
6816
|
} catch (err) {
|
|
6017
6817
|
markSessionEnded(ctx, session.id);
|
|
6018
6818
|
releaseForSession(ctx, session.id);
|
|
@@ -6041,6 +6841,7 @@ var init_launcher = __esm(() => {
|
|
|
6041
6841
|
init_runner();
|
|
6042
6842
|
init_artifacts();
|
|
6043
6843
|
init_sessions();
|
|
6844
|
+
init_sessions();
|
|
6044
6845
|
init_end_session();
|
|
6045
6846
|
CC_NESTING_ENV = [
|
|
6046
6847
|
"CLAUDE_CODE_CHILD_SESSION",
|
|
@@ -6055,7 +6856,7 @@ var init_launcher = __esm(() => {
|
|
|
6055
6856
|
});
|
|
6056
6857
|
|
|
6057
6858
|
// src/trpc/routers/feature.ts
|
|
6058
|
-
import * as
|
|
6859
|
+
import * as z7 from "zod";
|
|
6059
6860
|
var gateId, featureRouter;
|
|
6060
6861
|
var init_feature = __esm(() => {
|
|
6061
6862
|
init_src();
|
|
@@ -6066,37 +6867,38 @@ var init_feature = __esm(() => {
|
|
|
6066
6867
|
init_git();
|
|
6067
6868
|
init_repo();
|
|
6068
6869
|
init_context();
|
|
6069
|
-
gateId =
|
|
6870
|
+
gateId = z7.enum(["G1", "G2", "G3", "G4", "G5"]);
|
|
6070
6871
|
featureRouter = router({
|
|
6071
|
-
create: publicProcedure.input(
|
|
6072
|
-
projectId:
|
|
6073
|
-
title:
|
|
6074
|
-
oneLiner:
|
|
6075
|
-
baseBranch:
|
|
6872
|
+
create: publicProcedure.input(z7.object({
|
|
6873
|
+
projectId: z7.string(),
|
|
6874
|
+
title: z7.string().min(1),
|
|
6875
|
+
oneLiner: z7.string(),
|
|
6876
|
+
baseBranch: z7.string().optional()
|
|
6076
6877
|
})).mutation(({ ctx, input }) => createFeature(ctx, input)),
|
|
6077
|
-
list: publicProcedure.input(
|
|
6078
|
-
get: publicProcedure.input(
|
|
6079
|
-
launchSession: publicProcedure.input(
|
|
6080
|
-
featureId:
|
|
6878
|
+
list: publicProcedure.input(z7.object({ projectId: z7.string() })).query(({ ctx, input }) => list(ctx, input.projectId)),
|
|
6879
|
+
get: publicProcedure.input(z7.object({ id: z7.string() })).query(({ ctx, input }) => getFeatureFull(ctx, input.id)),
|
|
6880
|
+
launchSession: publicProcedure.input(z7.object({
|
|
6881
|
+
featureId: z7.string(),
|
|
6081
6882
|
kind: SessionKind,
|
|
6082
|
-
kickoffLine:
|
|
6883
|
+
kickoffLine: z7.string().min(1).optional()
|
|
6083
6884
|
})).mutation(({ ctx, input }) => launchSession(ctx, input)),
|
|
6084
|
-
workWaypoint: publicProcedure.input(
|
|
6085
|
-
converge: publicProcedure.input(
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6885
|
+
workWaypoint: publicProcedure.input(z7.object({ featureId: z7.string(), waypointId: z7.string() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
|
|
6886
|
+
converge: publicProcedure.input(z7.object({ featureId: z7.string(), overrideReason: z7.string().min(1).optional() })).mutation(({ ctx, input }) => converge(ctx, { featureId: input.featureId, overrideReason: input.overrideReason })),
|
|
6887
|
+
resendKickoff: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => resendKickoff(ctx, input.sessionId)),
|
|
6888
|
+
endSession: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => endSession(ctx, input.sessionId)),
|
|
6889
|
+
advance: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => advance(ctx, input.featureId)),
|
|
6890
|
+
overrideGate: publicProcedure.input(z7.object({ featureId: z7.string(), gate: gateId, reason: z7.string().min(1) })).mutation(({ ctx, input }) => overrideGate(ctx, input.featureId, input.gate, input.reason)),
|
|
6891
|
+
archive: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => archiveFeature(ctx, input.featureId)),
|
|
6892
|
+
unarchive: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => unarchiveFeature(ctx, input.featureId)),
|
|
6893
|
+
delete: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => deleteFeature(ctx, input.featureId)),
|
|
6894
|
+
burn: publicProcedure.input(z7.object({ featureId: z7.string(), model: z7.string().min(1).optional() })).mutation(({ ctx, input }) => burn(ctx, input.featureId, { modelOverride: input.model })),
|
|
6895
|
+
testDrive: publicProcedure.input(z7.object({ featureId: z7.string(), action: z7.enum(["start", "stop"]) })).mutation(async ({ ctx, input }) => {
|
|
6094
6896
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
6095
6897
|
const project = projectForFeature(ctx, feature);
|
|
6096
6898
|
return testDrive(ctx, project, feature, input.action);
|
|
6097
6899
|
}),
|
|
6098
6900
|
driveInfo: publicProcedure.query(() => activeDriveInfo()),
|
|
6099
|
-
merge: publicProcedure.input(
|
|
6901
|
+
merge: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(async ({ ctx, input }) => {
|
|
6100
6902
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
6101
6903
|
const project = projectForFeature(ctx, feature);
|
|
6102
6904
|
if (activeTestDriveFeatureId() === feature.id) {
|
|
@@ -6119,9 +6921,9 @@ var init_feature = __esm(() => {
|
|
|
6119
6921
|
});
|
|
6120
6922
|
|
|
6121
6923
|
// src/services/fsbrowse.ts
|
|
6122
|
-
import { existsSync as
|
|
6924
|
+
import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
|
|
6123
6925
|
import { homedir as homedir2 } from "os";
|
|
6124
|
-
import { basename, dirname as
|
|
6926
|
+
import { basename, dirname as dirname7, isAbsolute, join as join15, resolve as resolve8, sep as sep3 } from "path";
|
|
6125
6927
|
function expandPath(input, home = homedir2()) {
|
|
6126
6928
|
const trimmed = input.trim();
|
|
6127
6929
|
if (trimmed === "")
|
|
@@ -6130,12 +6932,12 @@ function expandPath(input, home = homedir2()) {
|
|
|
6130
6932
|
if (out === "~")
|
|
6131
6933
|
out = home;
|
|
6132
6934
|
else if (out.startsWith("~/") || out.startsWith("~\\"))
|
|
6133
|
-
out =
|
|
6935
|
+
out = join15(home, out.slice(2));
|
|
6134
6936
|
return resolve8(out);
|
|
6135
6937
|
}
|
|
6136
6938
|
function looksLikeRepo(dir) {
|
|
6137
6939
|
try {
|
|
6138
|
-
return
|
|
6940
|
+
return existsSync15(join15(dir, ".git"));
|
|
6139
6941
|
} catch {
|
|
6140
6942
|
return false;
|
|
6141
6943
|
}
|
|
@@ -6146,20 +6948,20 @@ function listRoots(home = homedir2()) {
|
|
|
6146
6948
|
for (let code = 67;code <= 90; code++) {
|
|
6147
6949
|
const drive = `${String.fromCharCode(code)}:${sep3}`;
|
|
6148
6950
|
try {
|
|
6149
|
-
if (
|
|
6951
|
+
if (existsSync15(drive))
|
|
6150
6952
|
roots.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
|
|
6151
6953
|
} catch {}
|
|
6152
6954
|
}
|
|
6153
6955
|
} else {
|
|
6154
6956
|
roots.push({ label: "/", path: "/", kind: "drive" });
|
|
6155
6957
|
}
|
|
6156
|
-
if (
|
|
6958
|
+
if (existsSync15(home))
|
|
6157
6959
|
roots.push({ label: "Home", path: home, kind: "home" });
|
|
6158
6960
|
const seen = new Set(roots.map(pathKey));
|
|
6159
6961
|
for (const name of COMMON_DIRS) {
|
|
6160
|
-
const candidate =
|
|
6962
|
+
const candidate = join15(home, name);
|
|
6161
6963
|
const key = pathKey({ path: candidate });
|
|
6162
|
-
if (seen.has(key) || !
|
|
6964
|
+
if (seen.has(key) || !existsSync15(candidate))
|
|
6163
6965
|
continue;
|
|
6164
6966
|
seen.add(key);
|
|
6165
6967
|
roots.push({ label: name, path: candidate, kind: "common" });
|
|
@@ -6174,7 +6976,7 @@ function crumbsFor(dir) {
|
|
|
6174
6976
|
let cur = dir;
|
|
6175
6977
|
for (;; ) {
|
|
6176
6978
|
out.unshift({ name: basename(cur) || cur, path: cur });
|
|
6177
|
-
const parent =
|
|
6979
|
+
const parent = dirname7(cur);
|
|
6178
6980
|
if (parent === cur)
|
|
6179
6981
|
break;
|
|
6180
6982
|
cur = parent;
|
|
@@ -6182,7 +6984,7 @@ function crumbsFor(dir) {
|
|
|
6182
6984
|
return out;
|
|
6183
6985
|
}
|
|
6184
6986
|
function parentOf(dir) {
|
|
6185
|
-
const parent =
|
|
6987
|
+
const parent = dirname7(dir);
|
|
6186
6988
|
return parent === dir ? null : parent;
|
|
6187
6989
|
}
|
|
6188
6990
|
function browseDir(input, showHidden = false, home = homedir2()) {
|
|
@@ -6190,13 +6992,13 @@ function browseDir(input, showHidden = false, home = homedir2()) {
|
|
|
6190
6992
|
if (!isAbsolute(target)) {
|
|
6191
6993
|
throw new InvalidInputError(`path is not absolute: ${target}`);
|
|
6192
6994
|
}
|
|
6193
|
-
if (!
|
|
6995
|
+
if (!existsSync15(target)) {
|
|
6194
6996
|
throw new InvalidInputError(`path does not exist: ${target}`);
|
|
6195
6997
|
}
|
|
6196
6998
|
let dir = target;
|
|
6197
6999
|
try {
|
|
6198
7000
|
if (!statSync2(target).isDirectory())
|
|
6199
|
-
dir =
|
|
7001
|
+
dir = dirname7(target);
|
|
6200
7002
|
} catch (e) {
|
|
6201
7003
|
throw new InvalidInputError(`cannot read path: ${target} (${errMsg2(e)})`);
|
|
6202
7004
|
}
|
|
@@ -6218,7 +7020,7 @@ function browseDir(input, showHidden = false, home = homedir2()) {
|
|
|
6218
7020
|
continue;
|
|
6219
7021
|
if (!showHidden && name.startsWith("."))
|
|
6220
7022
|
continue;
|
|
6221
|
-
const path =
|
|
7023
|
+
const path = join15(dir, name);
|
|
6222
7024
|
const isSymlink = dirent.isSymbolicLink();
|
|
6223
7025
|
let isDirectory = dirent.isDirectory();
|
|
6224
7026
|
if (isSymlink) {
|
|
@@ -6253,9 +7055,9 @@ var init_fsbrowse = __esm(() => {
|
|
|
6253
7055
|
});
|
|
6254
7056
|
|
|
6255
7057
|
// src/services/projects.ts
|
|
6256
|
-
import { existsSync as
|
|
6257
|
-
import { basename as basename2, join as
|
|
6258
|
-
import { eq as
|
|
7058
|
+
import { existsSync as existsSync16 } from "fs";
|
|
7059
|
+
import { basename as basename2, join as join16 } from "path";
|
|
7060
|
+
import { eq as eq11, isNull } from "drizzle-orm";
|
|
6259
7061
|
function listProjects(ctx) {
|
|
6260
7062
|
return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
|
|
6261
7063
|
}
|
|
@@ -6277,7 +7079,7 @@ async function openProject(ctx, rawPath) {
|
|
|
6277
7079
|
const name = basename2(repoPath) || repoPath;
|
|
6278
7080
|
const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
|
|
6279
7081
|
if (existing) {
|
|
6280
|
-
ctx.db.update(projects).set({ mainBranch, repoPath, closedAt: null }).where(
|
|
7082
|
+
ctx.db.update(projects).set({ mainBranch, repoPath, closedAt: null }).where(eq11(projects.id, existing.id)).run();
|
|
6281
7083
|
const reopened = { ...existing, mainBranch, repoPath };
|
|
6282
7084
|
emitProject(ctx, existing.id, {
|
|
6283
7085
|
type: "project.opened",
|
|
@@ -6301,14 +7103,25 @@ async function openProject(ctx, rawPath) {
|
|
|
6301
7103
|
message: `project ${name} at ${repoPath} (${mainBranch})`
|
|
6302
7104
|
});
|
|
6303
7105
|
warnIfTranslatedMount(ctx, project.id, repoPath);
|
|
7106
|
+
maybeAutoPrepare(ctx, project);
|
|
6304
7107
|
return project;
|
|
6305
7108
|
}
|
|
7109
|
+
function maybeAutoPrepare(ctx, project) {
|
|
7110
|
+
if (!ctx.config.autoPrepare)
|
|
7111
|
+
return;
|
|
7112
|
+
startPrep(ctx, project.id).catch((e) => {
|
|
7113
|
+
emitProject(ctx, project.id, {
|
|
7114
|
+
type: "prep.run.failed",
|
|
7115
|
+
message: `could not start preparation: ${e instanceof Error ? e.message : String(e)}`
|
|
7116
|
+
});
|
|
7117
|
+
});
|
|
7118
|
+
}
|
|
6306
7119
|
function closeProject(ctx, projectId) {
|
|
6307
7120
|
const project = requireProjectById(ctx, projectId);
|
|
6308
7121
|
if (projectHasActiveRun(ctx, projectId)) {
|
|
6309
7122
|
throw new InvalidInputError(`cannot close ${project.name}: a run is in flight \u2014 wait for it to finish or cancel it first`);
|
|
6310
7123
|
}
|
|
6311
|
-
ctx.db.update(projects).set({ closedAt: Date.now() }).where(
|
|
7124
|
+
ctx.db.update(projects).set({ closedAt: Date.now() }).where(eq11(projects.id, projectId)).run();
|
|
6312
7125
|
emitProject(ctx, projectId, {
|
|
6313
7126
|
type: "project.closed",
|
|
6314
7127
|
message: `project ${project.name} closed`
|
|
@@ -6317,7 +7130,7 @@ function closeProject(ctx, projectId) {
|
|
|
6317
7130
|
}
|
|
6318
7131
|
function renameProject(ctx, projectId, name) {
|
|
6319
7132
|
const project = requireProjectById(ctx, projectId);
|
|
6320
|
-
ctx.db.update(projects).set({ name }).where(
|
|
7133
|
+
ctx.db.update(projects).set({ name }).where(eq11(projects.id, projectId)).run();
|
|
6321
7134
|
emitProject(ctx, projectId, {
|
|
6322
7135
|
type: "project.renamed",
|
|
6323
7136
|
message: `project renamed ${project.name} \u2192 ${name}`,
|
|
@@ -6335,7 +7148,7 @@ async function assertRepoTolerant(ctx, repoPath) {
|
|
|
6335
7148
|
} catch (e) {
|
|
6336
7149
|
if (!isNotImplemented(e))
|
|
6337
7150
|
throw e;
|
|
6338
|
-
if (!
|
|
7151
|
+
if (!existsSync16(join16(repoPath, ".git"))) {
|
|
6339
7152
|
throw new InvalidInputError(`not a git repository: ${repoPath}`);
|
|
6340
7153
|
}
|
|
6341
7154
|
}
|
|
@@ -6356,31 +7169,59 @@ var init_projects = __esm(() => {
|
|
|
6356
7169
|
init_events();
|
|
6357
7170
|
init_fsbrowse();
|
|
6358
7171
|
init_git();
|
|
7172
|
+
init_prep();
|
|
6359
7173
|
init_repo();
|
|
6360
7174
|
});
|
|
6361
7175
|
|
|
6362
7176
|
// src/trpc/routers/project.ts
|
|
6363
|
-
import * as
|
|
7177
|
+
import * as z8 from "zod";
|
|
6364
7178
|
var projectRouter;
|
|
6365
7179
|
var init_project = __esm(() => {
|
|
7180
|
+
init_src();
|
|
7181
|
+
init_findings();
|
|
6366
7182
|
init_fsbrowse();
|
|
6367
7183
|
init_git();
|
|
7184
|
+
init_prep();
|
|
6368
7185
|
init_projects();
|
|
6369
7186
|
init_repo();
|
|
6370
7187
|
init_context();
|
|
6371
7188
|
projectRouter = router({
|
|
6372
7189
|
list: publicProcedure.query(({ ctx }) => listProjects(ctx)),
|
|
6373
7190
|
roots: publicProcedure.query(() => listRoots()),
|
|
6374
|
-
browse: publicProcedure.input(
|
|
6375
|
-
branches: publicProcedure.input(
|
|
6376
|
-
open: publicProcedure.input(
|
|
6377
|
-
close: publicProcedure.input(
|
|
6378
|
-
rename: publicProcedure.input(
|
|
7191
|
+
browse: publicProcedure.input(z8.object({ dir: z8.string().optional(), showHidden: z8.boolean().optional() }).optional()).query(({ input }) => browseDir(input?.dir, input?.showHidden ?? false)),
|
|
7192
|
+
branches: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => listBranches(requireProjectById(ctx, input.projectId))),
|
|
7193
|
+
open: publicProcedure.input(z8.object({ repoPath: z8.string().min(1) })).mutation(({ ctx, input }) => openProject(ctx, input.repoPath)),
|
|
7194
|
+
close: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ ctx, input }) => closeProject(ctx, input.projectId)),
|
|
7195
|
+
rename: publicProcedure.input(z8.object({ projectId: z8.string(), name: z8.string().min(1) })).mutation(({ ctx, input }) => renameProject(ctx, input.projectId, input.name)),
|
|
7196
|
+
prepare: publicProcedure.input(z8.object({
|
|
7197
|
+
projectId: z8.string(),
|
|
7198
|
+
refresh: z8.boolean().optional(),
|
|
7199
|
+
keys: z8.array(PreparedKey).optional()
|
|
7200
|
+
})).mutation(async ({ ctx, input }) => {
|
|
7201
|
+
const { prepId, keys } = await startPrep(ctx, input.projectId, {
|
|
7202
|
+
...input.refresh !== undefined ? { refresh: input.refresh } : {},
|
|
7203
|
+
...input.keys ? { keys: input.keys } : {}
|
|
7204
|
+
});
|
|
7205
|
+
return { prepId, keys };
|
|
7206
|
+
}),
|
|
7207
|
+
cancelPrepare: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ input }) => {
|
|
7208
|
+
cancelPrep(input.projectId);
|
|
7209
|
+
return { ok: true };
|
|
7210
|
+
}),
|
|
7211
|
+
prep: publicProcedure.input(z8.object({ projectId: z8.string() })).query(async ({ ctx, input }) => {
|
|
7212
|
+
const project = requireProjectById(ctx, input.projectId);
|
|
7213
|
+
return {
|
|
7214
|
+
latest: latestPrep(ctx, project.id),
|
|
7215
|
+
running: isPreparing(project.id),
|
|
7216
|
+
pendingKeys: keysToPrepare(ctx, project),
|
|
7217
|
+
findings: await listFindings(ctx, project)
|
|
7218
|
+
};
|
|
7219
|
+
})
|
|
6379
7220
|
});
|
|
6380
7221
|
});
|
|
6381
7222
|
|
|
6382
7223
|
// src/trpc/routers/run.ts
|
|
6383
|
-
import * as
|
|
7224
|
+
import * as z9 from "zod";
|
|
6384
7225
|
var runRouter;
|
|
6385
7226
|
var init_run = __esm(() => {
|
|
6386
7227
|
init_agent_stream();
|
|
@@ -6388,9 +7229,9 @@ var init_run = __esm(() => {
|
|
|
6388
7229
|
init_runner();
|
|
6389
7230
|
init_context();
|
|
6390
7231
|
runRouter = router({
|
|
6391
|
-
get: publicProcedure.input(
|
|
6392
|
-
agentTranscript: publicProcedure.input(
|
|
6393
|
-
cancel: publicProcedure.input(
|
|
7232
|
+
get: publicProcedure.input(z9.object({ runId: z9.string() })).query(({ ctx, input }) => getRunRow(ctx, input.runId)),
|
|
7233
|
+
agentTranscript: publicProcedure.input(z9.object({ ticketId: z9.string(), after: z9.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
|
|
7234
|
+
cancel: publicProcedure.input(z9.object({ runId: z9.string() })).mutation(({ input }) => {
|
|
6394
7235
|
cancelRun(input.runId);
|
|
6395
7236
|
return { ok: true };
|
|
6396
7237
|
})
|
|
@@ -6398,15 +7239,15 @@ var init_run = __esm(() => {
|
|
|
6398
7239
|
});
|
|
6399
7240
|
|
|
6400
7241
|
// src/services/settings.ts
|
|
6401
|
-
import { existsSync as
|
|
6402
|
-
import { dirname as
|
|
6403
|
-
import * as
|
|
6404
|
-
import { eq as
|
|
7242
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
7243
|
+
import { dirname as dirname8 } from "path";
|
|
7244
|
+
import * as z10 from "zod";
|
|
7245
|
+
import { eq as eq12 } from "drizzle-orm";
|
|
6405
7246
|
function readRawConfig(configFile) {
|
|
6406
|
-
if (!
|
|
7247
|
+
if (!existsSync17(configFile))
|
|
6407
7248
|
return {};
|
|
6408
7249
|
try {
|
|
6409
|
-
const parsed = JSON.parse(
|
|
7250
|
+
const parsed = JSON.parse(readFileSync7(configFile, "utf8"));
|
|
6410
7251
|
if (typeof parsed !== "object" || parsed === null)
|
|
6411
7252
|
return {};
|
|
6412
7253
|
return foldLegacyModelConfig(parsed);
|
|
@@ -6430,33 +7271,45 @@ function stepModelFields(fileRaw) {
|
|
|
6430
7271
|
});
|
|
6431
7272
|
}
|
|
6432
7273
|
function projectOverrides(ctx, projectId) {
|
|
6433
|
-
const row = ctx.db.select({
|
|
7274
|
+
const row = ctx.db.select({
|
|
7275
|
+
model: projects.model,
|
|
7276
|
+
sandbox: projects.sandbox,
|
|
7277
|
+
devCommand: projects.devCommand,
|
|
7278
|
+
setupCommand: projects.setupCommand,
|
|
7279
|
+
verifyCommands: projects.verifyCommands,
|
|
7280
|
+
knownFailures: projects.knownFailures,
|
|
7281
|
+
dbResetCommand: projects.dbResetCommand
|
|
7282
|
+
}).from(projects).where(eq12(projects.id, projectId)).get();
|
|
6434
7283
|
return {
|
|
6435
7284
|
model: row?.model ?? null,
|
|
6436
7285
|
sandbox: row?.sandbox ?? null,
|
|
6437
|
-
devCommand: row?.devCommand ?? null
|
|
7286
|
+
devCommand: row?.devCommand ?? null,
|
|
7287
|
+
setupCommand: row?.setupCommand ?? null,
|
|
7288
|
+
verifyCommands: row?.verifyCommands ?? null,
|
|
7289
|
+
knownFailures: row?.knownFailures ?? null,
|
|
7290
|
+
dbResetCommand: row?.dbResetCommand ?? null
|
|
6438
7291
|
};
|
|
6439
7292
|
}
|
|
6440
|
-
function resolveField(
|
|
7293
|
+
function resolveField(desc5, layers) {
|
|
6441
7294
|
const scoped = layers.overrides !== null;
|
|
6442
|
-
const scope = scoped &&
|
|
6443
|
-
const base = { key:
|
|
6444
|
-
if (
|
|
6445
|
-
const raw = layers.env[
|
|
7295
|
+
const scope = scoped && desc5.projectColumn ? "project" : "global";
|
|
7296
|
+
const base = { key: desc5.key, restartRequired: desc5.restartRequired, scope };
|
|
7297
|
+
if (desc5.envVar) {
|
|
7298
|
+
const raw = layers.env[desc5.envVar];
|
|
6446
7299
|
if (raw !== undefined && raw !== "") {
|
|
6447
|
-
return { ...base, value:
|
|
7300
|
+
return { ...base, value: desc5.parseEnv(raw), source: "env", editable: false };
|
|
6448
7301
|
}
|
|
6449
7302
|
}
|
|
6450
|
-
if (layers.overrides &&
|
|
6451
|
-
const ov = layers.overrides[
|
|
7303
|
+
if (layers.overrides && desc5.projectColumn) {
|
|
7304
|
+
const ov = layers.overrides[desc5.projectColumn];
|
|
6452
7305
|
if (ov !== null && ov !== "") {
|
|
6453
7306
|
return { ...base, value: ov, source: "project", editable: true };
|
|
6454
7307
|
}
|
|
6455
7308
|
}
|
|
6456
|
-
if (
|
|
6457
|
-
return { ...base, value: layers.fileRaw[
|
|
7309
|
+
if (desc5.configKey && Object.prototype.hasOwnProperty.call(layers.fileRaw, desc5.configKey)) {
|
|
7310
|
+
return { ...base, value: layers.fileRaw[desc5.configKey], source: "file", editable: true };
|
|
6458
7311
|
}
|
|
6459
|
-
const value =
|
|
7312
|
+
const value = desc5.configKey ? DEFAULTS[desc5.configKey] ?? null : null;
|
|
6460
7313
|
return { ...base, value, source: "default", editable: true };
|
|
6461
7314
|
}
|
|
6462
7315
|
function getSettings(ctx, projectId, io = {}) {
|
|
@@ -6473,59 +7326,61 @@ function updateSettings(ctx, input, io = {}) {
|
|
|
6473
7326
|
if (input.key.startsWith("stepModels.")) {
|
|
6474
7327
|
return updateStepModel(ctx, input, configFile, io);
|
|
6475
7328
|
}
|
|
6476
|
-
const
|
|
6477
|
-
if (!
|
|
7329
|
+
const desc5 = DESCRIPTORS.find((d) => d.key === input.key);
|
|
7330
|
+
if (!desc5)
|
|
6478
7331
|
throw new InvalidInputError(`unknown setting: ${input.key}`);
|
|
6479
|
-
if (
|
|
6480
|
-
const raw = env[
|
|
7332
|
+
if (desc5.envVar) {
|
|
7333
|
+
const raw = env[desc5.envVar];
|
|
6481
7334
|
if (raw !== undefined && raw !== "") {
|
|
6482
|
-
throw new InvalidInputError(`${
|
|
7335
|
+
throw new InvalidInputError(`${desc5.key} is set by environment variable ${desc5.envVar}; unset it to edit`);
|
|
6483
7336
|
}
|
|
6484
7337
|
}
|
|
6485
|
-
const toProject = input.projectId !== undefined &&
|
|
7338
|
+
const toProject = input.projectId !== undefined && desc5.projectColumn !== undefined;
|
|
6486
7339
|
if (input.value === null) {
|
|
6487
7340
|
if (!toProject)
|
|
6488
|
-
throw new InvalidInputError(`${
|
|
7341
|
+
throw new InvalidInputError(`${desc5.key} cannot be cleared`);
|
|
6489
7342
|
const project = requireProjectById(ctx, input.projectId);
|
|
6490
|
-
ctx.db.update(projects).set({ [
|
|
7343
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(eq12(projects.id, project.id)).run();
|
|
7344
|
+
recordHuman(ctx, project.id, desc5.key, null);
|
|
6491
7345
|
emitProject(ctx, project.id, {
|
|
6492
7346
|
type: "settings.updated",
|
|
6493
|
-
message: `${
|
|
6494
|
-
data: { key:
|
|
7347
|
+
message: `${desc5.key} override cleared`,
|
|
7348
|
+
data: { key: desc5.key, scope: "project", value: null }
|
|
6495
7349
|
});
|
|
6496
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
7350
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
6497
7351
|
}
|
|
6498
|
-
const parsed =
|
|
7352
|
+
const parsed = desc5.valueSchema.safeParse(input.value);
|
|
6499
7353
|
if (!parsed.success) {
|
|
6500
|
-
throw new InvalidInputError(`invalid value for ${
|
|
7354
|
+
throw new InvalidInputError(`invalid value for ${desc5.key}: ${parsed.error.issues[0]?.message}`);
|
|
6501
7355
|
}
|
|
6502
7356
|
const value = parsed.data;
|
|
6503
7357
|
if (toProject) {
|
|
6504
7358
|
const project = requireProjectById(ctx, input.projectId);
|
|
6505
|
-
ctx.db.update(projects).set({ [
|
|
7359
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(eq12(projects.id, project.id)).run();
|
|
7360
|
+
recordHuman(ctx, project.id, desc5.key, String(value));
|
|
6506
7361
|
emitProject(ctx, project.id, {
|
|
6507
7362
|
type: "settings.updated",
|
|
6508
|
-
message: `${
|
|
6509
|
-
data: { key:
|
|
7363
|
+
message: `${desc5.key} override set to ${String(value)}`,
|
|
7364
|
+
data: { key: desc5.key, scope: "project", value }
|
|
6510
7365
|
});
|
|
6511
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
7366
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
6512
7367
|
}
|
|
6513
|
-
if (!
|
|
6514
|
-
throw new InvalidInputError(`${
|
|
7368
|
+
if (!desc5.configKey) {
|
|
7369
|
+
throw new InvalidInputError(`${desc5.key} is a per-project setting; provide a projectId`);
|
|
6515
7370
|
}
|
|
6516
|
-
writeGlobal(configFile,
|
|
6517
|
-
ctx.config[
|
|
7371
|
+
writeGlobal(configFile, desc5.configKey, value);
|
|
7372
|
+
ctx.config[desc5.configKey] = value;
|
|
6518
7373
|
emitProject(ctx, GLOBAL_EVENT_KEY, {
|
|
6519
7374
|
type: "settings.updated",
|
|
6520
|
-
message: `${
|
|
6521
|
-
data: { key:
|
|
7375
|
+
message: `${desc5.key} set to ${String(value)}`,
|
|
7376
|
+
data: { key: desc5.key, scope: "global", value }
|
|
6522
7377
|
});
|
|
6523
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
7378
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
6524
7379
|
}
|
|
6525
7380
|
function writeGlobal(configFile, configKey, value) {
|
|
6526
7381
|
const raw = readRawConfig(configFile);
|
|
6527
7382
|
raw[configKey] = value;
|
|
6528
|
-
|
|
7383
|
+
mkdirSync8(dirname8(configFile), { recursive: true });
|
|
6529
7384
|
writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
|
|
6530
7385
|
`);
|
|
6531
7386
|
}
|
|
@@ -6536,7 +7391,7 @@ function updateStepModel(ctx, input, configFile, io) {
|
|
|
6536
7391
|
const modelStep = step;
|
|
6537
7392
|
let value = null;
|
|
6538
7393
|
if (input.value !== null) {
|
|
6539
|
-
const parsed =
|
|
7394
|
+
const parsed = z10.string().min(1).safeParse(input.value);
|
|
6540
7395
|
if (!parsed.success) {
|
|
6541
7396
|
throw new InvalidInputError(`invalid value for ${input.key}: ${parsed.error.issues[0]?.message}`);
|
|
6542
7397
|
}
|
|
@@ -6564,7 +7419,7 @@ function writeStepModel(configFile, step, value) {
|
|
|
6564
7419
|
else
|
|
6565
7420
|
stepModels[step] = value;
|
|
6566
7421
|
raw.stepModels = stepModels;
|
|
6567
|
-
|
|
7422
|
+
mkdirSync8(dirname8(configFile), { recursive: true });
|
|
6568
7423
|
writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
|
|
6569
7424
|
`);
|
|
6570
7425
|
}
|
|
@@ -6581,6 +7436,7 @@ var init_settings = __esm(() => {
|
|
|
6581
7436
|
init_schema();
|
|
6582
7437
|
init_errors();
|
|
6583
7438
|
init_events();
|
|
7439
|
+
init_findings();
|
|
6584
7440
|
init_repo();
|
|
6585
7441
|
DESCRIPTORS = [
|
|
6586
7442
|
{
|
|
@@ -6588,7 +7444,7 @@ var init_settings = __esm(() => {
|
|
|
6588
7444
|
configKey: "serverPort",
|
|
6589
7445
|
envVar: "RUNCASTLE_SERVER_PORT",
|
|
6590
7446
|
restartRequired: true,
|
|
6591
|
-
valueSchema:
|
|
7447
|
+
valueSchema: z10.number().int().positive(),
|
|
6592
7448
|
parseEnv: (raw) => Number(raw)
|
|
6593
7449
|
},
|
|
6594
7450
|
{
|
|
@@ -6597,7 +7453,7 @@ var init_settings = __esm(() => {
|
|
|
6597
7453
|
envVar: "RUNCASTLE_MODEL",
|
|
6598
7454
|
projectColumn: "model",
|
|
6599
7455
|
restartRequired: false,
|
|
6600
|
-
valueSchema:
|
|
7456
|
+
valueSchema: z10.string().min(1),
|
|
6601
7457
|
parseEnv: idEnv
|
|
6602
7458
|
},
|
|
6603
7459
|
{
|
|
@@ -6606,7 +7462,7 @@ var init_settings = __esm(() => {
|
|
|
6606
7462
|
envVar: "RUNCASTLE_SANDBOX",
|
|
6607
7463
|
projectColumn: "sandbox",
|
|
6608
7464
|
restartRequired: false,
|
|
6609
|
-
valueSchema:
|
|
7465
|
+
valueSchema: z10.enum(["docker", "podman", "noSandbox"]),
|
|
6610
7466
|
parseEnv: idEnv
|
|
6611
7467
|
},
|
|
6612
7468
|
{
|
|
@@ -6614,7 +7470,7 @@ var init_settings = __esm(() => {
|
|
|
6614
7470
|
configKey: "sandboxImage",
|
|
6615
7471
|
envVar: "RUNCASTLE_SANDBOX_IMAGE",
|
|
6616
7472
|
restartRequired: false,
|
|
6617
|
-
valueSchema:
|
|
7473
|
+
valueSchema: z10.string().min(1),
|
|
6618
7474
|
parseEnv: idEnv
|
|
6619
7475
|
},
|
|
6620
7476
|
{
|
|
@@ -6622,7 +7478,7 @@ var init_settings = __esm(() => {
|
|
|
6622
7478
|
configKey: "burnConcurrency",
|
|
6623
7479
|
envVar: "RUNCASTLE_BURN_CONCURRENCY",
|
|
6624
7480
|
restartRequired: false,
|
|
6625
|
-
valueSchema:
|
|
7481
|
+
valueSchema: z10.number().int().min(1).max(8),
|
|
6626
7482
|
parseEnv: (raw) => Number(raw)
|
|
6627
7483
|
},
|
|
6628
7484
|
{
|
|
@@ -6630,7 +7486,7 @@ var init_settings = __esm(() => {
|
|
|
6630
7486
|
configKey: "burnMaxIterations",
|
|
6631
7487
|
envVar: "RUNCASTLE_BURN_MAX_ITERATIONS",
|
|
6632
7488
|
restartRequired: false,
|
|
6633
|
-
valueSchema:
|
|
7489
|
+
valueSchema: z10.number().int().min(1).max(10),
|
|
6634
7490
|
parseEnv: (raw) => Number(raw)
|
|
6635
7491
|
},
|
|
6636
7492
|
{
|
|
@@ -6638,7 +7494,7 @@ var init_settings = __esm(() => {
|
|
|
6638
7494
|
configKey: "burnAttempts",
|
|
6639
7495
|
envVar: "RUNCASTLE_BURN_ATTEMPTS",
|
|
6640
7496
|
restartRequired: false,
|
|
6641
|
-
valueSchema:
|
|
7497
|
+
valueSchema: z10.number().int().min(1).max(5),
|
|
6642
7498
|
parseEnv: (raw) => Number(raw)
|
|
6643
7499
|
},
|
|
6644
7500
|
{
|
|
@@ -6646,7 +7502,7 @@ var init_settings = __esm(() => {
|
|
|
6646
7502
|
configKey: "burnConflictAttempts",
|
|
6647
7503
|
envVar: "RUNCASTLE_BURN_CONFLICT_ATTEMPTS",
|
|
6648
7504
|
restartRequired: false,
|
|
6649
|
-
valueSchema:
|
|
7505
|
+
valueSchema: z10.number().int().min(0).max(3),
|
|
6650
7506
|
parseEnv: (raw) => Number(raw)
|
|
6651
7507
|
},
|
|
6652
7508
|
{
|
|
@@ -6654,31 +7510,42 @@ var init_settings = __esm(() => {
|
|
|
6654
7510
|
configKey: "burnCpus",
|
|
6655
7511
|
envVar: "RUNCASTLE_BURN_CPUS",
|
|
6656
7512
|
restartRequired: false,
|
|
6657
|
-
valueSchema:
|
|
7513
|
+
valueSchema: z10.number().positive().max(256),
|
|
6658
7514
|
parseEnv: (raw) => Number(raw)
|
|
6659
7515
|
},
|
|
7516
|
+
{
|
|
7517
|
+
key: "autoPrepare",
|
|
7518
|
+
configKey: "autoPrepare",
|
|
7519
|
+
envVar: "RUNCASTLE_AUTO_PREPARE",
|
|
7520
|
+
restartRequired: false,
|
|
7521
|
+
valueSchema: z10.boolean(),
|
|
7522
|
+
parseEnv: (raw) => raw !== "0" && raw.toLowerCase() !== "false"
|
|
7523
|
+
},
|
|
6660
7524
|
{
|
|
6661
7525
|
key: "setupCommand",
|
|
6662
7526
|
configKey: "setupCommand",
|
|
6663
7527
|
envVar: "RUNCASTLE_SETUP_COMMAND",
|
|
7528
|
+
projectColumn: "setupCommand",
|
|
6664
7529
|
restartRequired: false,
|
|
6665
|
-
valueSchema:
|
|
7530
|
+
valueSchema: z10.string().min(1),
|
|
6666
7531
|
parseEnv: idEnv
|
|
6667
7532
|
},
|
|
6668
7533
|
{
|
|
6669
7534
|
key: "verifyCommands",
|
|
6670
7535
|
configKey: "verifyCommands",
|
|
6671
7536
|
envVar: "RUNCASTLE_VERIFY_COMMANDS",
|
|
7537
|
+
projectColumn: "verifyCommands",
|
|
6672
7538
|
restartRequired: false,
|
|
6673
|
-
valueSchema:
|
|
7539
|
+
valueSchema: z10.string().min(1),
|
|
6674
7540
|
parseEnv: idEnv
|
|
6675
7541
|
},
|
|
6676
7542
|
{
|
|
6677
7543
|
key: "knownFailures",
|
|
6678
7544
|
configKey: "knownFailures",
|
|
6679
7545
|
envVar: "RUNCASTLE_KNOWN_FAILURES",
|
|
7546
|
+
projectColumn: "knownFailures",
|
|
6680
7547
|
restartRequired: false,
|
|
6681
|
-
valueSchema:
|
|
7548
|
+
valueSchema: z10.string().min(1),
|
|
6682
7549
|
parseEnv: idEnv
|
|
6683
7550
|
},
|
|
6684
7551
|
{
|
|
@@ -6686,14 +7553,21 @@ var init_settings = __esm(() => {
|
|
|
6686
7553
|
configKey: "mainBranch",
|
|
6687
7554
|
envVar: "RUNCASTLE_MAIN_BRANCH",
|
|
6688
7555
|
restartRequired: false,
|
|
6689
|
-
valueSchema:
|
|
7556
|
+
valueSchema: z10.string().min(1),
|
|
6690
7557
|
parseEnv: idEnv
|
|
6691
7558
|
},
|
|
6692
7559
|
{
|
|
6693
7560
|
key: "devCommand",
|
|
6694
7561
|
projectColumn: "devCommand",
|
|
6695
7562
|
restartRequired: false,
|
|
6696
|
-
valueSchema:
|
|
7563
|
+
valueSchema: z10.string().min(1),
|
|
7564
|
+
parseEnv: idEnv
|
|
7565
|
+
},
|
|
7566
|
+
{
|
|
7567
|
+
key: "dbResetCommand",
|
|
7568
|
+
projectColumn: "dbResetCommand",
|
|
7569
|
+
restartRequired: false,
|
|
7570
|
+
valueSchema: z10.string().min(1),
|
|
6697
7571
|
parseEnv: idEnv
|
|
6698
7572
|
}
|
|
6699
7573
|
];
|
|
@@ -6702,14 +7576,14 @@ var init_settings = __esm(() => {
|
|
|
6702
7576
|
});
|
|
6703
7577
|
|
|
6704
7578
|
// src/trpc/routers/settings.ts
|
|
6705
|
-
import * as
|
|
7579
|
+
import * as z11 from "zod";
|
|
6706
7580
|
var settingsRouter;
|
|
6707
7581
|
var init_settings2 = __esm(() => {
|
|
6708
7582
|
init_src();
|
|
6709
7583
|
init_settings();
|
|
6710
7584
|
init_context();
|
|
6711
7585
|
settingsRouter = router({
|
|
6712
|
-
get: publicProcedure.input(
|
|
7586
|
+
get: publicProcedure.input(z11.object({ projectId: z11.string().optional() }).optional()).query(({ ctx, input }) => getSettings(ctx, input?.projectId)),
|
|
6713
7587
|
update: publicProcedure.input(SettingsUpdateInput).mutation(({ ctx, input }) => updateSettings(ctx, input))
|
|
6714
7588
|
});
|
|
6715
7589
|
});
|
|
@@ -6902,9 +7776,9 @@ var init_system_exec = __esm(() => {
|
|
|
6902
7776
|
});
|
|
6903
7777
|
|
|
6904
7778
|
// src/services/setup.ts
|
|
6905
|
-
import { copyFileSync, existsSync as
|
|
6906
|
-
import { dirname as
|
|
6907
|
-
import { fileURLToPath as
|
|
7779
|
+
import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
7780
|
+
import { dirname as dirname9, join as join17 } from "path";
|
|
7781
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
6908
7782
|
async function writeGitIdentity(exec, identity) {
|
|
6909
7783
|
const name = identity.name.trim();
|
|
6910
7784
|
const email = identity.email.trim();
|
|
@@ -6979,16 +7853,16 @@ async function resolveRuntime(exec, preferred) {
|
|
|
6979
7853
|
function resolveSandcastleBin() {
|
|
6980
7854
|
try {
|
|
6981
7855
|
const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
|
|
6982
|
-
let dir =
|
|
7856
|
+
let dir = dirname9(fileURLToPath7(mainUrl));
|
|
6983
7857
|
for (let hops = 0;hops < 6; hops++) {
|
|
6984
|
-
const manifestPath =
|
|
6985
|
-
if (
|
|
6986
|
-
const manifest = JSON.parse(
|
|
7858
|
+
const manifestPath = join17(dir, "package.json");
|
|
7859
|
+
if (existsSync18(manifestPath)) {
|
|
7860
|
+
const manifest = JSON.parse(readFileSync8(manifestPath, "utf8"));
|
|
6987
7861
|
if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
|
|
6988
|
-
return
|
|
7862
|
+
return join17(dir, manifest.bin.sandcastle);
|
|
6989
7863
|
}
|
|
6990
7864
|
}
|
|
6991
|
-
const parent =
|
|
7865
|
+
const parent = dirname9(dir);
|
|
6992
7866
|
if (parent === dir)
|
|
6993
7867
|
break;
|
|
6994
7868
|
dir = parent;
|
|
@@ -7010,21 +7884,21 @@ function terminalSpec(kind, opts) {
|
|
|
7010
7884
|
};
|
|
7011
7885
|
}
|
|
7012
7886
|
function sandcastleTemplateDir() {
|
|
7013
|
-
return resolveAsset(ASSET_ENV.sandcastleTemplate,
|
|
7887
|
+
return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath7(new URL("../assets/sandcastle", import.meta.url)));
|
|
7014
7888
|
}
|
|
7015
7889
|
function scaffoldSandcastleConfig(templateDir, targetDir) {
|
|
7016
|
-
const dir =
|
|
7017
|
-
if (
|
|
7890
|
+
const dir = join17(targetDir, ".sandcastle");
|
|
7891
|
+
if (existsSync18(dir))
|
|
7018
7892
|
return { scaffolded: false, dir };
|
|
7019
|
-
|
|
7893
|
+
mkdirSync9(dir, { recursive: true });
|
|
7020
7894
|
for (const name of readdirSync6(templateDir)) {
|
|
7021
|
-
copyFileSync(
|
|
7895
|
+
copyFileSync(join17(templateDir, name), join17(dir, name));
|
|
7022
7896
|
}
|
|
7023
7897
|
return { scaffolded: true, dir };
|
|
7024
7898
|
}
|
|
7025
7899
|
function prepareSandboxBuildContext() {
|
|
7026
7900
|
const target = sandboxBuildDir();
|
|
7027
|
-
|
|
7901
|
+
mkdirSync9(target, { recursive: true });
|
|
7028
7902
|
scaffoldSandcastleConfig(sandcastleTemplateDir(), target);
|
|
7029
7903
|
return target;
|
|
7030
7904
|
}
|
|
@@ -7060,9 +7934,9 @@ function createTokenVerifier(exec) {
|
|
|
7060
7934
|
function fileAfkTokenIo(exec) {
|
|
7061
7935
|
const path = envPath();
|
|
7062
7936
|
return {
|
|
7063
|
-
read: () =>
|
|
7937
|
+
read: () => existsSync18(path) ? readFileSync8(path, "utf8") : "",
|
|
7064
7938
|
write: (content) => {
|
|
7065
|
-
|
|
7939
|
+
mkdirSync9(dirname9(path), { recursive: true });
|
|
7066
7940
|
writeFileSync4(path, content, "utf8");
|
|
7067
7941
|
},
|
|
7068
7942
|
verify: createTokenVerifier(exec)
|
|
@@ -7077,7 +7951,7 @@ var init_setup = __esm(() => {
|
|
|
7077
7951
|
});
|
|
7078
7952
|
|
|
7079
7953
|
// src/trpc/routers/setup.ts
|
|
7080
|
-
import * as
|
|
7954
|
+
import * as z12 from "zod";
|
|
7081
7955
|
var setupRouter;
|
|
7082
7956
|
var init_setup2 = __esm(() => {
|
|
7083
7957
|
init_src();
|
|
@@ -7093,9 +7967,9 @@ var init_setup2 = __esm(() => {
|
|
|
7093
7967
|
...ctx.config.sandboxImage ? { imageName: ctx.config.sandboxImage } : {}
|
|
7094
7968
|
})),
|
|
7095
7969
|
runtimeGuide: publicProcedure.query(() => runtimeInstallGuide(process.platform)),
|
|
7096
|
-
gitIdentity: publicProcedure.input(
|
|
7097
|
-
afkToken: publicProcedure.input(
|
|
7098
|
-
startTerminal: publicProcedure.input(
|
|
7970
|
+
gitIdentity: publicProcedure.input(z12.object({ name: z12.string(), email: z12.string() })).mutation(({ input }) => writeGitIdentity(createSystemExec(), input)),
|
|
7971
|
+
afkToken: publicProcedure.input(z12.object({ token: z12.string() })).mutation(({ input }) => saveAfkToken(fileAfkTokenIo(createSystemExec()), input.token)),
|
|
7972
|
+
startTerminal: publicProcedure.input(z12.object({ kind: z12.enum(["setup-token", "build-image"]) })).mutation(async ({ ctx, input }) => {
|
|
7099
7973
|
const exec = createSystemExec();
|
|
7100
7974
|
const preferred = ctx.config.sandbox === "podman" ? "podman" : "docker";
|
|
7101
7975
|
const runtime = await resolveRuntime(exec, preferred);
|
|
@@ -7195,16 +8069,16 @@ var init_update_check = __esm(() => {
|
|
|
7195
8069
|
});
|
|
7196
8070
|
|
|
7197
8071
|
// src/version.ts
|
|
7198
|
-
import { readFileSync as
|
|
7199
|
-
import { dirname as
|
|
7200
|
-
import { fileURLToPath as
|
|
8072
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
8073
|
+
import { dirname as dirname10, join as join18 } from "path";
|
|
8074
|
+
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
7201
8075
|
function runcastleVersion() {
|
|
7202
8076
|
if (cached2)
|
|
7203
8077
|
return cached2;
|
|
7204
|
-
const here =
|
|
7205
|
-
for (const pkgPath of [
|
|
8078
|
+
const here = dirname10(fileURLToPath8(import.meta.url));
|
|
8079
|
+
for (const pkgPath of [join18(here, "..", "package.json"), join18(here, "package.json")]) {
|
|
7206
8080
|
try {
|
|
7207
|
-
const pkg = JSON.parse(
|
|
8081
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
7208
8082
|
if (typeof pkg.version === "string") {
|
|
7209
8083
|
cached2 = pkg.version;
|
|
7210
8084
|
return cached2;
|
|
@@ -7230,7 +8104,7 @@ var init_system = __esm(() => {
|
|
|
7230
8104
|
});
|
|
7231
8105
|
|
|
7232
8106
|
// src/trpc/routers/ticket.ts
|
|
7233
|
-
import * as
|
|
8107
|
+
import * as z13 from "zod";
|
|
7234
8108
|
var ticketRouter;
|
|
7235
8109
|
var init_ticket = __esm(() => {
|
|
7236
8110
|
init_features();
|
|
@@ -7239,8 +8113,8 @@ var init_ticket = __esm(() => {
|
|
|
7239
8113
|
init_ticket_burner();
|
|
7240
8114
|
init_context();
|
|
7241
8115
|
ticketRouter = router({
|
|
7242
|
-
retry: publicProcedure.input(
|
|
7243
|
-
stop: publicProcedure.input(
|
|
8116
|
+
retry: publicProcedure.input(z13.object({ ticketId: z13.string(), fresh: z13.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
|
|
8117
|
+
stop: publicProcedure.input(z13.object({ ticketId: z13.string() })).mutation(({ ctx, input }) => {
|
|
7244
8118
|
if (stopTicketRun(input.ticketId))
|
|
7245
8119
|
return { stopped: true, swept: false };
|
|
7246
8120
|
const ticket = getTicket(ctx, input.ticketId);
|
|
@@ -7250,7 +8124,7 @@ var init_ticket = __esm(() => {
|
|
|
7250
8124
|
}
|
|
7251
8125
|
return { stopped: false, swept: false };
|
|
7252
8126
|
}),
|
|
7253
|
-
cancel: publicProcedure.input(
|
|
8127
|
+
cancel: publicProcedure.input(z13.object({ ticketId: z13.string(), reason: z13.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason))
|
|
7254
8128
|
});
|
|
7255
8129
|
});
|
|
7256
8130
|
|
|
@@ -7281,18 +8155,18 @@ var init_router = __esm(() => {
|
|
|
7281
8155
|
});
|
|
7282
8156
|
|
|
7283
8157
|
// src/workflows/reconcile-runs.ts
|
|
7284
|
-
import { eq as
|
|
8158
|
+
import { eq as eq13 } from "drizzle-orm";
|
|
7285
8159
|
async function reconcileStaleRuns(ctx) {
|
|
7286
|
-
const stale = ctx.db.select().from(runs).where(
|
|
8160
|
+
const stale = ctx.db.select().from(runs).where(eq13(runs.status, "running")).all().map(rowToRun);
|
|
7287
8161
|
const reconciled = [];
|
|
7288
|
-
for (const
|
|
7289
|
-
if (isRunActive(
|
|
8162
|
+
for (const run4 of stale) {
|
|
8163
|
+
if (isRunActive(run4.id))
|
|
7290
8164
|
continue;
|
|
7291
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(
|
|
7292
|
-
const released = releaseForSession(ctx,
|
|
7293
|
-
const swept = workflowClaimsFeatureBranch(
|
|
7294
|
-
if (workflowClaimsFeatureBranch(
|
|
7295
|
-
const feature = tryGetFeature(ctx,
|
|
8165
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq13(runs.id, run4.id)).run();
|
|
8166
|
+
const released = releaseForSession(ctx, run4.id);
|
|
8167
|
+
const swept = workflowClaimsFeatureBranch(run4.workflow) ? sweepOrphanedBurning(ctx, run4.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
|
|
8168
|
+
if (workflowClaimsFeatureBranch(run4.workflow)) {
|
|
8169
|
+
const feature = tryGetFeature(ctx, run4.featureId);
|
|
7296
8170
|
const project = feature ? getProjectById(ctx, feature.projectId) : null;
|
|
7297
8171
|
if (feature && project) {
|
|
7298
8172
|
try {
|
|
@@ -7300,18 +8174,18 @@ async function reconcileStaleRuns(ctx) {
|
|
|
7300
8174
|
} catch {}
|
|
7301
8175
|
}
|
|
7302
8176
|
}
|
|
7303
|
-
emit(ctx,
|
|
8177
|
+
emit(ctx, run4.featureId, {
|
|
7304
8178
|
type: "run.reconciled",
|
|
7305
|
-
message: `run marked failed at boot \u2014 the server restarted while it was running (${
|
|
7306
|
-
runId:
|
|
8179
|
+
message: `run marked failed at boot \u2014 the server restarted while it was running (${run4.workflow})`,
|
|
8180
|
+
runId: run4.id,
|
|
7307
8181
|
data: {
|
|
7308
|
-
runId:
|
|
7309
|
-
workflow:
|
|
8182
|
+
runId: run4.id,
|
|
8183
|
+
workflow: run4.workflow,
|
|
7310
8184
|
releasedWaypointIds: released.map((w) => w.id),
|
|
7311
8185
|
sweptTicketSeqs: swept.map((t2) => t2.seq)
|
|
7312
8186
|
}
|
|
7313
8187
|
});
|
|
7314
|
-
reconciled.push(
|
|
8188
|
+
reconciled.push(run4);
|
|
7315
8189
|
}
|
|
7316
8190
|
for (const project of allProjects(ctx)) {
|
|
7317
8191
|
try {
|
|
@@ -7374,6 +8248,10 @@ async function startServer() {
|
|
|
7374
8248
|
if (staleRuns.length > 0) {
|
|
7375
8249
|
console.log(`reconciled ${staleRuns.length} stale run(s) from a previous server run`);
|
|
7376
8250
|
}
|
|
8251
|
+
const stalePreps = reconcilePreps(ctx);
|
|
8252
|
+
if (stalePreps > 0) {
|
|
8253
|
+
console.log(`reconciled ${stalePreps} stale preparation run(s) from a previous server run`);
|
|
8254
|
+
}
|
|
7377
8255
|
const server = Bun.serve({
|
|
7378
8256
|
port: config2.serverPort,
|
|
7379
8257
|
fetch(req, srv) {
|
|
@@ -7403,6 +8281,7 @@ var init_src2 = __esm(() => {
|
|
|
7403
8281
|
init_client();
|
|
7404
8282
|
init_migrate();
|
|
7405
8283
|
init_reconcile();
|
|
8284
|
+
init_prep();
|
|
7406
8285
|
init_server();
|
|
7407
8286
|
init_registry();
|
|
7408
8287
|
init_ws();
|