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