runcastle 1.2.10 → 1.2.11
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 +1159 -252
- package/drizzle/0033_robust_saracen.sql +20 -0
- package/drizzle/meta/0033_snapshot.json +1078 -0
- package/drizzle/meta/_journal.json +7 -0
- package/index.js +1156 -250
- package/package.json +1 -1
- package/skills/burner/implement-ticket.md +4 -1
- package/skills/burner/review-ticket.md +101 -63
- package/skills/packs/runcastle/skills/tickets/SKILL.md +4 -2
- package/web/assets/index-CcFZnrux.js +143 -0
- package/web/assets/index-jOHWTE5S.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-BCAA3_oq.css +0 -1
- package/web/assets/index-BlVLhZP1.js +0 -142
package/index.js
CHANGED
|
@@ -48,6 +48,12 @@ import * as z from "zod";
|
|
|
48
48
|
function resolveSandboxImage(config) {
|
|
49
49
|
return config.sandboxImage ?? DEFAULT_SANDBOX_IMAGE;
|
|
50
50
|
}
|
|
51
|
+
function resolveBurnCacheMode(config) {
|
|
52
|
+
return BURN_CACHE_SANDBOXES.includes(config.sandbox) ? config.burnCache : "off";
|
|
53
|
+
}
|
|
54
|
+
function resolveDefaultBurnConcurrency(logicalCpus) {
|
|
55
|
+
return Number.isFinite(logicalCpus) && logicalCpus > SMALL_HOST_LOGICAL_CPUS ? 3 : 1;
|
|
56
|
+
}
|
|
51
57
|
function resolvePreparedSettings(config, project) {
|
|
52
58
|
const pick = (a, b) => {
|
|
53
59
|
const own = a?.trim();
|
|
@@ -99,7 +105,7 @@ var MODEL_STEPS, ModelStep, AGENT_RUNTIMES, AgentRuntime, DEFAULT_RUNTIME = "cla
|
|
|
99
105
|
}
|
|
100
106
|
delete obj.smokeModel;
|
|
101
107
|
return obj;
|
|
102
|
-
}, RuncastleConfig, DEFAULT_SANDBOX_IMAGE = "sandcastle:runcastle";
|
|
108
|
+
}, RuncastleConfig, DEFAULT_SANDBOX_IMAGE = "sandcastle:runcastle", BURN_CACHE_SANDBOXES, SMALL_HOST_LOGICAL_CPUS = 8;
|
|
103
109
|
var init_config = __esm(() => {
|
|
104
110
|
MODEL_STEPS = [
|
|
105
111
|
"ideation",
|
|
@@ -109,6 +115,7 @@ var init_config = __esm(() => {
|
|
|
109
115
|
"revisit",
|
|
110
116
|
"research",
|
|
111
117
|
"implement",
|
|
118
|
+
"review",
|
|
112
119
|
"prepare",
|
|
113
120
|
"project",
|
|
114
121
|
"smoke"
|
|
@@ -156,8 +163,10 @@ var init_config = __esm(() => {
|
|
|
156
163
|
setupCommand: z.string().optional(),
|
|
157
164
|
verifyCommands: z.string().optional(),
|
|
158
165
|
knownFailures: z.string().optional(),
|
|
159
|
-
burnWorkspace: z.enum(["auto", "mounted", "isolated"]).default("auto")
|
|
166
|
+
burnWorkspace: z.enum(["auto", "mounted", "isolated"]).default("auto"),
|
|
167
|
+
burnCache: z.enum(["volume", "off"]).default("volume")
|
|
160
168
|
}));
|
|
169
|
+
BURN_CACHE_SANDBOXES = ["docker", "podman"];
|
|
161
170
|
});
|
|
162
171
|
|
|
163
172
|
// ../core/src/schemas.ts
|
|
@@ -165,7 +174,7 @@ import * as z2 from "zod";
|
|
|
165
174
|
function isProjectSessionKind(kind) {
|
|
166
175
|
return PROJECT_SESSION_KINDS.includes(kind);
|
|
167
176
|
}
|
|
168
|
-
var Phase, TicketStatus, SessionKind, PROJECT_SESSION_KINDS, SessionPurpose, MergeBranchPair, RunStatus, FeatureStatus, SessionStatus, TicketKind, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNoteAuthor, TestNote, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
177
|
+
var Phase, TicketStatus, SessionKind, PROJECT_SESSION_KINDS, SessionPurpose, MergeBranchPair, RunStatus, FeatureStatus, SessionStatus, TicketKind, FindingKind, FindingSeverity, FindingStatus, FindingOpenReason, ReviewFindingInput, ReviewFinding, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNoteAuthor, TestNote, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
169
178
|
var init_schemas = __esm(() => {
|
|
170
179
|
init_config();
|
|
171
180
|
Phase = z2.enum([
|
|
@@ -197,6 +206,35 @@ var init_schemas = __esm(() => {
|
|
|
197
206
|
FeatureStatus = z2.enum(["draft", "active", "shipped", "archived"]);
|
|
198
207
|
SessionStatus = z2.enum(["launching", "live", "ended"]);
|
|
199
208
|
TicketKind = z2.enum(["implementation", "review"]);
|
|
209
|
+
FindingKind = z2.enum(["defect", "observation"]);
|
|
210
|
+
FindingSeverity = z2.enum(["high", "medium", "low"]);
|
|
211
|
+
FindingStatus = z2.enum(["open", "fixing", "fixed", "failed", "dismissed"]);
|
|
212
|
+
FindingOpenReason = z2.enum(["over-cap", "fix-failed"]);
|
|
213
|
+
ReviewFindingInput = z2.object({
|
|
214
|
+
kind: FindingKind,
|
|
215
|
+
severity: FindingSeverity,
|
|
216
|
+
title: z2.string().min(1),
|
|
217
|
+
location: z2.string().min(1),
|
|
218
|
+
citation: z2.string().min(1),
|
|
219
|
+
detail: z2.string().min(1),
|
|
220
|
+
reproStep: z2.string().optional()
|
|
221
|
+
}).superRefine((finding, ctx) => {
|
|
222
|
+
if (finding.kind === "defect" && !finding.reproStep?.trim()) {
|
|
223
|
+
ctx.addIssue({ code: "custom", path: ["reproStep"], message: "defects require a repro step" });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
ReviewFinding = ReviewFindingInput.safeExtend({
|
|
227
|
+
id: z2.string(),
|
|
228
|
+
featureId: z2.string(),
|
|
229
|
+
lap: z2.number(),
|
|
230
|
+
reviewTicketId: z2.string(),
|
|
231
|
+
reproStep: z2.string(),
|
|
232
|
+
status: FindingStatus,
|
|
233
|
+
openReason: FindingOpenReason.nullable(),
|
|
234
|
+
failureReason: z2.string().nullable(),
|
|
235
|
+
fixTicketId: z2.string().nullable(),
|
|
236
|
+
createdAt: z2.number()
|
|
237
|
+
});
|
|
200
238
|
TicketInput = z2.object({
|
|
201
239
|
title: z2.string(),
|
|
202
240
|
goal: z2.string(),
|
|
@@ -205,7 +243,8 @@ var init_schemas = __esm(() => {
|
|
|
205
243
|
seams: z2.array(z2.string()),
|
|
206
244
|
blockedBy: z2.array(z2.number()),
|
|
207
245
|
kind: TicketKind.default("implementation"),
|
|
208
|
-
model: z2.string().optional()
|
|
246
|
+
model: z2.string().optional(),
|
|
247
|
+
originFindingId: z2.string().optional()
|
|
209
248
|
});
|
|
210
249
|
Ticket = TicketInput.extend({
|
|
211
250
|
id: z2.string(),
|
|
@@ -536,7 +575,7 @@ var init_pipeline = __esm(() => {
|
|
|
536
575
|
|
|
537
576
|
// ../core/src/db-schema.ts
|
|
538
577
|
import { index, integer, primaryKey, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
539
|
-
var projects, projectFindings, features, sessions, tickets, testNotes, waypoints, runs, events, gateOverrides;
|
|
578
|
+
var projects, projectFindings, features, sessions, tickets, reviewFindings, testNotes, waypoints, runs, events, gateOverrides;
|
|
540
579
|
var init_db_schema = __esm(() => {
|
|
541
580
|
projects = sqliteTable("projects", {
|
|
542
581
|
id: text("id").primaryKey(),
|
|
@@ -615,7 +654,26 @@ var init_db_schema = __esm(() => {
|
|
|
615
654
|
error: text("error"),
|
|
616
655
|
attemptBranch: text("attempt_branch"),
|
|
617
656
|
conflictFiles: text("conflict_files", { mode: "json" }).$type(),
|
|
618
|
-
digest: text("digest")
|
|
657
|
+
digest: text("digest"),
|
|
658
|
+
originFindingId: text("origin_finding_id")
|
|
659
|
+
});
|
|
660
|
+
reviewFindings = sqliteTable("review_findings", {
|
|
661
|
+
id: text("id").primaryKey(),
|
|
662
|
+
featureId: text("feature_id").notNull(),
|
|
663
|
+
lap: integer("lap").notNull(),
|
|
664
|
+
reviewTicketId: text("review_ticket_id").notNull(),
|
|
665
|
+
kind: text("kind").notNull().$type(),
|
|
666
|
+
severity: text("severity").notNull().$type(),
|
|
667
|
+
title: text("title").notNull(),
|
|
668
|
+
location: text("location").notNull(),
|
|
669
|
+
citation: text("citation").notNull(),
|
|
670
|
+
detail: text("detail").notNull(),
|
|
671
|
+
reproStep: text("repro_step").notNull(),
|
|
672
|
+
status: text("status").notNull().$type(),
|
|
673
|
+
openReason: text("open_reason").$type(),
|
|
674
|
+
failureReason: text("failure_reason"),
|
|
675
|
+
fixTicketId: text("fix_ticket_id"),
|
|
676
|
+
createdAt: integer("created_at").notNull()
|
|
619
677
|
});
|
|
620
678
|
testNotes = sqliteTable("test_notes", {
|
|
621
679
|
id: text("id").primaryKey(),
|
|
@@ -788,10 +846,12 @@ var init_paths = () => {};
|
|
|
788
846
|
// ../core/src/config-load.ts
|
|
789
847
|
var exports_config_load = {};
|
|
790
848
|
__export(exports_config_load, {
|
|
791
|
-
loadConfig: () => loadConfig
|
|
849
|
+
loadConfig: () => loadConfig,
|
|
850
|
+
hostLogicalCpus: () => hostLogicalCpus
|
|
792
851
|
});
|
|
793
852
|
import { existsSync, readFileSync } from "fs";
|
|
794
|
-
|
|
853
|
+
import { availableParallelism } from "os";
|
|
854
|
+
function loadConfig(env = process.env, logicalCpus = hostLogicalCpus()) {
|
|
795
855
|
let fileConfig = {};
|
|
796
856
|
const path = configPath();
|
|
797
857
|
if (existsSync(path)) {
|
|
@@ -833,8 +893,17 @@ function loadConfig(env = process.env) {
|
|
|
833
893
|
overrides.knownFailures = env.RUNCASTLE_KNOWN_FAILURES;
|
|
834
894
|
if (env.RUNCASTLE_BURN_WORKSPACE)
|
|
835
895
|
overrides.burnWorkspace = env.RUNCASTLE_BURN_WORKSPACE;
|
|
896
|
+
if (env.RUNCASTLE_BURN_CACHE)
|
|
897
|
+
overrides.burnCache = env.RUNCASTLE_BURN_CACHE;
|
|
836
898
|
const base = typeof fileConfig === "object" && fileConfig !== null ? fileConfig : {};
|
|
837
|
-
|
|
899
|
+
const merged = { ...base, ...overrides };
|
|
900
|
+
if (merged.burnConcurrency === undefined) {
|
|
901
|
+
merged.burnConcurrency = resolveDefaultBurnConcurrency(logicalCpus);
|
|
902
|
+
}
|
|
903
|
+
return RuncastleConfig.parse(merged);
|
|
904
|
+
}
|
|
905
|
+
function hostLogicalCpus() {
|
|
906
|
+
return availableParallelism();
|
|
838
907
|
}
|
|
839
908
|
var init_config_load = __esm(() => {
|
|
840
909
|
init_config();
|
|
@@ -863,6 +932,7 @@ var init_schema = __esm(() => {
|
|
|
863
932
|
init_src();
|
|
864
933
|
schema = {
|
|
865
934
|
projects,
|
|
935
|
+
reviewFindings,
|
|
866
936
|
features,
|
|
867
937
|
sessions,
|
|
868
938
|
tickets,
|
|
@@ -1571,6 +1641,198 @@ var init_registry = __esm(() => {
|
|
|
1571
1641
|
REGISTRY_KEY = Symbol.for("runcastle.pty.registry");
|
|
1572
1642
|
});
|
|
1573
1643
|
|
|
1644
|
+
// src/workflows/burn-cache.ts
|
|
1645
|
+
function burnCacheVolumeName(projectId) {
|
|
1646
|
+
return `runcastle-${projectId}`;
|
|
1647
|
+
}
|
|
1648
|
+
function slotRepoPath(slot) {
|
|
1649
|
+
return `${slotDirPath(slot)}/repo`;
|
|
1650
|
+
}
|
|
1651
|
+
function slotDirPath(slot) {
|
|
1652
|
+
return `${BURN_CACHE_MOUNT}/slots/${slot}`;
|
|
1653
|
+
}
|
|
1654
|
+
function slotStampPath(slot) {
|
|
1655
|
+
return `${slotDirPath(slot)}/.runcastle-stamp`;
|
|
1656
|
+
}
|
|
1657
|
+
function storePath(pm) {
|
|
1658
|
+
return `${BURN_CACHE_MOUNT}/store/${pm}`;
|
|
1659
|
+
}
|
|
1660
|
+
function burnCacheEnv(pm) {
|
|
1661
|
+
const store = storePath(pm);
|
|
1662
|
+
return {
|
|
1663
|
+
npm_config_store_dir: store,
|
|
1664
|
+
pnpm_config_store_dir: store,
|
|
1665
|
+
BUN_INSTALL_CACHE_DIR: store,
|
|
1666
|
+
npm_config_cache: store,
|
|
1667
|
+
YARN_GLOBAL_FOLDER: store,
|
|
1668
|
+
TMPDIR: `${BURN_CACHE_MOUNT}/tmp`,
|
|
1669
|
+
NODE_COMPILE_CACHE: `${BURN_CACHE_MOUNT}/node-compile`
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
async function execOrThrow(exec, engine, args) {
|
|
1673
|
+
const out = await exec(engine, args);
|
|
1674
|
+
if (!out.ok || out.code !== 0) {
|
|
1675
|
+
throw new Error(`${engine} ${args.join(" ")} failed: ${out.stderr.trim() || `exit ${out.code}`}`);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
async function ensureBurnCacheVolume(opts) {
|
|
1679
|
+
const { engine, exec } = opts;
|
|
1680
|
+
const name = burnCacheVolumeName(opts.projectId);
|
|
1681
|
+
const inspect = await exec(engine, ["volume", "inspect", name]);
|
|
1682
|
+
const existed = inspect.ok && inspect.code === 0;
|
|
1683
|
+
await execOrThrow(exec, engine, ["volume", "create", name]);
|
|
1684
|
+
if (existed)
|
|
1685
|
+
return;
|
|
1686
|
+
await execOrThrow(exec, engine, [
|
|
1687
|
+
"run",
|
|
1688
|
+
"--rm",
|
|
1689
|
+
"--user",
|
|
1690
|
+
"root",
|
|
1691
|
+
"-v",
|
|
1692
|
+
`${name}:${BURN_CACHE_MOUNT}`,
|
|
1693
|
+
opts.imageName,
|
|
1694
|
+
"chown",
|
|
1695
|
+
"-R",
|
|
1696
|
+
CONTAINER_USER,
|
|
1697
|
+
BURN_CACHE_MOUNT
|
|
1698
|
+
]);
|
|
1699
|
+
}
|
|
1700
|
+
async function removeBurnCacheVolume(opts) {
|
|
1701
|
+
if (opts.slots.length > 0)
|
|
1702
|
+
throw new BurnCacheBusyError([...opts.slots].sort((a, b) => a - b));
|
|
1703
|
+
await execOrThrow(opts.exec, opts.engine, ["volume", "rm", burnCacheVolumeName(opts.projectId)]);
|
|
1704
|
+
}
|
|
1705
|
+
async function burnCacheVolumeSize(opts) {
|
|
1706
|
+
const out = await opts.exec(opts.engine, ["system", "df", "-v", "--format", "json"]);
|
|
1707
|
+
if (!out.ok || out.code !== 0)
|
|
1708
|
+
return null;
|
|
1709
|
+
return findVolumeSize(out.stdout, burnCacheVolumeName(opts.projectId));
|
|
1710
|
+
}
|
|
1711
|
+
function parseJsonValues(stdout) {
|
|
1712
|
+
const values = [];
|
|
1713
|
+
const whole = tryParseJson(stdout);
|
|
1714
|
+
if (whole !== undefined)
|
|
1715
|
+
return [whole];
|
|
1716
|
+
for (const line of stdout.split(`
|
|
1717
|
+
`)) {
|
|
1718
|
+
const value = tryParseJson(line);
|
|
1719
|
+
if (value !== undefined)
|
|
1720
|
+
values.push(value);
|
|
1721
|
+
}
|
|
1722
|
+
return values;
|
|
1723
|
+
}
|
|
1724
|
+
function tryParseJson(text2) {
|
|
1725
|
+
const trimmed = text2.trim();
|
|
1726
|
+
if (!trimmed)
|
|
1727
|
+
return;
|
|
1728
|
+
try {
|
|
1729
|
+
return JSON.parse(trimmed);
|
|
1730
|
+
} catch {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
function findVolumeSize(stdout, name) {
|
|
1735
|
+
const pending = parseJsonValues(stdout);
|
|
1736
|
+
while (pending.length > 0) {
|
|
1737
|
+
const node = pending.shift();
|
|
1738
|
+
if (Array.isArray(node)) {
|
|
1739
|
+
pending.push(...node);
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
if (typeof node !== "object" || node === null)
|
|
1743
|
+
continue;
|
|
1744
|
+
const record = node;
|
|
1745
|
+
if (VOLUME_NAME_KEYS.some((key) => record[key] === name)) {
|
|
1746
|
+
const size = readSize(record);
|
|
1747
|
+
if (size !== null)
|
|
1748
|
+
return size;
|
|
1749
|
+
}
|
|
1750
|
+
pending.push(...Object.values(record));
|
|
1751
|
+
}
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1754
|
+
function readSize(record) {
|
|
1755
|
+
const usage = record.UsageData;
|
|
1756
|
+
const nested = typeof usage === "object" && usage !== null ? usage.Size : undefined;
|
|
1757
|
+
return parseSize(record.Size) ?? parseSize(nested);
|
|
1758
|
+
}
|
|
1759
|
+
function parseSize(value) {
|
|
1760
|
+
if (typeof value === "number")
|
|
1761
|
+
return Number.isFinite(value) ? value : null;
|
|
1762
|
+
if (typeof value !== "string")
|
|
1763
|
+
return null;
|
|
1764
|
+
const match = /^\s*(-?[\d.]+)\s*([a-z]*)\s*$/i.exec(value);
|
|
1765
|
+
if (!match)
|
|
1766
|
+
return null;
|
|
1767
|
+
const amount = Number(match[1]);
|
|
1768
|
+
const unit = SIZE_UNITS[(match[2] || "b").toLowerCase()];
|
|
1769
|
+
if (!Number.isFinite(amount) || unit === undefined)
|
|
1770
|
+
return null;
|
|
1771
|
+
return Math.round(amount * unit);
|
|
1772
|
+
}
|
|
1773
|
+
function createSlotAllocator(capacity) {
|
|
1774
|
+
const heldSlots = new Set;
|
|
1775
|
+
let width = capacity;
|
|
1776
|
+
return {
|
|
1777
|
+
claim() {
|
|
1778
|
+
for (let slot = 1;slot <= width; slot++) {
|
|
1779
|
+
if (!heldSlots.has(slot)) {
|
|
1780
|
+
heldSlots.add(slot);
|
|
1781
|
+
return slot;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
throw new BurnSlotsExhaustedError(width);
|
|
1785
|
+
},
|
|
1786
|
+
release(slot) {
|
|
1787
|
+
heldSlots.delete(slot);
|
|
1788
|
+
},
|
|
1789
|
+
held() {
|
|
1790
|
+
return [...heldSlots].sort((a, b) => a - b);
|
|
1791
|
+
},
|
|
1792
|
+
resize(next) {
|
|
1793
|
+
width = next;
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
function getBurnSlotAllocator(capacity) {
|
|
1798
|
+
if (!sharedAllocator)
|
|
1799
|
+
sharedAllocator = createSlotAllocator(capacity);
|
|
1800
|
+
else
|
|
1801
|
+
sharedAllocator.resize(capacity);
|
|
1802
|
+
return sharedAllocator;
|
|
1803
|
+
}
|
|
1804
|
+
var BURN_CACHE_MOUNT = "/home/agent/cache", BurnCacheBusyError, CONTAINER_USER = "1000:1000", VOLUME_NAME_KEYS, SIZE_UNITS, BurnSlotsExhaustedError, sharedAllocator;
|
|
1805
|
+
var init_burn_cache = __esm(() => {
|
|
1806
|
+
BurnCacheBusyError = class BurnCacheBusyError extends Error {
|
|
1807
|
+
slots;
|
|
1808
|
+
constructor(slots) {
|
|
1809
|
+
super(`burn cache is in use \u2014 slots ${slots.join(", ")} are held; stop those burns before clearing it`);
|
|
1810
|
+
this.name = "BurnCacheBusyError";
|
|
1811
|
+
this.slots = slots;
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
VOLUME_NAME_KEYS = ["Name", "VolumeName"];
|
|
1815
|
+
SIZE_UNITS = {
|
|
1816
|
+
b: 1,
|
|
1817
|
+
kb: 1000,
|
|
1818
|
+
mb: 1e6,
|
|
1819
|
+
gb: 1e9,
|
|
1820
|
+
tb: 1000000000000,
|
|
1821
|
+
pb: 1000000000000000,
|
|
1822
|
+
kib: 1024,
|
|
1823
|
+
mib: 1024 ** 2,
|
|
1824
|
+
gib: 1024 ** 3,
|
|
1825
|
+
tib: 1024 ** 4,
|
|
1826
|
+
pib: 1024 ** 5
|
|
1827
|
+
};
|
|
1828
|
+
BurnSlotsExhaustedError = class BurnSlotsExhaustedError extends Error {
|
|
1829
|
+
constructor(capacity) {
|
|
1830
|
+
super(`all ${capacity} burn cache slots are in use`);
|
|
1831
|
+
this.name = "BurnSlotsExhaustedError";
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1574
1836
|
// src/errors.ts
|
|
1575
1837
|
import { TRPCError } from "@trpc/server";
|
|
1576
1838
|
function isNotImplemented(e) {
|
|
@@ -1585,6 +1847,8 @@ function toTRPCError(e) {
|
|
|
1585
1847
|
return new TRPCError({ code: "NOT_FOUND", message: e.message, cause: e });
|
|
1586
1848
|
if (e instanceof GateError)
|
|
1587
1849
|
return new TRPCError({ code: "PRECONDITION_FAILED", message: e.message, cause: e });
|
|
1850
|
+
if (e instanceof BurnCacheBusyError)
|
|
1851
|
+
return new TRPCError({ code: "PRECONDITION_FAILED", message: e.message, cause: e });
|
|
1588
1852
|
if (e instanceof InvalidInputError)
|
|
1589
1853
|
return new TRPCError({ code: "BAD_REQUEST", message: e.message, cause: e });
|
|
1590
1854
|
if (e instanceof Error)
|
|
@@ -1593,6 +1857,7 @@ function toTRPCError(e) {
|
|
|
1593
1857
|
}
|
|
1594
1858
|
var NotImplementedError, GateError, InvalidInputError, NotFoundError;
|
|
1595
1859
|
var init_errors = __esm(() => {
|
|
1860
|
+
init_burn_cache();
|
|
1596
1861
|
NotImplementedError = class NotImplementedError extends Error {
|
|
1597
1862
|
wave;
|
|
1598
1863
|
constructor(wave) {
|
|
@@ -3253,6 +3518,11 @@ async function commitDocs(worktreePath, message) {
|
|
|
3253
3518
|
return;
|
|
3254
3519
|
await g.commit(message, [DOCS_PATHSPEC]);
|
|
3255
3520
|
}
|
|
3521
|
+
async function commitPipelineDocs(repoPath) {
|
|
3522
|
+
try {
|
|
3523
|
+
await commitDocs(repoPath, "runcastle: docs the pipeline left uncommitted");
|
|
3524
|
+
} catch {}
|
|
3525
|
+
}
|
|
3256
3526
|
function activeTestDriveFeatureId() {
|
|
3257
3527
|
return testDriveState?.kind === "feature" ? testDriveState.featureId : undefined;
|
|
3258
3528
|
}
|
|
@@ -3352,6 +3622,7 @@ async function testDrive(ctx, project, feature, action, purpose = "human") {
|
|
|
3352
3622
|
...teardown?.failure ? { hookFailure: teardown.failure } : {}
|
|
3353
3623
|
};
|
|
3354
3624
|
}
|
|
3625
|
+
await commitPipelineDocs(project.repoPath);
|
|
3355
3626
|
const porcelain = (await g.raw(["status", "--porcelain"])).trim();
|
|
3356
3627
|
if (porcelain !== "")
|
|
3357
3628
|
return { ok: false, deniedReason: DENY_DIRTY };
|
|
@@ -4629,7 +4900,8 @@ function codexLoggedIn(env = process.env, fileExists = existsSync8) {
|
|
|
4629
4900
|
var init_codex_auth = () => {};
|
|
4630
4901
|
|
|
4631
4902
|
// src/doctor/doctor.ts
|
|
4632
|
-
import { existsSync as existsSync9 } from "fs";
|
|
4903
|
+
import { existsSync as existsSync9, statSync } from "fs";
|
|
4904
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
4633
4905
|
async function versionProbe(exec, id, label, bin, fix) {
|
|
4634
4906
|
const out = await exec(bin, ["--version"]);
|
|
4635
4907
|
if (out.ok && out.code === 0) {
|
|
@@ -4836,15 +5108,29 @@ async function containerRuntimeProbe(exec) {
|
|
|
4836
5108
|
fix: "Install Docker Desktop or Podman (see docs/research/PREREQS-NOTES.md \xA74). Not needed for interactive-only use."
|
|
4837
5109
|
};
|
|
4838
5110
|
}
|
|
4839
|
-
async function sandcastleImageProbe(exec, imageName) {
|
|
5111
|
+
async function sandcastleImageProbe(exec, imageName, burnerDockerfile, fileMtime) {
|
|
4840
5112
|
const id = "sandcastle-image";
|
|
4841
5113
|
const label = "Sandcastle container image";
|
|
4842
5114
|
for (const runtime of ["docker", "podman"]) {
|
|
4843
5115
|
const present = await exec(runtime, ["--version"]);
|
|
4844
5116
|
if (!(present.ok && present.code === 0))
|
|
4845
5117
|
continue;
|
|
4846
|
-
const inspect = await exec(runtime, ["image", "inspect", imageName]);
|
|
5118
|
+
const inspect = await exec(runtime, ["image", "inspect", "--format", "{{.Created}}", imageName]);
|
|
4847
5119
|
if (inspect.ok && inspect.code === 0) {
|
|
5120
|
+
const created = new Date(inspect.stdout.trim());
|
|
5121
|
+
const dockerfileChanged = fileMtime(burnerDockerfile);
|
|
5122
|
+
if (dockerfileChanged > created) {
|
|
5123
|
+
const date = (value) => value.toISOString().slice(0, 10);
|
|
5124
|
+
return {
|
|
5125
|
+
id,
|
|
5126
|
+
label,
|
|
5127
|
+
tier: 2,
|
|
5128
|
+
status: "stale",
|
|
5129
|
+
severity: "error",
|
|
5130
|
+
detail: `${imageName} built ${date(created)}, burner Dockerfile changed ${date(dockerfileChanged)} \u2014 rebuild`,
|
|
5131
|
+
fix: 'Open Settings \u2192 AFK burns and click "Rebuild image".'
|
|
5132
|
+
};
|
|
5133
|
+
}
|
|
4848
5134
|
return { id, label, tier: 2, status: "ok", severity: "error", detail: `${imageName} present` };
|
|
4849
5135
|
}
|
|
4850
5136
|
return {
|
|
@@ -4871,6 +5157,8 @@ async function runDoctor(env) {
|
|
|
4871
5157
|
const { exec } = env;
|
|
4872
5158
|
const processEnv = env.env ?? process.env;
|
|
4873
5159
|
const imageName = env.imageName ?? DEFAULT_SANDBOX_IMAGE;
|
|
5160
|
+
const burnerDockerfile = env.burnerDockerfile ?? fileURLToPath4(new URL("../assets/sandcastle/Dockerfile", import.meta.url));
|
|
5161
|
+
const fileMtime = env.fileMtime ?? ((path) => statSync(path).mtime);
|
|
4874
5162
|
const required = new Set(env.runtimes ?? [DEFAULT_RUNTIME]);
|
|
4875
5163
|
const results = [
|
|
4876
5164
|
await bunProbe(exec),
|
|
@@ -4885,7 +5173,7 @@ async function runDoctor(env) {
|
|
|
4885
5173
|
severity: required.has(runtime) ? "error" : "info"
|
|
4886
5174
|
}));
|
|
4887
5175
|
}
|
|
4888
|
-
results.push(await gitIdentityProbe(exec, env.cwd), await containerRuntimeProbe(exec), await sandcastleImageProbe(exec, imageName));
|
|
5176
|
+
results.push(await gitIdentityProbe(exec, env.cwd), await containerRuntimeProbe(exec), await sandcastleImageProbe(exec, imageName, burnerDockerfile, fileMtime));
|
|
4889
5177
|
const counts = (r) => r.severity === "error";
|
|
4890
5178
|
return {
|
|
4891
5179
|
results,
|
|
@@ -4942,7 +5230,7 @@ import {
|
|
|
4942
5230
|
mkdirSync as mkdirSync4,
|
|
4943
5231
|
readFileSync as readFileSync4,
|
|
4944
5232
|
readdirSync as readdirSync2,
|
|
4945
|
-
statSync,
|
|
5233
|
+
statSync as statSync2,
|
|
4946
5234
|
writeFileSync as writeFileSync3
|
|
4947
5235
|
} from "fs";
|
|
4948
5236
|
import { basename, dirname as dirname3, join as join10, resolve as resolve7 } from "path";
|
|
@@ -5061,7 +5349,7 @@ function gitInfoDir(worktreePath) {
|
|
|
5061
5349
|
const dotGit = join10(worktreePath, ".git");
|
|
5062
5350
|
if (!existsSync10(dotGit))
|
|
5063
5351
|
return null;
|
|
5064
|
-
if (
|
|
5352
|
+
if (statSync2(dotGit).isDirectory())
|
|
5065
5353
|
return join10(dotGit, "info");
|
|
5066
5354
|
const named = /^gitdir:\s*(.+)$/m.exec(readFileSync4(dotGit, "utf8"));
|
|
5067
5355
|
if (!named)
|
|
@@ -5173,7 +5461,7 @@ var init_runtimes = __esm(() => {
|
|
|
5173
5461
|
});
|
|
5174
5462
|
|
|
5175
5463
|
// src/launcher/sessions.ts
|
|
5176
|
-
import { statSync as
|
|
5464
|
+
import { statSync as statSync3 } from "fs";
|
|
5177
5465
|
import { and as and5, desc as desc3, eq as eq5, inArray, isNotNull, sql as sql2 } from "drizzle-orm";
|
|
5178
5466
|
function createSessionRow(ctx, input) {
|
|
5179
5467
|
const row = ctx.db.insert(sessions).values({
|
|
@@ -5524,7 +5812,7 @@ function transcriptBytes(session) {
|
|
|
5524
5812
|
if (!session?.transcriptPath)
|
|
5525
5813
|
return;
|
|
5526
5814
|
try {
|
|
5527
|
-
return
|
|
5815
|
+
return statSync3(session.transcriptPath).size;
|
|
5528
5816
|
} catch {
|
|
5529
5817
|
return;
|
|
5530
5818
|
}
|
|
@@ -5639,7 +5927,8 @@ function rowToTicket(row) {
|
|
|
5639
5927
|
error: row.error ?? undefined,
|
|
5640
5928
|
attemptBranch: row.attemptBranch ?? undefined,
|
|
5641
5929
|
conflictFiles: row.conflictFiles ?? undefined,
|
|
5642
|
-
digest: row.digest ?? undefined
|
|
5930
|
+
digest: row.digest ?? undefined,
|
|
5931
|
+
originFindingId: row.originFindingId ?? undefined
|
|
5643
5932
|
});
|
|
5644
5933
|
}
|
|
5645
5934
|
function resolveBlocking(inputs, startSeq) {
|
|
@@ -5664,13 +5953,13 @@ function normalizeModel(ctx, value) {
|
|
|
5664
5953
|
function listByFeature2(ctx, featureId) {
|
|
5665
5954
|
return ctx.db.select().from(tickets).where(eq6(tickets.featureId, featureId)).orderBy(asc3(tickets.seq)).all().map(rowToTicket);
|
|
5666
5955
|
}
|
|
5667
|
-
function storeTickets(ctx, featureId, inputs) {
|
|
5956
|
+
function storeTickets(ctx, featureId, inputs, options = {}) {
|
|
5668
5957
|
if (inputs.length === 0)
|
|
5669
5958
|
return [];
|
|
5670
5959
|
const { lap } = getFeatureRow(ctx, featureId);
|
|
5671
5960
|
const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq6(tickets.featureId, featureId)).all();
|
|
5672
5961
|
const startSeq = existing.reduce((max2, r) => Math.max(max2, r.seq), 0) + 1;
|
|
5673
|
-
const resolved = resolveBlocking(inputs, startSeq);
|
|
5962
|
+
const resolved = options.blockedByAreGlobal ? inputs.map((input, index2) => ({ seq: startSeq + index2, blockedBy: input.blockedBy })) : resolveBlocking(inputs, startSeq);
|
|
5674
5963
|
const rows = inputs.map((t, i) => ({
|
|
5675
5964
|
id: newId("tkt"),
|
|
5676
5965
|
featureId,
|
|
@@ -5689,7 +5978,8 @@ function storeTickets(ctx, featureId, inputs) {
|
|
|
5689
5978
|
error: null,
|
|
5690
5979
|
attemptBranch: null,
|
|
5691
5980
|
conflictFiles: null,
|
|
5692
|
-
digest: null
|
|
5981
|
+
digest: null,
|
|
5982
|
+
originFindingId: t.originFindingId ?? null
|
|
5693
5983
|
}));
|
|
5694
5984
|
ctx.db.insert(tickets).values(rows).run();
|
|
5695
5985
|
emit(ctx, featureId, {
|
|
@@ -6071,11 +6361,195 @@ var init_end_session = __esm(() => {
|
|
|
6071
6361
|
init_registry();
|
|
6072
6362
|
});
|
|
6073
6363
|
|
|
6364
|
+
// src/services/review-findings.ts
|
|
6365
|
+
import { and as and8, asc as asc4, eq as eq8, isNotNull as isNotNull2 } from "drizzle-orm";
|
|
6366
|
+
function rowToFinding(row) {
|
|
6367
|
+
return ReviewFinding.parse(row);
|
|
6368
|
+
}
|
|
6369
|
+
function getFinding(ctx, id) {
|
|
6370
|
+
const row = ctx.db.select().from(reviewFindings).where(eq8(reviewFindings.id, id)).get();
|
|
6371
|
+
if (!row)
|
|
6372
|
+
throw new NotFoundError(`review finding ${id} not found`);
|
|
6373
|
+
return rowToFinding(row);
|
|
6374
|
+
}
|
|
6375
|
+
function listByFeature3(ctx, featureId) {
|
|
6376
|
+
return ctx.db.select().from(reviewFindings).where(eq8(reviewFindings.featureId, featureId)).orderBy(asc4(reviewFindings.createdAt), asc4(reviewFindings.id)).all().map(rowToFinding);
|
|
6377
|
+
}
|
|
6378
|
+
function buildFixTicket(finding) {
|
|
6379
|
+
return {
|
|
6380
|
+
title: finding.title,
|
|
6381
|
+
goal: `Fix: ${finding.title}`,
|
|
6382
|
+
context: [
|
|
6383
|
+
`Location: ${finding.location}`,
|
|
6384
|
+
`Citation: ${finding.citation}`,
|
|
6385
|
+
`Detail: ${finding.detail}`,
|
|
6386
|
+
`Repro step: ${finding.reproStep}`
|
|
6387
|
+
].join(`
|
|
6388
|
+
|
|
6389
|
+
`),
|
|
6390
|
+
acceptanceCriteria: [
|
|
6391
|
+
`The repro step no longer reproduces / the cited criterion holds: ${finding.reproStep}`
|
|
6392
|
+
],
|
|
6393
|
+
seams: [],
|
|
6394
|
+
blockedBy: [],
|
|
6395
|
+
kind: "implementation",
|
|
6396
|
+
originFindingId: finding.id
|
|
6397
|
+
};
|
|
6398
|
+
}
|
|
6399
|
+
function reportFinding(ctx, args) {
|
|
6400
|
+
const { featureId, reviewTicket, input } = args;
|
|
6401
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
6402
|
+
const id = newId("finding");
|
|
6403
|
+
const ticketCount = ctx.db.select({ id: reviewFindings.id }).from(reviewFindings).where(and8(eq8(reviewFindings.reviewTicketId, reviewTicket.id), isNotNull2(reviewFindings.fixTicketId))).all().length;
|
|
6404
|
+
const overCap = input.kind === "defect" && ticketCount >= AUTO_FIX_CAP;
|
|
6405
|
+
ctx.db.insert(reviewFindings).values({
|
|
6406
|
+
id,
|
|
6407
|
+
featureId,
|
|
6408
|
+
lap: feature.lap,
|
|
6409
|
+
reviewTicketId: reviewTicket.id,
|
|
6410
|
+
...input,
|
|
6411
|
+
reproStep: input.reproStep ?? "",
|
|
6412
|
+
status: "open",
|
|
6413
|
+
openReason: overCap ? "over-cap" : null,
|
|
6414
|
+
failureReason: null,
|
|
6415
|
+
fixTicketId: null,
|
|
6416
|
+
createdAt: Date.now()
|
|
6417
|
+
}).run();
|
|
6418
|
+
let fixTicket = null;
|
|
6419
|
+
if (input.kind === "defect" && !overCap) {
|
|
6420
|
+
const finding2 = getFinding(ctx, id);
|
|
6421
|
+
fixTicket = storeTickets(ctx, featureId, [
|
|
6422
|
+
{ ...buildFixTicket(finding2), blockedBy: [reviewTicket.seq] }
|
|
6423
|
+
], { blockedByAreGlobal: true })[0];
|
|
6424
|
+
ctx.db.update(reviewFindings).set({ fixTicketId: fixTicket.id }).where(eq8(reviewFindings.id, id)).run();
|
|
6425
|
+
}
|
|
6426
|
+
const finding = getFinding(ctx, id);
|
|
6427
|
+
emit(ctx, featureId, {
|
|
6428
|
+
type: "finding.reported",
|
|
6429
|
+
message: `${input.kind} reported: ${input.title}`,
|
|
6430
|
+
ticketId: fixTicket?.id,
|
|
6431
|
+
data: { findingId: id, fixTicketId: fixTicket?.id, overCap }
|
|
6432
|
+
});
|
|
6433
|
+
return { finding, fixTicket, overCap };
|
|
6434
|
+
}
|
|
6435
|
+
function updateStatus(ctx, findingId, patch) {
|
|
6436
|
+
const current2 = getFinding(ctx, findingId);
|
|
6437
|
+
ctx.db.update(reviewFindings).set(patch).where(eq8(reviewFindings.id, findingId)).run();
|
|
6438
|
+
emit(ctx, current2.featureId, {
|
|
6439
|
+
type: "finding.updated",
|
|
6440
|
+
message: `finding ${findingId} marked ${patch.status}`,
|
|
6441
|
+
data: { findingId, status: patch.status, reason: patch.failureReason }
|
|
6442
|
+
});
|
|
6443
|
+
return getFinding(ctx, findingId);
|
|
6444
|
+
}
|
|
6445
|
+
function markFixing(ctx, findingId) {
|
|
6446
|
+
return updateStatus(ctx, findingId, {
|
|
6447
|
+
status: "fixing",
|
|
6448
|
+
openReason: null,
|
|
6449
|
+
failureReason: null
|
|
6450
|
+
});
|
|
6451
|
+
}
|
|
6452
|
+
function markFixed(ctx, findingId) {
|
|
6453
|
+
return updateStatus(ctx, findingId, {
|
|
6454
|
+
status: "fixed",
|
|
6455
|
+
openReason: null,
|
|
6456
|
+
failureReason: null
|
|
6457
|
+
});
|
|
6458
|
+
}
|
|
6459
|
+
function markFailed(ctx, findingId, reason) {
|
|
6460
|
+
return updateStatus(ctx, findingId, {
|
|
6461
|
+
status: "failed",
|
|
6462
|
+
openReason: "fix-failed",
|
|
6463
|
+
failureReason: reason ?? null
|
|
6464
|
+
});
|
|
6465
|
+
}
|
|
6466
|
+
function markFixProgress(ctx, findingId, progress, reason) {
|
|
6467
|
+
if (progress === "fixing")
|
|
6468
|
+
return markFixing(ctx, findingId);
|
|
6469
|
+
if (progress === "fixed")
|
|
6470
|
+
return markFixed(ctx, findingId);
|
|
6471
|
+
return markFailed(ctx, findingId, reason);
|
|
6472
|
+
}
|
|
6473
|
+
function dismiss(ctx, findingId) {
|
|
6474
|
+
return updateStatus(ctx, findingId, {
|
|
6475
|
+
status: "dismissed",
|
|
6476
|
+
openReason: null,
|
|
6477
|
+
failureReason: null
|
|
6478
|
+
});
|
|
6479
|
+
}
|
|
6480
|
+
function defectState(finding, fixTicket) {
|
|
6481
|
+
if (finding.status === "dismissed")
|
|
6482
|
+
return "dismissed";
|
|
6483
|
+
if (finding.status === "fixed" || fixTicket?.status === "done")
|
|
6484
|
+
return "fixed";
|
|
6485
|
+
if (fixTicket && fixTicket.status !== "failed" && fixTicket.status !== "cancelled") {
|
|
6486
|
+
return "fixing";
|
|
6487
|
+
}
|
|
6488
|
+
return finding.status === "open" || finding.status === "failed" ? "open" : "fixing";
|
|
6489
|
+
}
|
|
6490
|
+
function viewByFeature(ctx, featureId) {
|
|
6491
|
+
const findings = listByFeature3(ctx, featureId);
|
|
6492
|
+
const tickets2 = listByFeature2(ctx, featureId);
|
|
6493
|
+
const summary = { found: 0, fixed: 0, open: 0, observations: 0 };
|
|
6494
|
+
const openDefects = [];
|
|
6495
|
+
for (const finding of findings) {
|
|
6496
|
+
if (finding.kind === "observation") {
|
|
6497
|
+
summary.observations += 1;
|
|
6498
|
+
continue;
|
|
6499
|
+
}
|
|
6500
|
+
summary.found += 1;
|
|
6501
|
+
const state = defectState(finding, fixTicketOf(finding, tickets2));
|
|
6502
|
+
if (state === "fixed")
|
|
6503
|
+
summary.fixed += 1;
|
|
6504
|
+
else if (state === "open") {
|
|
6505
|
+
summary.open += 1;
|
|
6506
|
+
openDefects.push(finding);
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
return { findings, openDefects, summary };
|
|
6510
|
+
}
|
|
6511
|
+
function fixTicketOf(finding, tickets2) {
|
|
6512
|
+
return finding.fixTicketId ? tickets2.find((t) => t.id === finding.fixTicketId) : undefined;
|
|
6513
|
+
}
|
|
6514
|
+
function promoteOpenDefects(ctx, featureId) {
|
|
6515
|
+
const defects = viewByFeature(ctx, featureId).openDefects;
|
|
6516
|
+
if (defects.length === 0)
|
|
6517
|
+
throw new InvalidInputError("no open defects to fix");
|
|
6518
|
+
const tickets2 = storeTickets(ctx, featureId, defects.map(buildFixTicket));
|
|
6519
|
+
const findings = defects.map((defect, index2) => {
|
|
6520
|
+
ctx.db.update(reviewFindings).set({
|
|
6521
|
+
status: "fixing",
|
|
6522
|
+
openReason: null,
|
|
6523
|
+
failureReason: null,
|
|
6524
|
+
fixTicketId: tickets2[index2].id
|
|
6525
|
+
}).where(eq8(reviewFindings.id, defect.id)).run();
|
|
6526
|
+
return getFinding(ctx, defect.id);
|
|
6527
|
+
});
|
|
6528
|
+
emit(ctx, featureId, {
|
|
6529
|
+
type: "finding.fixing",
|
|
6530
|
+
message: `${findings.length} open defect${findings.length === 1 ? "" : "s"} promoted to fix ticket${tickets2.length === 1 ? ` ${tickets2[0].seq}` : `s ${tickets2.map((t) => t.seq).join(", ")}`}`,
|
|
6531
|
+
data: { findingIds: findings.map((f) => f.id), seqs: tickets2.map((t) => t.seq) }
|
|
6532
|
+
});
|
|
6533
|
+
return { findings, tickets: tickets2 };
|
|
6534
|
+
}
|
|
6535
|
+
var AUTO_FIX_CAP = 8;
|
|
6536
|
+
var init_review_findings = __esm(() => {
|
|
6537
|
+
init_src();
|
|
6538
|
+
init_schema();
|
|
6539
|
+
init_errors();
|
|
6540
|
+
init_events();
|
|
6541
|
+
init_repo();
|
|
6542
|
+
init_tickets();
|
|
6543
|
+
});
|
|
6544
|
+
|
|
6074
6545
|
// src/services/settings.ts
|
|
6075
6546
|
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
6076
6547
|
import { dirname as dirname4 } from "path";
|
|
6077
6548
|
import * as z3 from "zod";
|
|
6078
|
-
import { eq as
|
|
6549
|
+
import { eq as eq9 } from "drizzle-orm";
|
|
6550
|
+
function hostDefaults(logicalCpus) {
|
|
6551
|
+
return { ...DEFAULTS, burnConcurrency: resolveDefaultBurnConcurrency(logicalCpus) };
|
|
6552
|
+
}
|
|
6079
6553
|
function readRawConfig(configFile) {
|
|
6080
6554
|
if (!existsSync13(configFile))
|
|
6081
6555
|
return {};
|
|
@@ -6115,7 +6589,7 @@ function projectOverrides(ctx, projectId) {
|
|
|
6115
6589
|
dbResetCommand: projects.dbResetCommand,
|
|
6116
6590
|
driveSetupCommand: projects.driveSetupCommand,
|
|
6117
6591
|
driveStopCommand: projects.driveStopCommand
|
|
6118
|
-
}).from(projects).where(
|
|
6592
|
+
}).from(projects).where(eq9(projects.id, projectId)).get();
|
|
6119
6593
|
return {
|
|
6120
6594
|
model: row?.model ?? null,
|
|
6121
6595
|
sandbox: row?.sandbox ?? null,
|
|
@@ -6148,15 +6622,16 @@ function resolveField(desc5, layers) {
|
|
|
6148
6622
|
if (desc5.configKey && Object.prototype.hasOwnProperty.call(layers.fileRaw, desc5.configKey)) {
|
|
6149
6623
|
return { ...base, value: layers.fileRaw[desc5.configKey], source: "file", editable: true };
|
|
6150
6624
|
}
|
|
6151
|
-
const value = desc5.configKey ?
|
|
6625
|
+
const value = desc5.configKey ? layers.defaults[desc5.configKey] ?? null : null;
|
|
6152
6626
|
return { ...base, value, source: "default", editable: true };
|
|
6153
6627
|
}
|
|
6154
6628
|
function getSettings(ctx, projectId, io = {}) {
|
|
6155
6629
|
const env = io.env ?? process.env;
|
|
6156
6630
|
const fileRaw = readRawConfig(io.configFile ?? configPath());
|
|
6157
6631
|
const overrides = projectId !== undefined ? projectOverrides(ctx, projectId) : null;
|
|
6632
|
+
const defaults = hostDefaults(io.logicalCpus ?? hostLogicalCpus());
|
|
6158
6633
|
const visible = DESCRIPTORS.filter((d) => projectId !== undefined || d.configKey !== undefined);
|
|
6159
|
-
const fields = visible.map((d) => resolveField(d, { env, fileRaw, overrides }));
|
|
6634
|
+
const fields = visible.map((d) => resolveField(d, { env, fileRaw, overrides, defaults }));
|
|
6160
6635
|
return { projectId, fields: [...fields, ...stepModelFields(fileRaw)] };
|
|
6161
6636
|
}
|
|
6162
6637
|
function updateSettings(ctx, input, io = {}) {
|
|
@@ -6179,7 +6654,7 @@ function updateSettings(ctx, input, io = {}) {
|
|
|
6179
6654
|
if (!toProject)
|
|
6180
6655
|
throw new InvalidInputError(`${desc5.key} cannot be cleared`);
|
|
6181
6656
|
const project = requireProjectById(ctx, input.projectId);
|
|
6182
|
-
ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(
|
|
6657
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(eq9(projects.id, project.id)).run();
|
|
6183
6658
|
recordHuman(ctx, project.id, desc5.key, null);
|
|
6184
6659
|
emitProject(ctx, project.id, {
|
|
6185
6660
|
type: "settings.updated",
|
|
@@ -6195,7 +6670,7 @@ function updateSettings(ctx, input, io = {}) {
|
|
|
6195
6670
|
const value = parsed.data;
|
|
6196
6671
|
if (toProject) {
|
|
6197
6672
|
const project = requireProjectById(ctx, input.projectId);
|
|
6198
|
-
ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(
|
|
6673
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(eq9(projects.id, project.id)).run();
|
|
6199
6674
|
recordHuman(ctx, project.id, desc5.key, String(value));
|
|
6200
6675
|
emitProject(ctx, project.id, {
|
|
6201
6676
|
type: "settings.updated",
|
|
@@ -6274,6 +6749,7 @@ function field(view, key) {
|
|
|
6274
6749
|
var GLOBAL_EVENT_KEY = "global", idEnv = (raw) => raw, DESCRIPTORS, DEFAULTS, STEP_SET;
|
|
6275
6750
|
var init_settings = __esm(() => {
|
|
6276
6751
|
init_src();
|
|
6752
|
+
init_config_load();
|
|
6277
6753
|
init_paths();
|
|
6278
6754
|
init_schema();
|
|
6279
6755
|
init_errors();
|
|
@@ -6440,7 +6916,7 @@ var init_settings = __esm(() => {
|
|
|
6440
6916
|
// src/services/setup.ts
|
|
6441
6917
|
import { copyFileSync as copyFileSync2, existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
6442
6918
|
import { dirname as dirname5, join as join12 } from "path";
|
|
6443
|
-
import { fileURLToPath as
|
|
6919
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
6444
6920
|
async function writeGitIdentity(exec, identity) {
|
|
6445
6921
|
const name = identity.name.trim();
|
|
6446
6922
|
const email = identity.email.trim();
|
|
@@ -6543,7 +7019,7 @@ async function resolveRuntime(exec, preferred) {
|
|
|
6543
7019
|
function resolveSandcastleBin() {
|
|
6544
7020
|
try {
|
|
6545
7021
|
const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
|
|
6546
|
-
let dir = dirname5(
|
|
7022
|
+
let dir = dirname5(fileURLToPath5(mainUrl));
|
|
6547
7023
|
for (let hops = 0;hops < 6; hops++) {
|
|
6548
7024
|
const manifestPath = join12(dir, "package.json");
|
|
6549
7025
|
if (existsSync14(manifestPath)) {
|
|
@@ -6578,22 +7054,19 @@ function terminalSpec(kind, opts) {
|
|
|
6578
7054
|
};
|
|
6579
7055
|
}
|
|
6580
7056
|
function sandcastleTemplateDir() {
|
|
6581
|
-
return resolveAsset(ASSET_ENV.sandcastleTemplate,
|
|
7057
|
+
return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath5(new URL("../assets/sandcastle", import.meta.url)));
|
|
6582
7058
|
}
|
|
6583
|
-
function
|
|
7059
|
+
function refreshSandcastleConfig(templateDir, targetDir) {
|
|
6584
7060
|
const dir = join12(targetDir, ".sandcastle");
|
|
6585
|
-
if (existsSync14(dir))
|
|
6586
|
-
return { scaffolded: false, dir };
|
|
6587
7061
|
mkdirSync7(dir, { recursive: true });
|
|
6588
7062
|
for (const name of readdirSync4(templateDir)) {
|
|
6589
7063
|
copyFileSync2(join12(templateDir, name), join12(dir, name));
|
|
6590
7064
|
}
|
|
6591
|
-
return
|
|
7065
|
+
return dir;
|
|
6592
7066
|
}
|
|
6593
|
-
function prepareSandboxBuildContext() {
|
|
6594
|
-
const target = sandboxBuildDir();
|
|
7067
|
+
function prepareSandboxBuildContext(templateDir = sandcastleTemplateDir(), target = sandboxBuildDir()) {
|
|
6595
7068
|
mkdirSync7(target, { recursive: true });
|
|
6596
|
-
|
|
7069
|
+
refreshSandcastleConfig(templateDir, target);
|
|
6597
7070
|
return target;
|
|
6598
7071
|
}
|
|
6599
7072
|
function createCredentialVerifier(exec, runtime = DEFAULT_RUNTIME) {
|
|
@@ -6661,6 +7134,32 @@ var init_setup = __esm(() => {
|
|
|
6661
7134
|
};
|
|
6662
7135
|
});
|
|
6663
7136
|
|
|
7137
|
+
// src/doctor/system-exec.ts
|
|
7138
|
+
import { spawn as spawn4 } from "child_process";
|
|
7139
|
+
function createSystemExec(opts = {}) {
|
|
7140
|
+
return (command, args) => new Promise((resolve9) => {
|
|
7141
|
+
const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
|
|
7142
|
+
let stdout = "";
|
|
7143
|
+
let stderr = "";
|
|
7144
|
+
const child = spawn4(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
|
|
7145
|
+
child.stdout?.on("data", (d) => {
|
|
7146
|
+
stdout += d.toString();
|
|
7147
|
+
});
|
|
7148
|
+
child.stderr?.on("data", (d) => {
|
|
7149
|
+
stderr += d.toString();
|
|
7150
|
+
});
|
|
7151
|
+
child.on("error", (err) => {
|
|
7152
|
+
resolve9({ ok: false, code: null, stdout, stderr: stderr || String(err) });
|
|
7153
|
+
});
|
|
7154
|
+
child.on("close", (code) => {
|
|
7155
|
+
resolve9({ ok: true, code: code ?? 0, stdout, stderr });
|
|
7156
|
+
});
|
|
7157
|
+
});
|
|
7158
|
+
}
|
|
7159
|
+
var init_system_exec = __esm(() => {
|
|
7160
|
+
init_resolve_executable();
|
|
7161
|
+
});
|
|
7162
|
+
|
|
6664
7163
|
// src/services/agent-stream.ts
|
|
6665
7164
|
function beginTranscript(ticketId) {
|
|
6666
7165
|
transcripts.set(ticketId, { live: true, chunks: [], nextIndex: 0, bytes: 0 });
|
|
@@ -6716,13 +7215,15 @@ function renderGuardScript(rules = GUARD_RULES) {
|
|
|
6716
7215
|
"command -v jq >/dev/null 2>&1 || exit 0",
|
|
6717
7216
|
'tool=$(printf %s "$payload" | jq -r ".tool_name // empty" 2>/dev/null || true)',
|
|
6718
7217
|
'[ "$tool" = "Bash" ] || exit 0',
|
|
6719
|
-
'
|
|
6720
|
-
'[ -n "$
|
|
7218
|
+
'raw_cmd=$(printf %s "$payload" | jq -r ".tool_input.command // empty" 2>/dev/null || true)',
|
|
7219
|
+
'[ -n "$raw_cmd" ] || exit 0',
|
|
6721
7220
|
"",
|
|
6722
7221
|
"# Match against the command with quoted spans blanked, so an argument is",
|
|
6723
7222
|
'# never read as a command: `grep -rn "git stash" docs/` searches for a',
|
|
6724
|
-
"# string, it does not run one.",
|
|
6725
|
-
|
|
7223
|
+
"# string, it does not run one. Flatten newlines so one grep sees a whole",
|
|
7224
|
+
"# multiline loop.",
|
|
7225
|
+
"cmd=$raw_cmd",
|
|
7226
|
+
`cmd=$(printf %s "$cmd" | sed "s/'[^']*'/ /g; s/\\"[^\\"]*\\"/ /g" | tr '\\n' ' ')`,
|
|
6726
7227
|
"",
|
|
6727
7228
|
"deny() {",
|
|
6728
7229
|
` printf %s "$1" | jq -R -s '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:.}}'`,
|
|
@@ -6731,7 +7232,8 @@ function renderGuardScript(rules = GUARD_RULES) {
|
|
|
6731
7232
|
""
|
|
6732
7233
|
];
|
|
6733
7234
|
for (const rule of rules) {
|
|
6734
|
-
|
|
7235
|
+
const commandVariable = rule.inspectQuotedArguments ? "$raw_cmd" : "$cmd";
|
|
7236
|
+
lines.push(`# ${rule.id}`, `if printf %s "${commandVariable}" | grep -Eq -- ${shSingleQuote(rule.pattern)}; then`, ` deny ${shSingleQuote(rule.reason)}`, "fi", "");
|
|
6735
7237
|
}
|
|
6736
7238
|
lines.push("exit 0", "");
|
|
6737
7239
|
return lines.join(`
|
|
@@ -6781,11 +7283,37 @@ var init_burn_guard = __esm(() => {
|
|
|
6781
7283
|
pattern: "--maxWorkers|--runInBand|--pool=|--shard=|singleFork|--poolOptions",
|
|
6782
7284
|
reason: `Do not override the test runner's concurrency. Measured in this sandbox, a suite that runs in ~55s at its configured concurrency takes 10-20 minutes serialised, so this "safe" flag is the single most expensive habit in a burn. Run the repo's test command as configured. If it is killed for memory, run only the test files your change touches and say so in your final message.`
|
|
6783
7285
|
},
|
|
7286
|
+
{
|
|
7287
|
+
id: "no-long-sleep",
|
|
7288
|
+
pattern: `${CMD_START}sleep[[:space:]]+((3[1-9]|[4-9][0-9]|[1-9][0-9]{2,})s?|[1-9][0-9]*(m|h|d))([^A-Za-z0-9_]|$)`,
|
|
7289
|
+
reason: "Polling and long sleeps are never needed in a burn. Run the command directly and read its output."
|
|
7290
|
+
},
|
|
7291
|
+
{
|
|
7292
|
+
id: "no-verification-polling-loop",
|
|
7293
|
+
pattern: `${CMD_START}(until|while)[[:space:]].*(tsc|typecheck|vitest|jest|pytest|mocha|(npm|pnpm|yarn|bun)[[:space:]]+(run[[:space:]]+)?test|turbo[[:space:]]+run[[:space:]]+test).*[;[:space:]][[:space:]]*done`,
|
|
7294
|
+
reason: "Polling and long sleeps are never needed in a burn. Run the command directly and read its output."
|
|
7295
|
+
},
|
|
6784
7296
|
{
|
|
6785
7297
|
id: "no-interpreter-heredoc-edit",
|
|
6786
7298
|
pattern: `${CMD_START}(python3?|node|bun|perl|ruby)[[:space:]]+-[[:space:]]*<<`,
|
|
6787
7299
|
reason: "Do not rewrite files by piping a heredoc into an interpreter \u2014 individual such calls have been measured at 29s, 57s, 120s and 761s, and a half-written file survives if this process dies mid-command. Use the Edit tool, which is faster, atomic, and fails loudly when its target text is not found."
|
|
6788
7300
|
},
|
|
7301
|
+
{
|
|
7302
|
+
id: "no-interpreter-inline-edit",
|
|
7303
|
+
pattern: `${CMD_START}node[[:space:]]+-e([[:space:]]|$)`,
|
|
7304
|
+
reason: "Do not rewrite files with a `node -e` program. Use the Edit tool, which is faster, atomic, and fails loudly when its target text is not found."
|
|
7305
|
+
},
|
|
7306
|
+
{
|
|
7307
|
+
id: "no-perl-in-place-edit",
|
|
7308
|
+
pattern: `${CMD_START}perl[[:space:]]+-[0-9p]*i[^[:space:]]*`,
|
|
7309
|
+
reason: "Do not rewrite files with Perl in-place editing. Use the Edit tool, which is faster, atomic, and fails loudly when its target text is not found."
|
|
7310
|
+
},
|
|
7311
|
+
{
|
|
7312
|
+
id: "no-sed-multi-range-edit",
|
|
7313
|
+
pattern: `${CMD_START}sed[[:space:]]+-[^[:space:]]*i[^[:space:]]*[[:space:]]+[^;]*,[^;]*d;[^;]*,[^;]*d`,
|
|
7314
|
+
inspectQuotedArguments: true,
|
|
7315
|
+
reason: "Do not perform multi-range file surgery with `sed -i`. Use the Edit tool, which is atomic and fails loudly when its target text is not found."
|
|
7316
|
+
},
|
|
6789
7317
|
{
|
|
6790
7318
|
id: "no-cat-heredoc-edit",
|
|
6791
7319
|
pattern: `${CMD_START}cat[[:space:]]+>>?[[:space:]]*[^[:space:]|]+[[:space:]]*<<`,
|
|
@@ -6832,6 +7360,37 @@ function findOnPath(bin, env = process.env, platform = process.platform) {
|
|
|
6832
7360
|
}
|
|
6833
7361
|
return;
|
|
6834
7362
|
}
|
|
7363
|
+
function buildDriveAvailability(browserPath, devCommand) {
|
|
7364
|
+
const missing = [];
|
|
7365
|
+
if (!browserPath) {
|
|
7366
|
+
missing.push(`\`${AGENT_BROWSER_BIN}\` is not on this machine's PATH, so there is no browser to walk the app with`);
|
|
7367
|
+
}
|
|
7368
|
+
if (!devCommand?.trim()) {
|
|
7369
|
+
missing.push("this project has no dev command configured, so a drive has no app to boot");
|
|
7370
|
+
}
|
|
7371
|
+
if (missing.length === 0) {
|
|
7372
|
+
return `A drive **is** available: \`${AGENT_BROWSER_BIN}\` is on this machine's PATH and the ` + "project has a dev command, so `review_drive` can boot the app and you can walk it. Drive " + "mode is open to you \u2014 take it if, and only if, this lap has a surface a human could operate.";
|
|
7373
|
+
}
|
|
7374
|
+
return `A drive is **not** available: ${missing.join(", and ")}. So the mode is already decided \u2014 ` + "run Gates mode, whatever this lap touched, and do not call `review_drive`. This is not a " + "degraded review; it is the whole review this lap gets.";
|
|
7375
|
+
}
|
|
7376
|
+
function buildGateNotes(config2) {
|
|
7377
|
+
const commands = config2.verifyCommands?.trim();
|
|
7378
|
+
const failures = config2.knownFailures?.trim();
|
|
7379
|
+
const out = [];
|
|
7380
|
+
if (commands) {
|
|
7381
|
+
out.push("Run exactly these, once each \u2014 they are this project's own verify commands, so do not go looking for alternatives, add concurrency flags, or re-run one to re-read its output (redirect to a file and read that instead):", "", "```", commands, "```");
|
|
7382
|
+
} else {
|
|
7383
|
+
out.push("This project has no verify commands configured, so there are no gates to run. Do not go hunting for them \u2014 say so in one line of your summary note and spend the whole mode on the diff.");
|
|
7384
|
+
}
|
|
7385
|
+
out.push("");
|
|
7386
|
+
if (failures) {
|
|
7387
|
+
out.push("These already fail on this repo without this lap's help:", "", "```", failures, "```", "", "Subtract that baseline: a failure inside it is not this lap's and is not a finding. A failure outside it is one this lap introduced, and it is the finding worth the human's attention above every other.");
|
|
7388
|
+
} else {
|
|
7389
|
+
out.push("No pre-existing-failure baseline is configured, so a red gate may well predate this lap. Run it once, then check whether the failure touches the diff before writing it up as this lap's \u2014 and never re-run a suite to establish what was already red.");
|
|
7390
|
+
}
|
|
7391
|
+
return out.join(`
|
|
7392
|
+
`);
|
|
7393
|
+
}
|
|
6835
7394
|
function writeReviewArtifacts(ticket, runId, config2) {
|
|
6836
7395
|
const dir = reviewDir(ticket.id);
|
|
6837
7396
|
rmSync2(dir, { recursive: true, force: true });
|
|
@@ -6853,13 +7412,19 @@ async function releaseDriveQuietly() {
|
|
|
6853
7412
|
} catch {}
|
|
6854
7413
|
}
|
|
6855
7414
|
async function executeReviewTicket(ctx, ticket, deps) {
|
|
7415
|
+
const startedAt = Date.now();
|
|
7416
|
+
const timer = createToolTimer();
|
|
7417
|
+
try {
|
|
7418
|
+
return await reviewTicketOutcome(ctx, ticket, deps, timer);
|
|
7419
|
+
} finally {
|
|
7420
|
+
emitTicketTiming(ctx, ticket, buildTicketTiming(timer.summary(), startedAt, Date.now()));
|
|
7421
|
+
}
|
|
7422
|
+
}
|
|
7423
|
+
async function reviewTicketOutcome(ctx, ticket, deps, timer) {
|
|
6856
7424
|
const { project, feature } = ctx;
|
|
6857
7425
|
if (!feature.baseBranch) {
|
|
6858
7426
|
return couldNotReview(ticket, `feature ${feature.slug} has no recorded base branch, so there is nothing to diff \`${feature.branch}\` against.`);
|
|
6859
7427
|
}
|
|
6860
|
-
if (!findOnPath(AGENT_BROWSER_BIN)) {
|
|
6861
|
-
return couldNotReview(ticket, `\`${AGENT_BROWSER_BIN}\` is not on this machine's PATH, so there is no way to drive the app. Install it and re-burn this ticket.`);
|
|
6862
|
-
}
|
|
6863
7428
|
const artifacts = writeReviewArtifacts(ticket, ctx.runId, deps.config);
|
|
6864
7429
|
const prompt = renderReviewPrompt(readFileSync8(reviewTemplatePath(), "utf8"), {
|
|
6865
7430
|
TICKET_JSON: buildTicketJson(ticket),
|
|
@@ -6868,6 +7433,8 @@ async function executeReviewTicket(ctx, ticket, deps) {
|
|
|
6868
7433
|
LAP_DIGESTS: buildLapDigestsBlock(deps.lapDigests),
|
|
6869
7434
|
FEATURE_BRANCH: feature.branch,
|
|
6870
7435
|
BASE_BRANCH: feature.baseBranch,
|
|
7436
|
+
DRIVE_AVAILABILITY: buildDriveAvailability(findOnPath(AGENT_BROWSER_BIN), project.devCommand),
|
|
7437
|
+
GATE_NOTES: buildGateNotes(deps.config),
|
|
6871
7438
|
DIGEST_PATH: artifacts.digestPath,
|
|
6872
7439
|
BLOCKED_PATH: artifacts.blockedPath,
|
|
6873
7440
|
WALKTHROUGH_PATH: artifacts.walkthroughPath
|
|
@@ -6877,6 +7444,7 @@ async function executeReviewTicket(ctx, ticket, deps) {
|
|
|
6877
7444
|
beginTranscript(ticket.id);
|
|
6878
7445
|
const onStreamEvent = (event) => {
|
|
6879
7446
|
throttle.onEvent(event);
|
|
7447
|
+
timer.onEvent(event);
|
|
6880
7448
|
if (event.type === "text") {
|
|
6881
7449
|
appendTranscript(ticket.id, { kind: "text", text: event.message });
|
|
6882
7450
|
} else if (event.type === "toolCall") {
|
|
@@ -6951,11 +7519,15 @@ var init_review_ticket = __esm(() => {
|
|
|
6951
7519
|
// src/workflows/ticket-burner.ts
|
|
6952
7520
|
import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
|
|
6953
7521
|
import { dirname as dirname6, join as join14, posix as posixPath, win32 as winPath } from "path";
|
|
6954
|
-
import { fileURLToPath as
|
|
7522
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
6955
7523
|
import { claudeCode, codex, run as run2 } from "@ai-hero/sandcastle";
|
|
6956
7524
|
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
6957
7525
|
import { podman } from "@ai-hero/sandcastle/sandboxes/podman";
|
|
6958
7526
|
import { noSandbox as noSandbox2 } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
|
|
7527
|
+
function missingImageRuntimeMessage(runtime, image) {
|
|
7528
|
+
const binary = RUNTIME_BINARY[runtime];
|
|
7529
|
+
return `${binary} is not installed in image ${image} \u2014 the image predates the burner Dockerfile. Rebuild it from Settings \u2192 AFK burns (Rebuild image).`;
|
|
7530
|
+
}
|
|
6959
7531
|
function burnAuthReady(runtime, token, loggedIn = codexLoggedIn) {
|
|
6960
7532
|
if (token !== undefined)
|
|
6961
7533
|
return true;
|
|
@@ -7158,7 +7730,9 @@ function buildDriveNotes(settings) {
|
|
|
7158
7730
|
`);
|
|
7159
7731
|
}
|
|
7160
7732
|
function buildGuardNotes(guardInstalled) {
|
|
7161
|
-
|
|
7733
|
+
const introduction = guardInstalled ? "The following commands are **denied before they run** by a tool hook. Each denial and its prescribed alternative comes verbatim from the guard that enforces it. A denial is policy, not a broken environment \u2014 follow the alternative and do not try to route around it." : "The deny hook is not installed in this burn, so the following guard rules are not machine-enforced. Follow them on discipline alone; each denial and its prescribed alternative comes verbatim from the guard used in protected burns.";
|
|
7734
|
+
return [introduction, "", ...GUARD_RULES.map((rule) => `- ${rule.reason}`)].join(`
|
|
7735
|
+
`);
|
|
7162
7736
|
}
|
|
7163
7737
|
function buildBlockersBlock(blockedBy, digests) {
|
|
7164
7738
|
if (blockedBy.length === 0) {
|
|
@@ -7181,6 +7755,16 @@ ${d.digest.trim()}`;
|
|
|
7181
7755
|
return [head, "", "In their own words:", "", ...bodies].join(`
|
|
7182
7756
|
`);
|
|
7183
7757
|
}
|
|
7758
|
+
function buildFixNotes(ticket) {
|
|
7759
|
+
if (!ticket.originFindingId)
|
|
7760
|
+
return "";
|
|
7761
|
+
return [
|
|
7762
|
+
"### This ticket fixes a review finding",
|
|
7763
|
+
"",
|
|
7764
|
+
"It was minted from a defect the review of this same lap reported, and its `context` carries the reviewer's repro step verbatim. **Before you declare done, re-run that repro step exactly, and say in your digest that you re-ran it and what happened.** Nothing reviews your change afterwards \u2014 that repro step is the check, and the verify commands run on top of it."
|
|
7765
|
+
].join(`
|
|
7766
|
+
`);
|
|
7767
|
+
}
|
|
7184
7768
|
function buildLapDigestsBlock(digests) {
|
|
7185
7769
|
if (digests.length === 0) {
|
|
7186
7770
|
return "_No implementation ticket in this burn left a digest \u2014 the diff and the feature docs are all you have._";
|
|
@@ -7217,13 +7801,13 @@ function parseEnvFile2(content) {
|
|
|
7217
7801
|
if (line.length === 0 || line.startsWith("#"))
|
|
7218
7802
|
continue;
|
|
7219
7803
|
const body = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
7220
|
-
const
|
|
7221
|
-
if (
|
|
7804
|
+
const eq10 = body.indexOf("=");
|
|
7805
|
+
if (eq10 === -1)
|
|
7222
7806
|
continue;
|
|
7223
|
-
const key = body.slice(0,
|
|
7807
|
+
const key = body.slice(0, eq10).trim();
|
|
7224
7808
|
if (key.length === 0)
|
|
7225
7809
|
continue;
|
|
7226
|
-
let value = body.slice(
|
|
7810
|
+
let value = body.slice(eq10 + 1).trim();
|
|
7227
7811
|
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
7228
7812
|
value = value.slice(1, -1);
|
|
7229
7813
|
}
|
|
@@ -7271,6 +7855,16 @@ function cacheMountFor(pm, hostPath) {
|
|
|
7271
7855
|
const sandboxPath = PM_CACHE_SANDBOX_PATHS[pm];
|
|
7272
7856
|
return sandboxPath ? { hostPath, sandboxPath } : undefined;
|
|
7273
7857
|
}
|
|
7858
|
+
function buildBurnCacheMounts(slot, projectId, sandbox, pm) {
|
|
7859
|
+
if (slot !== undefined) {
|
|
7860
|
+
return {
|
|
7861
|
+
mounts: [{ volume: burnCacheVolumeName(projectId), sandboxPath: BURN_CACHE_MOUNT }],
|
|
7862
|
+
env: pm ? burnCacheEnv(pm) : {}
|
|
7863
|
+
};
|
|
7864
|
+
}
|
|
7865
|
+
const mount = sandbox !== "noSandbox" && pm ? cacheMountFor(pm, burnCacheDir(pm)) : undefined;
|
|
7866
|
+
return { mounts: mount ? [mount] : [], env: {} };
|
|
7867
|
+
}
|
|
7274
7868
|
function codexAuthMountFor(runtime, sandbox, env = process.env, loggedIn = codexLoggedIn) {
|
|
7275
7869
|
if (runtime !== "codex" || sandbox === "noSandbox" || !loggedIn(env))
|
|
7276
7870
|
return;
|
|
@@ -7284,6 +7878,8 @@ function chainSetupCommands(...steps) {
|
|
|
7284
7878
|
return present.length > 0 ? present.join(" && ") : undefined;
|
|
7285
7879
|
}
|
|
7286
7880
|
function resolveBurnWorkspaceMode(config2, platform = process.platform) {
|
|
7881
|
+
if (resolveBurnCacheMode(config2) === "volume")
|
|
7882
|
+
return "slot";
|
|
7287
7883
|
if (config2.sandbox === "noSandbox")
|
|
7288
7884
|
return "mounted";
|
|
7289
7885
|
if (config2.burnWorkspace === "auto")
|
|
@@ -7291,26 +7887,84 @@ function resolveBurnWorkspaceMode(config2, platform = process.platform) {
|
|
|
7291
7887
|
return config2.burnWorkspace;
|
|
7292
7888
|
}
|
|
7293
7889
|
function buildIsolatedSetupCommand(tempBranch, setupCommand, pm) {
|
|
7294
|
-
const
|
|
7295
|
-
|
|
7296
|
-
const cloneAttachmentsDir = `${ISOLATED_REPO_PATH}/${ATTACHMENTS_DIR}`;
|
|
7297
|
-
const parts = [
|
|
7890
|
+
const steps = buildRepoSetupSteps(ISOLATED_REPO_PATH, tempBranch, setupCommand, pm);
|
|
7891
|
+
return [
|
|
7298
7892
|
`git config --global --add safe.directory '*'`,
|
|
7299
7893
|
`git clone ${SANDBOX_WORKSPACE_PATH} ${ISOLATED_REPO_PATH}`,
|
|
7300
|
-
|
|
7301
|
-
|
|
7894
|
+
...steps.pre,
|
|
7895
|
+
...steps.install,
|
|
7896
|
+
...steps.post
|
|
7897
|
+
].join(" && ");
|
|
7898
|
+
}
|
|
7899
|
+
function buildRepoSetupSteps(repoPath, tempBranch, setupCommand, pm) {
|
|
7900
|
+
const hookFile = `${repoPath}/.git/hooks/post-commit`;
|
|
7901
|
+
const attachmentsDir = `${SANDBOX_WORKSPACE_PATH}/${ATTACHMENTS_DIR}`;
|
|
7902
|
+
const repoAttachmentsDir = `${repoPath}/${ATTACHMENTS_DIR}`;
|
|
7903
|
+
const excludeFile = `${repoPath}/.git/info/exclude`;
|
|
7904
|
+
const pre = [
|
|
7905
|
+
`if [ -d "${attachmentsDir}" ]; then mkdir -p "${repoAttachmentsDir}" && cp -r "${attachmentsDir}/." "${repoAttachmentsDir}/" && mkdir -p "${repoPath}/.git/info" && { grep -qxF '${ATTACHMENTS_DIR}/' "${excludeFile}" 2>/dev/null || printf '%s\\n' '${ATTACHMENTS_DIR}/' >> "${excludeFile}"; }; fi`,
|
|
7906
|
+
`printf '#!/bin/sh\\nunset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE\\ngit push --quiet origin HEAD:%s && exit 0\\nsleep 2\\ngit push --quiet origin HEAD:%s && exit 0\\necho "runcastle: commit sync failed (will retry on your next commit); do not re-commit" >&2\\nexit 0\\n' '${tempBranch}' '${tempBranch}' > ${hookFile}`,
|
|
7302
7907
|
`chmod +x ${hookFile}`
|
|
7303
7908
|
];
|
|
7304
7909
|
if (pm === "pnpm" || pm === "yarn") {
|
|
7305
7910
|
const shim = `$HOME/.local/bin/${pm}`;
|
|
7306
|
-
|
|
7911
|
+
pre.push(`mkdir -p "$HOME/.local/bin"`, `printf '#!/bin/sh\\nexec corepack ${pm} "$@"\\n' > "${shim}"`, `chmod +x "${shim}"`);
|
|
7912
|
+
}
|
|
7913
|
+
return {
|
|
7914
|
+
pre,
|
|
7915
|
+
install: setupCommand ? [`cd ${repoPath} && ${setupCommand}`] : [],
|
|
7916
|
+
post: [`git -C ${repoPath} config core.hooksPath ${repoPath}/.git/hooks`]
|
|
7917
|
+
};
|
|
7918
|
+
}
|
|
7919
|
+
function parseSetupMarker(text2) {
|
|
7920
|
+
const m = new RegExp(String.raw`${SETUP_MARKER} cold=([01]) sync_ms=(\d+) install_ms=(\d+)`).exec(text2);
|
|
7921
|
+
if (!m)
|
|
7922
|
+
return;
|
|
7923
|
+
return { cold: m[1] === "1", syncMs: Number(m[2]), installMs: Number(m[3]) };
|
|
7924
|
+
}
|
|
7925
|
+
function takeSetupMarker(worktreePath) {
|
|
7926
|
+
const file = join14(worktreePath, SETUP_MARKER_FILE);
|
|
7927
|
+
let text2;
|
|
7928
|
+
try {
|
|
7929
|
+
text2 = readFileSync9(file, "utf8");
|
|
7930
|
+
} catch {
|
|
7931
|
+
return;
|
|
7307
7932
|
}
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
parts.push(`git -C ${ISOLATED_REPO_PATH} config core.hooksPath ${ISOLATED_REPO_PATH}/.git/hooks`);
|
|
7311
|
-
return parts.join(" && ");
|
|
7933
|
+
rmSync3(file, { force: true });
|
|
7934
|
+
return parseSetupMarker(text2);
|
|
7312
7935
|
}
|
|
7313
|
-
function
|
|
7936
|
+
function fmtSeconds(ms) {
|
|
7937
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
7938
|
+
}
|
|
7939
|
+
function buildSlotStamp(imageName, packageManagerField) {
|
|
7940
|
+
const pm = packageManagerField?.split("@")[0] ?? "none";
|
|
7941
|
+
const major = packageManagerField?.split("@")[1]?.split(".")[0] ?? "any";
|
|
7942
|
+
return `${imageName} node=$(node --version 2>/dev/null) pm=${pm}@${major}`;
|
|
7943
|
+
}
|
|
7944
|
+
function buildSlotSetupCommand(slot, tempBranch, setupCommand, pm, stamp) {
|
|
7945
|
+
const repo = slotRepoPath(slot);
|
|
7946
|
+
const stampFile = slotStampPath(slot);
|
|
7947
|
+
const steps = buildRepoSetupSteps(repo, tempBranch, setupCommand, pm);
|
|
7948
|
+
return [
|
|
7949
|
+
`git config --global --add safe.directory '*'`,
|
|
7950
|
+
`RC_COLD=0`,
|
|
7951
|
+
`RC_STAMP="${stamp}"`,
|
|
7952
|
+
`RC_SYNC_START=$(date +%s%3N)`,
|
|
7953
|
+
`mkdir -p ${slotDirPath(slot)}`,
|
|
7954
|
+
`rm -f ${repo}/.git/*.lock`,
|
|
7955
|
+
`if ! git -C ${repo} rev-parse --git-dir >/dev/null 2>&1; then rm -rf ${repo} && git clone ${SANDBOX_WORKSPACE_PATH} ${repo} && RC_COLD=1; else git -C ${repo} fetch ${SANDBOX_WORKSPACE_PATH} ${tempBranch} && git -C ${repo} reset --hard FETCH_HEAD && git -C ${repo} checkout -B ${tempBranch}; fi`,
|
|
7956
|
+
`git -C ${repo} clean -fd`,
|
|
7957
|
+
`if [ "$(cat ${stampFile} 2>/dev/null)" != "$RC_STAMP" ]; then rm -rf ${repo}/node_modules && git -C ${repo} clean -fdX && RC_COLD=1 && printf '%s\\n' "$RC_STAMP" > ${stampFile}; fi`,
|
|
7958
|
+
...steps.pre,
|
|
7959
|
+
`RC_SYNC_MS=$(( $(date +%s%3N) - RC_SYNC_START ))`,
|
|
7960
|
+
`RC_INSTALL_START=$(date +%s%3N)`,
|
|
7961
|
+
...steps.install,
|
|
7962
|
+
`RC_INSTALL_MS=$(( $(date +%s%3N) - RC_INSTALL_START ))`,
|
|
7963
|
+
...steps.post,
|
|
7964
|
+
`printf '${SETUP_MARKER} cold=%s sync_ms=%s install_ms=%s\\n' "$RC_COLD" "$RC_SYNC_MS" "$RC_INSTALL_MS" > ${SANDBOX_WORKSPACE_PATH}/${SETUP_MARKER_FILE}`
|
|
7965
|
+
].join(" && ");
|
|
7966
|
+
}
|
|
7967
|
+
function buildWorkspaceNotes(mode, repoPath = ISOLATED_REPO_PATH) {
|
|
7314
7968
|
if (mode === "mounted") {
|
|
7315
7969
|
return [
|
|
7316
7970
|
"Work in the current directory \u2014 it is the repo checkout on your branch.",
|
|
@@ -7322,13 +7976,13 @@ function buildWorkspaceNotes(mode) {
|
|
|
7322
7976
|
`);
|
|
7323
7977
|
}
|
|
7324
7978
|
return [
|
|
7325
|
-
`Your working repository is \`${
|
|
7979
|
+
`Your working repository is \`${repoPath}\` \u2014 a clone on the container's fast native filesystem, with dependencies already installed. Do ALL work there: \`cd ${repoPath}\` first; every file you read, edit, test, and commit lives under it.`,
|
|
7326
7980
|
"",
|
|
7327
|
-
`The directory you start in (\`${SANDBOX_WORKSPACE_PATH}\`) is a slow mounted mirror used only to collect your commits \u2014 never edit files, install, or run tests there. Your commits sync back automatically (a post-commit hook pushes them); just commit as normal. If you re-run the dependency install and it reconfigures git hooks (husky), run \`git -C ${
|
|
7981
|
+
`The directory you start in (\`${SANDBOX_WORKSPACE_PATH}\`) is a slow mounted mirror used only to collect your commits \u2014 never edit files, install, or run tests there. Your commits sync back automatically (a post-commit hook pushes them); just commit as normal. If you re-run the dependency install and it reconfigures git hooks (husky), run \`git -C ${repoPath} config core.hooksPath ${repoPath}/.git/hooks\` afterwards so the sync hook stays armed.`,
|
|
7328
7982
|
"",
|
|
7329
|
-
`If you are blocked and write \`BLOCKED.md\`, write it at \`${
|
|
7983
|
+
`If you are blocked and write \`BLOCKED.md\`, write it at \`${repoPath}/BLOCKED.md\` AND copy it to \`${SANDBOX_WORKSPACE_PATH}/BLOCKED.md\` so the orchestrator can see it.`,
|
|
7330
7984
|
"",
|
|
7331
|
-
`Write \`DIGEST.md\` at \`${SANDBOX_WORKSPACE_PATH}/DIGEST.md\` \u2014 the mounted mirror, so the host can see it \u2014 and NOT inside \`${
|
|
7985
|
+
`Write \`DIGEST.md\` at \`${SANDBOX_WORKSPACE_PATH}/DIGEST.md\` \u2014 the mounted mirror, so the host can see it \u2014 and NOT inside \`${repoPath}\`, where it would be committed with your work.`,
|
|
7332
7986
|
"",
|
|
7333
7987
|
"**Those paths are the only authoritative ones** \u2014 nothing later in this prompt overrides them."
|
|
7334
7988
|
].join(`
|
|
@@ -7473,8 +8127,9 @@ function createToolTimer() {
|
|
|
7473
8127
|
if (event.type === "raw")
|
|
7474
8128
|
return;
|
|
7475
8129
|
const at = event.timestamp.getTime();
|
|
7476
|
-
if (iteration !== null && event.iteration !== iteration)
|
|
7477
|
-
pending =
|
|
8130
|
+
if (iteration !== null && event.iteration !== iteration && pending) {
|
|
8131
|
+
pending = { category: "setup", at: pending.at };
|
|
8132
|
+
}
|
|
7478
8133
|
iteration = event.iteration;
|
|
7479
8134
|
if (pending) {
|
|
7480
8135
|
const gap = at - pending.at;
|
|
@@ -7491,7 +8146,11 @@ function createToolTimer() {
|
|
|
7491
8146
|
pending = { category: "model", at };
|
|
7492
8147
|
}
|
|
7493
8148
|
};
|
|
7494
|
-
|
|
8149
|
+
const beginSetup = (at = Date.now()) => {
|
|
8150
|
+
pending = { category: "setup", at };
|
|
8151
|
+
iteration = null;
|
|
8152
|
+
};
|
|
8153
|
+
return { onEvent, beginSetup, summary: () => ({ totalMs, calls, byCategory }) };
|
|
7495
8154
|
}
|
|
7496
8155
|
function formatTimingSummary(s) {
|
|
7497
8156
|
if (s.totalMs === 0)
|
|
@@ -7499,6 +8158,22 @@ function formatTimingSummary(s) {
|
|
|
7499
8158
|
const parts = Object.entries(s.byCategory).sort((a, b) => (b[1]?.ms ?? 0) - (a[1]?.ms ?? 0)).slice(0, 5).map(([c, t]) => `${c} ${Math.round((t?.ms ?? 0) / s.totalMs * 100)}%`);
|
|
7500
8159
|
return `${Math.round(s.totalMs / 60000)}min across ${s.calls} tool call(s) \u2014 ${parts.join(", ")}`;
|
|
7501
8160
|
}
|
|
8161
|
+
function buildTicketTiming(summary, startedAt, endedAt) {
|
|
8162
|
+
return { ...summary, startedAt, endedAt, wallMs: Math.max(0, endedAt - startedAt) };
|
|
8163
|
+
}
|
|
8164
|
+
function formatTicketTiming(t) {
|
|
8165
|
+
const clock = fmtClock(Math.round(t.wallMs / 1000));
|
|
8166
|
+
const bare = t.calls === 0 && t.totalMs === 0;
|
|
8167
|
+
return bare ? clock : `${clock} (${formatTimingSummary(t)})`;
|
|
8168
|
+
}
|
|
8169
|
+
function emitTicketTiming(ctx, ticket, timing) {
|
|
8170
|
+
ctx.emitEvent({
|
|
8171
|
+
type: "ticket.timing",
|
|
8172
|
+
message: `ticket ${ticket.seq} spent ${formatTicketTiming(timing)}`,
|
|
8173
|
+
ticketId: ticket.id,
|
|
8174
|
+
data: timing
|
|
8175
|
+
});
|
|
8176
|
+
}
|
|
7502
8177
|
function interpretRunResult(result, blockedContent) {
|
|
7503
8178
|
const commits = result.commits.map((c) => c.sha);
|
|
7504
8179
|
if (commits.length > 0)
|
|
@@ -7519,10 +8194,20 @@ function isMergeConflictError(err) {
|
|
|
7519
8194
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
7520
8195
|
return /conflict/i.test(msg) || /git branch -D/i.test(msg) || /automatic merge failed/i.test(msg);
|
|
7521
8196
|
}
|
|
8197
|
+
function missingAgentBinaryMessage(err, runtime, image) {
|
|
8198
|
+
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
8199
|
+
const binary = AGENT_BINARY[runtime];
|
|
8200
|
+
const missing = new RegExp(`\\b${binary}\\b[^\\n]*(?:exited with code 127|command not found|not found|no such file or directory)`, "i");
|
|
8201
|
+
if (!missing.test(msg))
|
|
8202
|
+
return;
|
|
8203
|
+
return `${binary} is not installed in image ${image} \u2014 the image predates the burner Dockerfile. Rebuild it from Settings \u2192 AFK burns (Rebuild image).`;
|
|
8204
|
+
}
|
|
7522
8205
|
function classifyTicketRunError(err, runtime) {
|
|
7523
8206
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
7524
8207
|
const runtimes = runtime ? [runtime] : ["claude-code", "codex"];
|
|
7525
8208
|
const forRuntimes = (table) => runtimes.flatMap((r) => table[r]);
|
|
8209
|
+
if (runtimes.some((r) => missingAgentBinaryMessage(msg, r, "image")))
|
|
8210
|
+
return "fatal";
|
|
7526
8211
|
if ([...FATAL_ERROR_PATTERNS, ...forRuntimes(RUNTIME_FATAL_ERROR_PATTERNS)].some((p) => p.test(msg)))
|
|
7527
8212
|
return "fatal";
|
|
7528
8213
|
if ([...RETRYABLE_ERROR_PATTERNS, ...forRuntimes(RUNTIME_RETRYABLE_ERROR_PATTERNS)].some((p) => p.test(msg)))
|
|
@@ -7636,13 +8321,42 @@ function failedBlockerNote(seqs) {
|
|
|
7636
8321
|
}
|
|
7637
8322
|
async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
7638
8323
|
const width = Math.max(1, Math.floor(concurrency));
|
|
7639
|
-
const
|
|
7640
|
-
const
|
|
7641
|
-
const
|
|
8324
|
+
const scheduled = [...tickets2];
|
|
8325
|
+
const bySeq = indexBySeq(scheduled);
|
|
8326
|
+
const status = new Map(scheduled.map((t) => [t.seq, t.status]));
|
|
8327
|
+
const pending = new Set(scheduled.filter((t) => t.status === "pending").map((t) => t.seq));
|
|
7642
8328
|
const inFlight = new Map;
|
|
7643
8329
|
const digests = [];
|
|
8330
|
+
let admitted = 0;
|
|
7644
8331
|
const satisfied = (t, s) => s === "done" || s === "cancelled" || isReviewTicket(t) && s === "failed";
|
|
7645
|
-
const implementationsSettled = () =>
|
|
8332
|
+
const implementationsSettled = () => scheduled.every((t) => isReviewTicket(t) || !pending.has(t.seq) && !inFlight.has(t.seq));
|
|
8333
|
+
const admitNewTickets = () => {
|
|
8334
|
+
const added = [];
|
|
8335
|
+
for (const t of ctx.listTickets?.() ?? []) {
|
|
8336
|
+
if (bySeq.has(t.seq))
|
|
8337
|
+
continue;
|
|
8338
|
+
scheduled.push(t);
|
|
8339
|
+
bySeq.set(t.seq, t);
|
|
8340
|
+
status.set(t.seq, t.status);
|
|
8341
|
+
if (t.status !== "pending")
|
|
8342
|
+
continue;
|
|
8343
|
+
pending.add(t.seq);
|
|
8344
|
+
added.push(t.seq);
|
|
8345
|
+
}
|
|
8346
|
+
if (added.length === 0)
|
|
8347
|
+
return;
|
|
8348
|
+
admitted += added.length;
|
|
8349
|
+
ctx.emitEvent({
|
|
8350
|
+
type: "burn.admitted",
|
|
8351
|
+
message: `${added.length} ticket(s) minted during this run joined it: ${added.join(", ")}`,
|
|
8352
|
+
data: { seqs: added }
|
|
8353
|
+
});
|
|
8354
|
+
};
|
|
8355
|
+
const mirrorFinding = (t, progress, reason) => {
|
|
8356
|
+
if (!t.originFindingId)
|
|
8357
|
+
return;
|
|
8358
|
+
ctx.updateFinding?.(t.originFindingId, progress, reason);
|
|
8359
|
+
};
|
|
7646
8360
|
const readyState = (seq) => {
|
|
7647
8361
|
const t = bySeq.get(seq);
|
|
7648
8362
|
if (!t)
|
|
@@ -7658,7 +8372,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
7658
8372
|
return t.blockedBy.every((b) => satisfied(t, status.get(b))) ? "ready" : "wait";
|
|
7659
8373
|
};
|
|
7660
8374
|
const harvestDigest2 = (t, digest) => {
|
|
7661
|
-
const failed = isReviewTicket(t) ?
|
|
8375
|
+
const failed = isReviewTicket(t) ? scheduled.filter((x) => !isReviewTicket(x) && status.get(x.seq) === "failed").map((x) => x.seq) : [];
|
|
7662
8376
|
const body = [failed.length > 0 ? failedBlockerNote(failed) : undefined, digest].filter(Boolean).join(`
|
|
7663
8377
|
|
|
7664
8378
|
`);
|
|
@@ -7671,6 +8385,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
7671
8385
|
if (!t)
|
|
7672
8386
|
return;
|
|
7673
8387
|
ctx.updateTicket(t.id, { status: "failed", error, ...digest ? { digest } : {} });
|
|
8388
|
+
mirrorFinding(t, "failed", error);
|
|
7674
8389
|
harvestDigest2(t, digest);
|
|
7675
8390
|
if (extra)
|
|
7676
8391
|
ctx.emitEvent({ ...extra, ticketId: t.id });
|
|
@@ -7681,6 +8396,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
7681
8396
|
return;
|
|
7682
8397
|
status.set(seq, "burning");
|
|
7683
8398
|
ctx.updateTicket(t.id, { status: "burning" });
|
|
8399
|
+
mirrorFinding(t, "fixing");
|
|
7684
8400
|
ctx.emitEvent({
|
|
7685
8401
|
type: "ticket.burning",
|
|
7686
8402
|
message: `burning ticket ${t.seq}: ${t.title}`,
|
|
@@ -7690,6 +8406,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
7690
8406
|
if (outcome.status === "done") {
|
|
7691
8407
|
status.set(seq, "done");
|
|
7692
8408
|
ctx.updateTicket(t.id, { status: "done", commits: outcome.commits, digest: outcome.digest });
|
|
8409
|
+
mirrorFinding(t, "fixed");
|
|
7693
8410
|
ctx.emitEvent({
|
|
7694
8411
|
type: "ticket.done",
|
|
7695
8412
|
message: `ticket ${t.seq} done \u2014 ${outcome.commits.length} commit(s)`,
|
|
@@ -7713,6 +8430,8 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
7713
8430
|
data: { error: outcome.error }
|
|
7714
8431
|
});
|
|
7715
8432
|
}
|
|
8433
|
+
if (isReviewTicket(t))
|
|
8434
|
+
admitNewTickets();
|
|
7716
8435
|
};
|
|
7717
8436
|
while (pending.size > 0 || inFlight.size > 0) {
|
|
7718
8437
|
ctx.signal.throwIfAborted();
|
|
@@ -7769,7 +8488,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
7769
8488
|
for (const s of status.values())
|
|
7770
8489
|
if (s === "done")
|
|
7771
8490
|
done += 1;
|
|
7772
|
-
return { done, digests };
|
|
8491
|
+
return { done, admitted, digests };
|
|
7773
8492
|
}
|
|
7774
8493
|
function composeRunDigest(entries) {
|
|
7775
8494
|
if (entries.length === 0)
|
|
@@ -7797,11 +8516,27 @@ async function burnRun(ctx, deps) {
|
|
|
7797
8516
|
const tickets2 = ctx.tickets;
|
|
7798
8517
|
const burnable = tickets2.filter((t) => t.status !== "cancelled");
|
|
7799
8518
|
const cancelled = tickets2.length - burnable.length;
|
|
7800
|
-
const total = burnable.length;
|
|
7801
8519
|
if (deps.config.sandbox !== "noSandbox" && !deps.hasAuthToken) {
|
|
7802
8520
|
ctx.emitEvent({ type: AUTH_MISSING_EVENT, message: RUNTIME_AUTH_SETUP_HINT[deps.runtime] });
|
|
7803
8521
|
return { status: "failed", summary: "burn aborted: auth token missing" };
|
|
7804
8522
|
}
|
|
8523
|
+
if (deps.config.sandbox !== "noSandbox" && deps.exec) {
|
|
8524
|
+
const image = resolveSandboxImage(deps.config);
|
|
8525
|
+
const binary = RUNTIME_BINARY[deps.runtime];
|
|
8526
|
+
const probe = await deps.exec(deps.config.sandbox, [
|
|
8527
|
+
"run",
|
|
8528
|
+
"--rm",
|
|
8529
|
+
"--entrypoint",
|
|
8530
|
+
binary,
|
|
8531
|
+
image,
|
|
8532
|
+
"--version"
|
|
8533
|
+
]);
|
|
8534
|
+
if (!probe.ok || probe.code !== 0) {
|
|
8535
|
+
const message = missingImageRuntimeMessage(deps.runtime, image);
|
|
8536
|
+
ctx.emitEvent({ type: IMAGE_RUNTIME_MISSING_EVENT, message });
|
|
8537
|
+
return { status: "failed", summary: message };
|
|
8538
|
+
}
|
|
8539
|
+
}
|
|
7805
8540
|
const cycle = detectCycle(burnable);
|
|
7806
8541
|
if (cycle) {
|
|
7807
8542
|
const path = cycle.join(" \u2192 ");
|
|
@@ -7812,7 +8547,8 @@ async function burnRun(ctx, deps) {
|
|
|
7812
8547
|
});
|
|
7813
8548
|
return { status: "failed", summary: `dependency cycle: ${path}` };
|
|
7814
8549
|
}
|
|
7815
|
-
const { done, digests } = await burnTickets(ctx, tickets2, gateTicketAuth(deps), deps.concurrency);
|
|
8550
|
+
const { done, admitted, digests } = await burnTickets(ctx, tickets2, gateTicketAuth(deps), deps.concurrency);
|
|
8551
|
+
const total = burnable.length + admitted;
|
|
7816
8552
|
const summary = cancelled > 0 ? `${done}/${total} tickets done (${cancelled} cancelled)` : `${done}/${total} tickets done`;
|
|
7817
8553
|
ctx.emitEvent({ type: "burn.summary", message: summary, data: { done, total, cancelled } });
|
|
7818
8554
|
const digest = composeRunDigest(digests);
|
|
@@ -7825,7 +8561,7 @@ function resolverTemplatePath() {
|
|
|
7825
8561
|
return burnerAssetPath("resolve-conflict.md");
|
|
7826
8562
|
}
|
|
7827
8563
|
function burnerAssetPath(file) {
|
|
7828
|
-
const here = dirname6(
|
|
8564
|
+
const here = dirname6(fileURLToPath6(import.meta.url));
|
|
7829
8565
|
return join14(resolveSkillsRoot(here), "burner", file);
|
|
7830
8566
|
}
|
|
7831
8567
|
function listFeatureDocs(docsDir, prefix = "") {
|
|
@@ -7986,8 +8722,8 @@ function buildAgentExtraArgs(runtime, options) {
|
|
|
7986
8722
|
function quoteArg(value) {
|
|
7987
8723
|
return `"${value}"`;
|
|
7988
8724
|
}
|
|
7989
|
-
function selectSandbox(config2, mounts = []) {
|
|
7990
|
-
const imageOpts = buildSandboxOptions(config2, mounts);
|
|
8725
|
+
function selectSandbox(config2, mounts = [], env = {}) {
|
|
8726
|
+
const imageOpts = buildSandboxOptions(config2, mounts, env);
|
|
7991
8727
|
switch (config2.sandbox) {
|
|
7992
8728
|
case "docker":
|
|
7993
8729
|
return docker(imageOpts);
|
|
@@ -7999,17 +8735,46 @@ function selectSandbox(config2, mounts = []) {
|
|
|
7999
8735
|
throw new Error(`no sandbox provider for sandbox "${config2.sandbox}" \u2014 refusing to run the agent unsandboxed`);
|
|
8000
8736
|
}
|
|
8001
8737
|
}
|
|
8002
|
-
function buildSandboxOptions(config2, mounts = []) {
|
|
8738
|
+
function buildSandboxOptions(config2, mounts = [], env = {}) {
|
|
8003
8739
|
return {
|
|
8004
8740
|
imageName: resolveSandboxImage(config2),
|
|
8005
8741
|
...mounts.length > 0 ? { mounts } : {},
|
|
8006
|
-
...config2.burnCpus !== undefined ? { cpus: config2.burnCpus } : {}
|
|
8742
|
+
...config2.burnCpus !== undefined ? { cpus: config2.burnCpus } : {},
|
|
8743
|
+
...Object.keys(env).length > 0 ? { env } : {}
|
|
8007
8744
|
};
|
|
8008
8745
|
}
|
|
8009
|
-
async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget, blocks, runCtx) {
|
|
8746
|
+
async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget, ensureCacheVolume, blocks, runCtx) {
|
|
8747
|
+
const allocator = resolveBurnCacheMode(config2) === "volume" ? getBurnSlotAllocator(config2.burnConcurrency) : undefined;
|
|
8748
|
+
return withBurnCacheSlot(allocator, async (slot) => {
|
|
8749
|
+
if (slot !== undefined)
|
|
8750
|
+
await ensureCacheVolume();
|
|
8751
|
+
return burnTicket(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget, blocks, runCtx, slot);
|
|
8752
|
+
});
|
|
8753
|
+
}
|
|
8754
|
+
async function withBurnCacheSlot(allocator, body) {
|
|
8755
|
+
if (!allocator)
|
|
8756
|
+
return body(undefined);
|
|
8757
|
+
let slot;
|
|
8758
|
+
try {
|
|
8759
|
+
slot = allocator.claim();
|
|
8760
|
+
} catch (err) {
|
|
8761
|
+
if (!(err instanceof BurnSlotsExhaustedError))
|
|
8762
|
+
throw err;
|
|
8763
|
+
}
|
|
8764
|
+
if (slot === undefined)
|
|
8765
|
+
return body(undefined);
|
|
8766
|
+
try {
|
|
8767
|
+
return await body(slot);
|
|
8768
|
+
} finally {
|
|
8769
|
+
allocator.release(slot);
|
|
8770
|
+
}
|
|
8771
|
+
}
|
|
8772
|
+
async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget, blocks, runCtx, slot) {
|
|
8010
8773
|
const { project, feature } = ctx;
|
|
8011
|
-
const
|
|
8012
|
-
|
|
8774
|
+
const startedAt = Date.now();
|
|
8775
|
+
const workspaceMode = resolveBurnWorkspaceMode(slot === undefined ? { ...config2, burnCache: "off" } : config2);
|
|
8776
|
+
const agentRepoPath = slot !== undefined ? slotRepoPath(slot) : ISOLATED_REPO_PATH;
|
|
8777
|
+
if (workspaceMode !== "mounted")
|
|
8013
8778
|
await ensureIsolatedPushTarget();
|
|
8014
8779
|
const attachments = attachmentSources(ticket.context);
|
|
8015
8780
|
const attachmentCommands = buildAttachmentCopyCommands(attachments);
|
|
@@ -8025,7 +8790,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8025
8790
|
const ticketJson = buildTicketJson(ticket);
|
|
8026
8791
|
const featureBrief = buildFeatureBrief(feature);
|
|
8027
8792
|
const docsDigest = blocks.docsDigest;
|
|
8028
|
-
const workspaceNotes = buildWorkspaceNotes(workspaceMode);
|
|
8793
|
+
const workspaceNotes = buildWorkspaceNotes(workspaceMode, agentRepoPath);
|
|
8029
8794
|
const guardInstalled = config2.burnGuard && config2.sandbox !== "noSandbox";
|
|
8030
8795
|
const prepared = resolvePreparedSettings(config2, project);
|
|
8031
8796
|
const verifyNotes = buildVerifyNotes(prepared);
|
|
@@ -8038,18 +8803,18 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8038
8803
|
DRIVE_NOTES: blocks.driveNotes,
|
|
8039
8804
|
GUARD_NOTES: buildGuardNotes(guardInstalled),
|
|
8040
8805
|
TICKET_JSON: ticketJson,
|
|
8041
|
-
BLOCKERS: buildBlockersBlock(ticket.blockedBy, runCtx.digests)
|
|
8806
|
+
BLOCKERS: buildBlockersBlock(ticket.blockedBy, runCtx.digests),
|
|
8807
|
+
FIX_NOTES: buildFixNotes(ticket)
|
|
8042
8808
|
});
|
|
8043
8809
|
const toolchain = readRepoToolchain(project.repoPath);
|
|
8044
8810
|
const pm = detectPackageManager(toolchain);
|
|
8045
8811
|
const setupCommand = resolveSetupCommand(toolchain, prepared.setupCommand);
|
|
8046
|
-
const
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8812
|
+
const cache = buildBurnCacheMounts(slot, project.id, config2.sandbox, pm);
|
|
8813
|
+
const sandboxEnv = cache.env;
|
|
8814
|
+
const mounts = [...cache.mounts];
|
|
8815
|
+
for (const mount of cache.mounts) {
|
|
8816
|
+
if ("hostPath" in mount)
|
|
8050
8817
|
mkdirSync9(mount.hostPath, { recursive: true });
|
|
8051
|
-
mounts.push(mount);
|
|
8052
|
-
}
|
|
8053
8818
|
}
|
|
8054
8819
|
const codexAuthMount = codexAuthMountFor(model.runtime, config2.sandbox);
|
|
8055
8820
|
if (codexAuthMount)
|
|
@@ -8057,14 +8822,37 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8057
8822
|
const guardInstall = guardInstalled ? buildGuardInstallCommand(model.runtime) : undefined;
|
|
8058
8823
|
const withPrelude = (setup) => chainSetupCommands(codexAuthMount ? buildCodexAuthCopyCommand() : undefined, guardInstall, setup);
|
|
8059
8824
|
const agentOptions = guardInstalled ? { bypassHookTrust: true } : {};
|
|
8825
|
+
const slotStamp = buildSlotStamp(resolveSandboxImage(config2), toolchain.packageManagerField);
|
|
8826
|
+
const setupHookFor = (branch) => withPrelude(slot !== undefined ? buildSlotSetupCommand(slot, branch, setupCommand, pm, slotStamp) : workspaceMode === "isolated" ? buildIsolatedSetupCommand(branch, setupCommand, pm) : setupCommand);
|
|
8060
8827
|
mkdirSync9(logsDir(), { recursive: true });
|
|
8061
8828
|
const logFilePath = join14(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
|
|
8062
8829
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
8063
8830
|
beginTranscript(ticket.id);
|
|
8064
8831
|
const timer = createToolTimer();
|
|
8832
|
+
const setupRun = { worktree: undefined, iteration: null };
|
|
8833
|
+
const consumeSetupMarker = (iteration) => {
|
|
8834
|
+
if (slot === undefined || !setupRun.worktree || setupRun.iteration === iteration)
|
|
8835
|
+
return;
|
|
8836
|
+
setupRun.iteration = iteration;
|
|
8837
|
+
const marker = takeSetupMarker(setupRun.worktree);
|
|
8838
|
+
if (!marker)
|
|
8839
|
+
return;
|
|
8840
|
+
ctx.emitEvent({
|
|
8841
|
+
type: "burn.setup",
|
|
8842
|
+
message: `ticket ${ticket.seq}: cache slot ${slot} ${marker.cold ? "cold" : "warm"} \u2014 synced in ${fmtSeconds(marker.syncMs)}, installed in ${fmtSeconds(marker.installMs)}`,
|
|
8843
|
+
ticketId: ticket.id,
|
|
8844
|
+
data: { slot, cold: marker.cold, syncMs: marker.syncMs, installMs: marker.installMs }
|
|
8845
|
+
});
|
|
8846
|
+
};
|
|
8847
|
+
const beginSetupSpan = (branch) => {
|
|
8848
|
+
setupRun.worktree = burnWorktreePath(project.repoPath, branch);
|
|
8849
|
+
setupRun.iteration = null;
|
|
8850
|
+
timer.beginSetup();
|
|
8851
|
+
};
|
|
8065
8852
|
const onStreamEvent = (event) => {
|
|
8066
8853
|
throttle.onEvent(event);
|
|
8067
8854
|
timer.onEvent(event);
|
|
8855
|
+
consumeSetupMarker(event.iteration);
|
|
8068
8856
|
if (event.type === "text") {
|
|
8069
8857
|
appendTranscript(ticket.id, { kind: "text", text: event.message });
|
|
8070
8858
|
} else if (event.type === "toolCall") {
|
|
@@ -8100,6 +8888,12 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8100
8888
|
const preserveChain = (branch) => {
|
|
8101
8889
|
ctx.updateTicket(ticket.id, { attemptBranch: branch });
|
|
8102
8890
|
};
|
|
8891
|
+
const discards = new Map;
|
|
8892
|
+
const discardWorktree = (branch) => {
|
|
8893
|
+
const pending = discards.get(branch) ?? cleanupBurnWorktree(project.repoPath, branch);
|
|
8894
|
+
discards.set(branch, pending);
|
|
8895
|
+
return pending;
|
|
8896
|
+
};
|
|
8103
8897
|
const runResolver = async (input) => {
|
|
8104
8898
|
const resolveBranch = ticketBranchName(feature.slug, ticket.seq, newId("r").slice(2, 10));
|
|
8105
8899
|
const otherSide = await commitSummaries(project.repoPath, input.branch, feature.branch);
|
|
@@ -8115,7 +8909,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8115
8909
|
OTHER_SIDE: buildOtherSideBlock(otherSide),
|
|
8116
8910
|
MERGE_COMMAND: resolveMergeCommand(workspaceMode, feature.branch)
|
|
8117
8911
|
});
|
|
8118
|
-
const hookCommand =
|
|
8912
|
+
const hookCommand = setupHookFor(resolveBranch);
|
|
8119
8913
|
appendTranscript(ticket.id, {
|
|
8120
8914
|
kind: "text",
|
|
8121
8915
|
text: `
|
|
@@ -8128,9 +8922,10 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8128
8922
|
};
|
|
8129
8923
|
let result;
|
|
8130
8924
|
try {
|
|
8925
|
+
beginSetupSpan(resolveBranch);
|
|
8131
8926
|
result = await run2({
|
|
8132
8927
|
agent: buildBurnAgent(config2, token, model, agentOptions),
|
|
8133
|
-
sandbox: selectSandbox(config2, mounts),
|
|
8928
|
+
sandbox: selectSandbox(config2, mounts, sandboxEnv),
|
|
8134
8929
|
cwd: project.repoPath,
|
|
8135
8930
|
prompt,
|
|
8136
8931
|
branchStrategy: { type: "branch", branch: resolveBranch, baseBranch: input.branch },
|
|
@@ -8147,6 +8942,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8147
8942
|
logging: { type: "file", path: logFilePath, onAgentStreamEvent: onStreamEvent }
|
|
8148
8943
|
});
|
|
8149
8944
|
} catch (err) {
|
|
8945
|
+
await discardWorktree(resolveBranch);
|
|
8150
8946
|
if (ctx.signal.aborted)
|
|
8151
8947
|
throw err;
|
|
8152
8948
|
const tip = await bestTip();
|
|
@@ -8158,9 +8954,9 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8158
8954
|
error: ticketAbort.signal.aborted ? "stopped by user during conflict resolution" : errorHeadline(err instanceof Error ? err.message : String(err))
|
|
8159
8955
|
};
|
|
8160
8956
|
}
|
|
8161
|
-
await cleanupBurnWorktree(project.repoPath, resolveBranch);
|
|
8162
8957
|
}
|
|
8163
8958
|
const blocked = readAgentFile([result?.preservedWorktreePath, project.repoPath], "BLOCKED.md");
|
|
8959
|
+
await discardWorktree(resolveBranch);
|
|
8164
8960
|
if (blocked !== undefined && blocked.trim().length > 0) {
|
|
8165
8961
|
return {
|
|
8166
8962
|
ok: false,
|
|
@@ -8250,17 +9046,18 @@ ${blocked.trim()}`
|
|
|
8250
9046
|
let tempBranch = "";
|
|
8251
9047
|
for (let attempt = 1;; attempt++) {
|
|
8252
9048
|
tempBranch = ticketBranchName(feature.slug, ticket.seq, newId("b").slice(2, 10));
|
|
8253
|
-
const hookCommand =
|
|
9049
|
+
const hookCommand = setupHookFor(tempBranch);
|
|
8254
9050
|
if (hookCommand && attempt === 1) {
|
|
8255
9051
|
ctx.emitEvent({
|
|
8256
9052
|
type: "burn.setup",
|
|
8257
|
-
message: workspaceMode === "isolated" ? `preparing isolated workspace (native-FS clone)${setupCommand ? ` + deps install: ${setupCommand}` : ""}` : `installing deps before agent start: ${setupCommand}`,
|
|
8258
|
-
ticketId: ticket.id
|
|
9053
|
+
message: slot !== undefined ? `preparing cache slot ${slot}${setupCommand ? ` + deps install: ${setupCommand}` : ""}` : workspaceMode === "isolated" ? `preparing isolated workspace (native-FS clone)${setupCommand ? ` + deps install: ${setupCommand}` : ""}` : `installing deps before agent start: ${setupCommand}`,
|
|
9054
|
+
ticketId: ticket.id,
|
|
9055
|
+
...slot !== undefined ? { data: { slot } } : {}
|
|
8259
9056
|
});
|
|
8260
9057
|
}
|
|
8261
9058
|
const runOptions = {
|
|
8262
9059
|
agent: buildBurnAgent(config2, token, model, agentOptions),
|
|
8263
|
-
sandbox: selectSandbox(config2, mounts),
|
|
9060
|
+
sandbox: selectSandbox(config2, mounts, sandboxEnv),
|
|
8264
9061
|
cwd: project.repoPath,
|
|
8265
9062
|
prompt: retryNotes ? `${basePrompt}
|
|
8266
9063
|
|
|
@@ -8285,14 +9082,17 @@ ${retryNotes}` : basePrompt,
|
|
|
8285
9082
|
};
|
|
8286
9083
|
try {
|
|
8287
9084
|
await excludeAttachments();
|
|
9085
|
+
beginSetupSpan(tempBranch);
|
|
8288
9086
|
result = await run2(runOptions);
|
|
8289
9087
|
await clearAttachmentsFor(tempBranch);
|
|
8290
9088
|
break;
|
|
8291
9089
|
} catch (err) {
|
|
8292
9090
|
await clearAttachmentsFor(tempBranch);
|
|
9091
|
+
await discardWorktree(tempBranch);
|
|
8293
9092
|
if (ctx.signal.aborted)
|
|
8294
9093
|
throw err;
|
|
8295
9094
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9095
|
+
const missingBinary = missingAgentBinaryMessage(err, model.runtime, resolveSandboxImage(config2));
|
|
8296
9096
|
const salvaged = await branchCommitsAhead(project.repoPath, feature.branch, tempBranch);
|
|
8297
9097
|
if (salvaged.length > 0)
|
|
8298
9098
|
baseBranch = tempBranch;
|
|
@@ -8309,7 +9109,7 @@ ${retryNotes}` : basePrompt,
|
|
|
8309
9109
|
};
|
|
8310
9110
|
}
|
|
8311
9111
|
if (isWorktreeTeardownError(err)) {
|
|
8312
|
-
const removed = await
|
|
9112
|
+
const removed = await discardWorktree(tempBranch);
|
|
8313
9113
|
if (salvaged.length > 0) {
|
|
8314
9114
|
ctx.emitEvent({
|
|
8315
9115
|
type: "burn.worktree.teardown-failed",
|
|
@@ -8334,6 +9134,11 @@ ${retryNotes}` : basePrompt,
|
|
|
8334
9134
|
}
|
|
8335
9135
|
};
|
|
8336
9136
|
}
|
|
9137
|
+
if (missingBinary) {
|
|
9138
|
+
if (salvaged.length > 0)
|
|
9139
|
+
preserveChain(tempBranch);
|
|
9140
|
+
return { status: "failed", error: missingBinary };
|
|
9141
|
+
}
|
|
8337
9142
|
if (classifyTicketRunError(err, model.runtime) === "retryable" && attempt < maxAttempts) {
|
|
8338
9143
|
const headline = errorHeadline(msg);
|
|
8339
9144
|
retryNotes = buildRetryNotes({ error: headline, commitCount: salvaged.length });
|
|
@@ -8375,6 +9180,7 @@ ${retryNotes}` : basePrompt,
|
|
|
8375
9180
|
const agentFileDirs = [result.preservedWorktreePath, project.repoPath];
|
|
8376
9181
|
const blocked = readAgentFile(agentFileDirs, "BLOCKED.md");
|
|
8377
9182
|
harvestedDigest = harvestDigest(agentFileDirs);
|
|
9183
|
+
await discardWorktree(tempBranch);
|
|
8378
9184
|
const chain = await branchCommitsAhead(project.repoPath, feature.branch, tempBranch);
|
|
8379
9185
|
let outcome;
|
|
8380
9186
|
if (result.commits.length === 0 && blocked !== undefined && blocked.trim().length > 0) {
|
|
@@ -8395,27 +9201,43 @@ ${blocked.trim()}` };
|
|
|
8395
9201
|
releaseTicketAbort(ticket.id);
|
|
8396
9202
|
throttle.flush();
|
|
8397
9203
|
endTranscript(ticket.id);
|
|
8398
|
-
|
|
8399
|
-
if (timing.calls > 0) {
|
|
8400
|
-
ctx.emitEvent({
|
|
8401
|
-
type: "ticket.timing",
|
|
8402
|
-
message: `ticket ${ticket.seq} spent ${formatTimingSummary(timing)}`,
|
|
8403
|
-
ticketId: ticket.id,
|
|
8404
|
-
data: timing
|
|
8405
|
-
});
|
|
8406
|
-
}
|
|
9204
|
+
emitTicketTiming(ctx, ticket, buildTicketTiming(timer.summary(), startedAt, Date.now()));
|
|
8407
9205
|
}
|
|
8408
9206
|
}
|
|
8409
9207
|
function resolveTicketModel(config2, project, runOverride, ticket) {
|
|
8410
|
-
|
|
9208
|
+
const step = isReviewTicket(ticket) ? "review" : "implement";
|
|
9209
|
+
return resolveModelEntry(step, config2, project, ticket.model ?? runOverride);
|
|
8411
9210
|
}
|
|
8412
9211
|
function resolveBurnDeps(ctx) {
|
|
8413
9212
|
const config2 = loadConfig();
|
|
8414
9213
|
const model = resolveModelEntry("implement", config2, ctx.project, ctx.modelOverride);
|
|
8415
9214
|
const token = readTokenFromEnvFile(envPath(), model.runtime);
|
|
9215
|
+
const exec = createSystemExec({ cwd: ctx.project.repoPath });
|
|
9216
|
+
const imageProbeCache = new Map;
|
|
9217
|
+
const cachedExec = (command, args) => {
|
|
9218
|
+
const key = `${resolveSandboxImage(config2)}\x00${model.runtime}`;
|
|
9219
|
+
let result = imageProbeCache.get(key);
|
|
9220
|
+
if (!result) {
|
|
9221
|
+
result = exec(command, args);
|
|
9222
|
+
imageProbeCache.set(key, result);
|
|
9223
|
+
}
|
|
9224
|
+
return result;
|
|
9225
|
+
};
|
|
8416
9226
|
const land = createSerialQueue();
|
|
8417
9227
|
let pushTargetReady;
|
|
8418
9228
|
const ensureIsolatedPushTarget = () => pushTargetReady ??= allowPushToCheckedOutBranches(ctx.project.repoPath);
|
|
9229
|
+
let cacheVolumeReady;
|
|
9230
|
+
const ensureCacheVolume = () => {
|
|
9231
|
+
if (resolveBurnCacheMode(config2) !== "volume")
|
|
9232
|
+
return Promise.resolve();
|
|
9233
|
+
const engine = config2.sandbox === "podman" ? "podman" : "docker";
|
|
9234
|
+
return cacheVolumeReady ??= ensureBurnCacheVolume({
|
|
9235
|
+
engine,
|
|
9236
|
+
imageName: resolveSandboxImage(config2),
|
|
9237
|
+
projectId: ctx.project.id,
|
|
9238
|
+
exec
|
|
9239
|
+
});
|
|
9240
|
+
};
|
|
8419
9241
|
const docs2 = readDocsDigest(ctx.project.id, ctx.feature.slug);
|
|
8420
9242
|
emitDocsDigestEvent(ctx, docs2);
|
|
8421
9243
|
const blocks = {
|
|
@@ -8440,6 +9262,7 @@ function resolveBurnDeps(ctx) {
|
|
|
8440
9262
|
config: config2,
|
|
8441
9263
|
runtime: model.runtime,
|
|
8442
9264
|
hasAuthToken: burnAuthReady(model.runtime, token),
|
|
9265
|
+
exec: cachedExec,
|
|
8443
9266
|
ticketAuthMissing: (ticket) => {
|
|
8444
9267
|
const { model: ticketModel, token: ticketToken } = ticketCredentials(ticket);
|
|
8445
9268
|
return burnAuthReady(ticketModel.runtime, ticketToken) ? undefined : ticketModel.runtime;
|
|
@@ -8453,7 +9276,7 @@ function resolveBurnDeps(ctx) {
|
|
|
8453
9276
|
model: ticketModel,
|
|
8454
9277
|
docsDigest: docs2.text,
|
|
8455
9278
|
lapDigests: run3.digests
|
|
8456
|
-
}) : realExecuteTicketRun(c, ticket, config2, ticketToken, ticketModel, land, ensureIsolatedPushTarget, blocks, run3);
|
|
9279
|
+
}) : realExecuteTicketRun(c, ticket, config2, ticketToken, ticketModel, land, ensureIsolatedPushTarget, ensureCacheVolume, blocks, run3);
|
|
8457
9280
|
}
|
|
8458
9281
|
};
|
|
8459
9282
|
}
|
|
@@ -8470,19 +9293,25 @@ function readTokenFromEnvFile(path, runtime) {
|
|
|
8470
9293
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env[key];
|
|
8471
9294
|
return token && token.length > 0 ? token : undefined;
|
|
8472
9295
|
}
|
|
8473
|
-
var AUTH_MISSING_EVENT = "auth.missing", DROPPED_MAP_SECTIONS, AGENT_CONVENTIONS_FILE = "CLAUDE.md", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, CODEX_HOST_MOUNT_PATH = "/mnt/host-codex", 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, RUNTIME_FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, RUNTIME_RETRYABLE_ERROR_PATTERNS, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
9296
|
+
var AUTH_MISSING_EVENT = "auth.missing", IMAGE_RUNTIME_MISSING_EVENT = "burn.image_runtime_missing", RUNTIME_BINARY, DROPPED_MAP_SECTIONS, AGENT_CONVENTIONS_FILE = "CLAUDE.md", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, CODEX_HOST_MOUNT_PATH = "/mnt/host-codex", SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", SETUP_MARKER_FILE = ".runcastle-setup", SETUP_MARKER = "RUNCASTLE_SETUP", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RUNTIME_FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, RUNTIME_RETRYABLE_ERROR_PATTERNS, AGENT_BINARY, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
8474
9297
|
var init_ticket_burner = __esm(() => {
|
|
8475
9298
|
init_src();
|
|
8476
9299
|
init_config_load();
|
|
8477
9300
|
init_paths();
|
|
8478
9301
|
init_skills_root();
|
|
8479
9302
|
init_codex_auth();
|
|
9303
|
+
init_system_exec();
|
|
8480
9304
|
init_knowledge();
|
|
8481
9305
|
init_setup();
|
|
8482
9306
|
init_agent_stream();
|
|
8483
9307
|
init_git();
|
|
8484
9308
|
init_burn_guard();
|
|
9309
|
+
init_burn_cache();
|
|
8485
9310
|
init_review_ticket();
|
|
9311
|
+
RUNTIME_BINARY = {
|
|
9312
|
+
"claude-code": "claude",
|
|
9313
|
+
codex: "codex"
|
|
9314
|
+
};
|
|
8486
9315
|
DROPPED_MAP_SECTIONS = ["Not yet specified", "Out of scope"];
|
|
8487
9316
|
PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
8488
9317
|
PM_CACHE_SANDBOX_PATHS = {
|
|
@@ -8555,6 +9384,10 @@ var init_ticket_burner = __esm(() => {
|
|
|
8555
9384
|
"claude-code": [],
|
|
8556
9385
|
codex: [/rate_limit_exceeded/i, /\b429\b/, /server_error/i, /stream (disconnected|interrupted)/i]
|
|
8557
9386
|
};
|
|
9387
|
+
AGENT_BINARY = {
|
|
9388
|
+
"claude-code": "claude",
|
|
9389
|
+
codex: "codex"
|
|
9390
|
+
};
|
|
8558
9391
|
activeTicketAborts = new Map;
|
|
8559
9392
|
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
8560
9393
|
ticketBurner = {
|
|
@@ -8566,7 +9399,7 @@ var init_ticket_burner = __esm(() => {
|
|
|
8566
9399
|
// src/workflows/research.ts
|
|
8567
9400
|
import { mkdirSync as mkdirSync10, readFileSync as readFileSync10 } from "fs";
|
|
8568
9401
|
import { dirname as dirname7, join as join15 } from "path";
|
|
8569
|
-
import { fileURLToPath as
|
|
9402
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
8570
9403
|
import { run as run3 } from "@ai-hero/sandcastle";
|
|
8571
9404
|
function waypointSlug(waypoint) {
|
|
8572
9405
|
const base = waypoint.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -8634,7 +9467,7 @@ function interpretResearchResult(result, docRelPath) {
|
|
|
8634
9467
|
return { status: "failed", error: "agent produced no commits \u2014 no research summary landed" };
|
|
8635
9468
|
}
|
|
8636
9469
|
function researchTemplatePath() {
|
|
8637
|
-
const here = dirname7(
|
|
9470
|
+
const here = dirname7(fileURLToPath7(import.meta.url));
|
|
8638
9471
|
return join15(resolveSkillsRoot(here), "burner", "research-waypoint.md");
|
|
8639
9472
|
}
|
|
8640
9473
|
async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
@@ -8743,7 +9576,7 @@ var init_registry2 = __esm(() => {
|
|
|
8743
9576
|
});
|
|
8744
9577
|
|
|
8745
9578
|
// src/workflows/runner.ts
|
|
8746
|
-
import { eq as
|
|
9579
|
+
import { eq as eq10 } from "drizzle-orm";
|
|
8747
9580
|
function workflowClaimsFeatureBranch(workflowId) {
|
|
8748
9581
|
return BRANCH_CLAIMING.has(workflowId);
|
|
8749
9582
|
}
|
|
@@ -8772,7 +9605,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
8772
9605
|
claim(ctx, opts.claimWaypointId, runId);
|
|
8773
9606
|
} catch (e) {
|
|
8774
9607
|
const summary = e instanceof Error ? e.message : "claim failed";
|
|
8775
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(
|
|
9608
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(eq10(runs.id, runId)).run();
|
|
8776
9609
|
throw e;
|
|
8777
9610
|
}
|
|
8778
9611
|
}
|
|
@@ -8803,6 +9636,10 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
8803
9636
|
updateTicket: (id, patch) => {
|
|
8804
9637
|
updateTicket(ctx, id, patch);
|
|
8805
9638
|
},
|
|
9639
|
+
listTickets: () => listByFeature2(ctx, featureId),
|
|
9640
|
+
updateFinding: (findingId, progress, reason) => {
|
|
9641
|
+
markFixProgress(ctx, findingId, progress, reason);
|
|
9642
|
+
},
|
|
8806
9643
|
input: opts.input,
|
|
8807
9644
|
modelOverride: opts.modelOverride,
|
|
8808
9645
|
resolveWaypoint: (id, disposition, summary) => {
|
|
@@ -8840,7 +9677,7 @@ async function executeRun(ctx, runId, featureId, workflow2, runPromise, controll
|
|
|
8840
9677
|
} finally {
|
|
8841
9678
|
controllers.delete(runId);
|
|
8842
9679
|
}
|
|
8843
|
-
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary, ...digest ? { digest } : {} }).where(
|
|
9680
|
+
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary, ...digest ? { digest } : {} }).where(eq10(runs.id, runId)).run();
|
|
8844
9681
|
releaseForSession(ctx, runId);
|
|
8845
9682
|
if (workflowClaimsFeatureBranch(workflow2)) {
|
|
8846
9683
|
sweepOrphanedBurning(ctx, featureId, `orphaned \u2014 the run ended (${status}) while it was burning`);
|
|
@@ -8880,6 +9717,7 @@ var init_runner = __esm(() => {
|
|
|
8880
9717
|
init_gates();
|
|
8881
9718
|
init_git();
|
|
8882
9719
|
init_repo();
|
|
9720
|
+
init_review_findings();
|
|
8883
9721
|
init_tickets();
|
|
8884
9722
|
init_waypoints();
|
|
8885
9723
|
init_registry2();
|
|
@@ -8888,7 +9726,7 @@ var init_runner = __esm(() => {
|
|
|
8888
9726
|
});
|
|
8889
9727
|
|
|
8890
9728
|
// src/services/features.ts
|
|
8891
|
-
import { desc as desc5, eq as
|
|
9729
|
+
import { desc as desc5, eq as eq11 } from "drizzle-orm";
|
|
8892
9730
|
import { rmSync as rmSync4 } from "fs";
|
|
8893
9731
|
async function createFeature(ctx, input) {
|
|
8894
9732
|
const project = requireProjectById(ctx, input.projectId);
|
|
@@ -8925,12 +9763,7 @@ async function createFeature(ctx, input) {
|
|
|
8925
9763
|
});
|
|
8926
9764
|
if (!cut)
|
|
8927
9765
|
return feature;
|
|
8928
|
-
|
|
8929
|
-
if (cut.branchReady) {
|
|
8930
|
-
try {
|
|
8931
|
-
await commitDocs(project.repoPath, `runcastle: scaffold ${slug} docs`);
|
|
8932
|
-
} catch {}
|
|
8933
|
-
}
|
|
9766
|
+
await scaffoldDocsOnFeatureBranch(ctx, project, feature, { brief: input.brief });
|
|
8934
9767
|
return feature;
|
|
8935
9768
|
}
|
|
8936
9769
|
function requireNotDraft(feature) {
|
|
@@ -8946,19 +9779,14 @@ async function startDraft(ctx, featureId, opts = {}) {
|
|
|
8946
9779
|
const project = projectForFeature(ctx, feature);
|
|
8947
9780
|
const base = await requestedBase(project, opts.baseBranch);
|
|
8948
9781
|
const { branchReady, baseBranch } = await ensureFeatureBranch(project, feature.slug, base);
|
|
8949
|
-
ctx.db.update(features).set({ status: "active", baseBranch }).where(
|
|
9782
|
+
ctx.db.update(features).set({ status: "active", baseBranch }).where(eq11(features.id, featureId)).run();
|
|
8950
9783
|
const started = { ...feature, status: "active", baseBranch };
|
|
8951
9784
|
emit(ctx, featureId, {
|
|
8952
9785
|
type: "feature.started",
|
|
8953
9786
|
message: branchReady ? `feature ${feature.slug} started (${feature.branch} \u2190 ${baseBranch})` : `feature ${feature.slug} started (branch pending)`,
|
|
8954
9787
|
data: { branch: feature.branch, baseBranch, branchReady }
|
|
8955
9788
|
});
|
|
8956
|
-
|
|
8957
|
-
if (branchReady) {
|
|
8958
|
-
try {
|
|
8959
|
-
await commitDocs(project.repoPath, `runcastle: scaffold ${feature.slug} docs`);
|
|
8960
|
-
} catch {}
|
|
8961
|
-
}
|
|
9789
|
+
await scaffoldDocsOnFeatureBranch(ctx, project, started, { brief: feature.brief });
|
|
8962
9790
|
return started;
|
|
8963
9791
|
}
|
|
8964
9792
|
function quickTicketTitle(prose) {
|
|
@@ -8984,10 +9812,10 @@ ${body}
|
|
|
8984
9812
|
function quickReviewTicket(proses) {
|
|
8985
9813
|
return {
|
|
8986
9814
|
title: "Review the integrated change",
|
|
8987
|
-
goal: "Review the integrated feature branch and report what you find
|
|
8988
|
-
context: "This feature came through the quick-change door, so there was no grill session and there " + "is no spec.md or decisions.md to review it against: the whole statement of intent is the " + `${proses.length === 1 ? "sentence" : `${proses.length} sentences`} the human typed, which ` + "are the other tickets in this batch and are reproduced verbatim in brief.md. Nobody " + "prescribed a walkthrough for you either \u2014 judge from the diff whether there is anything " + "drivable, and if there is not,
|
|
9815
|
+
goal: "Review the integrated feature branch and report what you find, in exactly one mode: " + "drive the app in a browser against the criteria below when this change touched something " + "a human can operate and a drive is available, and otherwise run the verify gates and " + "code-review the branch's diff against its base. Write one note per finding; finding bugs " + "is a successful review, and the notes are the deliverable.",
|
|
9816
|
+
context: "This feature came through the quick-change door, so there was no grill session and there " + "is no spec.md or decisions.md to review it against: the whole statement of intent is the " + `${proses.length === 1 ? "sentence" : `${proses.length} sentences`} the human typed, which ` + "are the other tickets in this batch and are reproduced verbatim in brief.md. Nobody " + "prescribed a walkthrough for you either \u2014 judge from the diff whether there is anything " + "drivable, and if there is not, take the gates-and-diff mode. Your digest is " + "the lap's prose summary of what landed, and its first line names the mode you ran; the " + "review page leads with it.",
|
|
8989
9817
|
acceptanceCriteria: [
|
|
8990
|
-
"
|
|
9818
|
+
"Reviewed in one mode \u2014 either walked in a browser, or put through the verify gates and " + "code-reviewed on both axes (the repo's own standards, and the change against what was " + "asked for).",
|
|
8991
9819
|
...proses.map((prose) => `Landed and does what it says: ${prose}`)
|
|
8992
9820
|
],
|
|
8993
9821
|
seams: [],
|
|
@@ -9028,7 +9856,7 @@ async function quickChange(ctx, input) {
|
|
|
9028
9856
|
message: branchReady ? `feature.created (${branch} \u2190 ${baseBranch})` : "feature.created (branch pending)",
|
|
9029
9857
|
data: { slug, branch, baseBranch, branchReady }
|
|
9030
9858
|
});
|
|
9031
|
-
|
|
9859
|
+
await scaffoldDocsOnFeatureBranch(ctx, project, feature, { brief: quickBrief(title, proses) });
|
|
9032
9860
|
const stored = storeTickets(ctx, feature.id, [
|
|
9033
9861
|
...proses.map((prose) => ({
|
|
9034
9862
|
title: quickTicketTitle(prose),
|
|
@@ -9048,11 +9876,6 @@ async function quickChange(ctx, input) {
|
|
|
9048
9876
|
message: `quick change \u2014 born at implementation on lap 1 with ${tally} (${typed.map((t) => `#${t.seq}`).join(", ")}) plus a review ticket (#${review.seq}); no grill session, no spec.md`,
|
|
9049
9877
|
data: { slug, ticketSeqs: stored.map((t) => t.seq), phase: "implementation" }
|
|
9050
9878
|
});
|
|
9051
|
-
if (branchReady) {
|
|
9052
|
-
try {
|
|
9053
|
-
await commitDocs(project.repoPath, `runcastle: scaffold ${slug} docs`);
|
|
9054
|
-
} catch {}
|
|
9055
|
-
}
|
|
9056
9879
|
return feature;
|
|
9057
9880
|
}
|
|
9058
9881
|
async function requestedBase(project, picked) {
|
|
@@ -9069,6 +9892,26 @@ async function ensureFeatureBranch(project, slug, base) {
|
|
|
9069
9892
|
throw e;
|
|
9070
9893
|
}
|
|
9071
9894
|
}
|
|
9895
|
+
async function scaffoldDocsOnFeatureBranch(ctx, project, feature, opts) {
|
|
9896
|
+
let worktreePath;
|
|
9897
|
+
try {
|
|
9898
|
+
worktreePath = await ensureTalkWorktree(project, feature);
|
|
9899
|
+
} catch (e) {
|
|
9900
|
+
if (isNotImplemented(e)) {
|
|
9901
|
+
scaffoldDocs(ctx, feature, opts);
|
|
9902
|
+
return;
|
|
9903
|
+
}
|
|
9904
|
+
emit(ctx, feature.id, {
|
|
9905
|
+
type: "docs.scaffold_failed",
|
|
9906
|
+
message: `docs not scaffolded onto ${feature.branch}: ${e instanceof Error ? e.message : String(e)}`
|
|
9907
|
+
});
|
|
9908
|
+
return;
|
|
9909
|
+
}
|
|
9910
|
+
scaffoldDocs(ctx, feature, opts);
|
|
9911
|
+
try {
|
|
9912
|
+
await commitDocs(worktreePath, `runcastle: scaffold ${feature.slug} docs`);
|
|
9913
|
+
} catch {}
|
|
9914
|
+
}
|
|
9072
9915
|
function getFeatureFull(ctx, id) {
|
|
9073
9916
|
const feature = getFeatureRow(ctx, id);
|
|
9074
9917
|
const waypoints2 = feature.mapped ? listByFeature(ctx, id) : [];
|
|
@@ -9085,7 +9928,7 @@ function getFeatureFull(ctx, id) {
|
|
|
9085
9928
|
};
|
|
9086
9929
|
}
|
|
9087
9930
|
function list(ctx, projectId) {
|
|
9088
|
-
const rows = ctx.db.select().from(features).where(
|
|
9931
|
+
const rows = ctx.db.select().from(features).where(eq11(features.projectId, projectId)).orderBy(desc5(features.createdAt)).all();
|
|
9089
9932
|
const lastActivity = latestTsByFeature(ctx, projectId);
|
|
9090
9933
|
return rows.map((row) => {
|
|
9091
9934
|
const feature = rowToFeature(row);
|
|
@@ -9200,7 +10043,7 @@ function rethink(ctx, featureId) {
|
|
|
9200
10043
|
throw new GateError(`${feature.slug} is being test-driven \u2014 stop the test drive first, it holds the feature branch`);
|
|
9201
10044
|
}
|
|
9202
10045
|
const lap = feature.lap + 1;
|
|
9203
|
-
ctx.db.update(features).set({ lap }).where(
|
|
10046
|
+
ctx.db.update(features).set({ lap }).where(eq11(features.id, featureId)).run();
|
|
9204
10047
|
return setPhase(ctx, featureId, RETHINK_LOOP_BACK.to, "lap.started", `rethink \u2014 lap ${lap}`);
|
|
9205
10048
|
}
|
|
9206
10049
|
async function rethinkAndLaunch(ctx, featureId, launch) {
|
|
@@ -9209,7 +10052,7 @@ async function rethinkAndLaunch(ctx, featureId, launch) {
|
|
|
9209
10052
|
try {
|
|
9210
10053
|
return await launch(feature);
|
|
9211
10054
|
} catch (e) {
|
|
9212
|
-
ctx.db.update(features).set({ lap: before.lap }).where(
|
|
10055
|
+
ctx.db.update(features).set({ lap: before.lap }).where(eq11(features.id, featureId)).run();
|
|
9213
10056
|
setPhase(ctx, featureId, before.phase, "lap.aborted", `lap ${feature.lap} aborted \u2014 its terminal could not be opened (${errMsg2(e)}); back at ${before.phase} on lap ${before.lap}`);
|
|
9214
10057
|
throw e;
|
|
9215
10058
|
}
|
|
@@ -9288,7 +10131,7 @@ function escalateToMap(ctx, featureId, input) {
|
|
|
9288
10131
|
return { ok: true, warning: `feature ${feature.slug} is already mapped \u2014 no changes made` };
|
|
9289
10132
|
}
|
|
9290
10133
|
const project = projectForFeature(ctx, feature);
|
|
9291
|
-
ctx.db.update(features).set({ mapped: true }).where(
|
|
10134
|
+
ctx.db.update(features).set({ mapped: true }).where(eq11(features.id, featureId)).run();
|
|
9292
10135
|
scaffoldMapDoc(project, { ...feature, mapped: true }, input);
|
|
9293
10136
|
emit(ctx, featureId, {
|
|
9294
10137
|
type: "feature.escalated",
|
|
@@ -9306,7 +10149,7 @@ function archiveFeature(ctx, featureId) {
|
|
|
9306
10149
|
const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
|
|
9307
10150
|
if (live)
|
|
9308
10151
|
endSession(ctx, live.id);
|
|
9309
|
-
ctx.db.update(features).set({ status: "archived" }).where(
|
|
10152
|
+
ctx.db.update(features).set({ status: "archived" }).where(eq11(features.id, featureId)).run();
|
|
9310
10153
|
emit(ctx, featureId, {
|
|
9311
10154
|
type: "feature.archived",
|
|
9312
10155
|
message: `feature ${feature.slug} archived`,
|
|
@@ -9320,7 +10163,7 @@ function unarchiveFeature(ctx, featureId) {
|
|
|
9320
10163
|
throw new GateError(`feature ${feature.slug} is not archived`);
|
|
9321
10164
|
}
|
|
9322
10165
|
const restored = feature.phase === "shipped" ? "shipped" : "active";
|
|
9323
|
-
ctx.db.update(features).set({ status: restored }).where(
|
|
10166
|
+
ctx.db.update(features).set({ status: restored }).where(eq11(features.id, featureId)).run();
|
|
9324
10167
|
emit(ctx, featureId, {
|
|
9325
10168
|
type: "feature.unarchived",
|
|
9326
10169
|
message: `feature ${feature.slug} unarchived (${restored})`,
|
|
@@ -9360,13 +10203,13 @@ async function deleteFeature(ctx, featureId) {
|
|
|
9360
10203
|
return { ok: true, slug: feature.slug };
|
|
9361
10204
|
}
|
|
9362
10205
|
function deleteFeatureRows(ctx, featureId) {
|
|
9363
|
-
ctx.db.delete(tickets).where(
|
|
9364
|
-
ctx.db.delete(sessions).where(
|
|
9365
|
-
ctx.db.delete(runs).where(
|
|
9366
|
-
ctx.db.delete(events).where(
|
|
9367
|
-
ctx.db.delete(gateOverrides).where(
|
|
9368
|
-
ctx.db.delete(waypoints).where(
|
|
9369
|
-
ctx.db.delete(features).where(
|
|
10206
|
+
ctx.db.delete(tickets).where(eq11(tickets.featureId, featureId)).run();
|
|
10207
|
+
ctx.db.delete(sessions).where(eq11(sessions.featureId, featureId)).run();
|
|
10208
|
+
ctx.db.delete(runs).where(eq11(runs.featureId, featureId)).run();
|
|
10209
|
+
ctx.db.delete(events).where(eq11(events.featureId, featureId)).run();
|
|
10210
|
+
ctx.db.delete(gateOverrides).where(eq11(gateOverrides.featureId, featureId)).run();
|
|
10211
|
+
ctx.db.delete(waypoints).where(eq11(waypoints.featureId, featureId)).run();
|
|
10212
|
+
ctx.db.delete(features).where(eq11(features.id, featureId)).run();
|
|
9370
10213
|
}
|
|
9371
10214
|
function gateState(ctx, feature) {
|
|
9372
10215
|
const gate = nextGate(feature);
|
|
@@ -9377,7 +10220,7 @@ function gateState(ctx, feature) {
|
|
|
9377
10220
|
}
|
|
9378
10221
|
function uniqueSlug(ctx, projectId, title) {
|
|
9379
10222
|
const base = slugify(title);
|
|
9380
|
-
const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(
|
|
10223
|
+
const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(eq11(features.projectId, projectId)).all().map((r) => r.slug));
|
|
9381
10224
|
if (!existing.has(base))
|
|
9382
10225
|
return base;
|
|
9383
10226
|
let n = 2;
|
|
@@ -9410,7 +10253,7 @@ var init_features = __esm(() => {
|
|
|
9410
10253
|
// src/services/test-notes.ts
|
|
9411
10254
|
import { existsSync as existsSync18, mkdirSync as mkdirSync11, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
9412
10255
|
import { dirname as dirname8 } from "path";
|
|
9413
|
-
import { asc as
|
|
10256
|
+
import { asc as asc5, eq as eq12 } from "drizzle-orm";
|
|
9414
10257
|
function rowToNote(row) {
|
|
9415
10258
|
return TestNote.parse({
|
|
9416
10259
|
id: row.id,
|
|
@@ -9427,7 +10270,7 @@ function rowToNote(row) {
|
|
|
9427
10270
|
});
|
|
9428
10271
|
}
|
|
9429
10272
|
function getNote(ctx, id) {
|
|
9430
|
-
const row = ctx.db.select().from(testNotes).where(
|
|
10273
|
+
const row = ctx.db.select().from(testNotes).where(eq12(testNotes.id, id)).get();
|
|
9431
10274
|
if (!row)
|
|
9432
10275
|
throw new NotFoundError(`test note ${id} not found`);
|
|
9433
10276
|
return rowToNote(row);
|
|
@@ -9443,8 +10286,8 @@ function assertOpen(note, action) {
|
|
|
9443
10286
|
throw new InvalidInputError(`cannot ${action} note ${note.id} \u2014 it is ${note.status}; only open notes can be changed`);
|
|
9444
10287
|
}
|
|
9445
10288
|
}
|
|
9446
|
-
function
|
|
9447
|
-
return ctx.db.select().from(testNotes).where(
|
|
10289
|
+
function listByFeature4(ctx, featureId) {
|
|
10290
|
+
return ctx.db.select().from(testNotes).where(eq12(testNotes.featureId, featureId)).orderBy(asc5(testNotes.createdAt), asc5(testNotes.id)).all().map(rowToNote);
|
|
9448
10291
|
}
|
|
9449
10292
|
function addNote(ctx, featureId, text2, author = "human", videoTimestamp) {
|
|
9450
10293
|
const body = cleanText(text2);
|
|
@@ -9475,7 +10318,7 @@ function editNote(ctx, noteId, text2) {
|
|
|
9475
10318
|
const current2 = getNote(ctx, noteId);
|
|
9476
10319
|
assertOpen(current2, "edit");
|
|
9477
10320
|
const body = cleanText(text2);
|
|
9478
|
-
ctx.db.update(testNotes).set({ text: body, updatedAt: Date.now() }).where(
|
|
10321
|
+
ctx.db.update(testNotes).set({ text: body, updatedAt: Date.now() }).where(eq12(testNotes.id, noteId)).run();
|
|
9479
10322
|
emit(ctx, current2.featureId, {
|
|
9480
10323
|
type: "note.edited",
|
|
9481
10324
|
message: "note edited",
|
|
@@ -9487,7 +10330,7 @@ function editNote(ctx, noteId, text2) {
|
|
|
9487
10330
|
function deleteNote(ctx, noteId) {
|
|
9488
10331
|
const current2 = getNote(ctx, noteId);
|
|
9489
10332
|
assertOpen(current2, "delete");
|
|
9490
|
-
ctx.db.delete(testNotes).where(
|
|
10333
|
+
ctx.db.delete(testNotes).where(eq12(testNotes.id, noteId)).run();
|
|
9491
10334
|
rmSync5(annotationPath(noteId), { force: true });
|
|
9492
10335
|
emit(ctx, current2.featureId, {
|
|
9493
10336
|
type: "note.deleted",
|
|
@@ -9514,7 +10357,7 @@ function toggleNote(ctx, noteId) {
|
|
|
9514
10357
|
throw new InvalidInputError(`cannot toggle note ${noteId} \u2014 it is promoted, and promoted notes are frozen`);
|
|
9515
10358
|
}
|
|
9516
10359
|
const status = current2.status === "open" ? "done" : "open";
|
|
9517
|
-
ctx.db.update(testNotes).set({ status, updatedAt: Date.now() }).where(
|
|
10360
|
+
ctx.db.update(testNotes).set({ status, updatedAt: Date.now() }).where(eq12(testNotes.id, noteId)).run();
|
|
9518
10361
|
emit(ctx, current2.featureId, {
|
|
9519
10362
|
type: "note.toggled",
|
|
9520
10363
|
message: `note ${current2.status} \u2192 ${status}`,
|
|
@@ -9564,7 +10407,7 @@ function freezeAsTickets(ctx, noteIds) {
|
|
|
9564
10407
|
const tickets2 = storeTickets(ctx, featureId, selected.map((note) => promotionTicket(feature, note)));
|
|
9565
10408
|
const now = Date.now();
|
|
9566
10409
|
selected.forEach((note, i) => {
|
|
9567
|
-
ctx.db.update(testNotes).set({ status: "promoted", ticketId: tickets2[i].id, updatedAt: now }).where(
|
|
10410
|
+
ctx.db.update(testNotes).set({ status: "promoted", ticketId: tickets2[i].id, updatedAt: now }).where(eq12(testNotes.id, note.id)).run();
|
|
9568
10411
|
});
|
|
9569
10412
|
return { feature, notes: selected.map((n) => getNote(ctx, n.id)), tickets: tickets2 };
|
|
9570
10413
|
}
|
|
@@ -9602,7 +10445,7 @@ function noteLine(ctx, note) {
|
|
|
9602
10445
|
return `- [x] ${note.text}${ticket}${shot}`;
|
|
9603
10446
|
}
|
|
9604
10447
|
function renderTestNotes(ctx, feature) {
|
|
9605
|
-
const notes =
|
|
10448
|
+
const notes = listByFeature4(ctx, feature.id);
|
|
9606
10449
|
const laps = [...new Set(notes.map((n) => n.lap))].sort((a, b) => a - b);
|
|
9607
10450
|
const lines = ["# Test notes"];
|
|
9608
10451
|
for (const lap of laps) {
|
|
@@ -9928,6 +10771,23 @@ function toolAddTestNote(ctx, caller, input) {
|
|
|
9928
10771
|
const identity = requireRunIdentity(ctx, caller, "add_test_note");
|
|
9929
10772
|
return addNote(ctx, identity.featureId, input.text, "agent");
|
|
9930
10773
|
}
|
|
10774
|
+
function toolReportFinding(ctx, caller, input) {
|
|
10775
|
+
const identity = requireRunIdentity(ctx, caller, "report_finding");
|
|
10776
|
+
const reviewTicket = listByFeature2(ctx, identity.featureId).find((ticket) => ticket.kind === "review" && ticket.status === "burning");
|
|
10777
|
+
if (!reviewTicket) {
|
|
10778
|
+
throw new GateError("report_finding requires a burning review ticket on this run\u2019s feature");
|
|
10779
|
+
}
|
|
10780
|
+
const result = reportFinding(ctx, {
|
|
10781
|
+
featureId: identity.featureId,
|
|
10782
|
+
reviewTicket,
|
|
10783
|
+
input
|
|
10784
|
+
});
|
|
10785
|
+
return {
|
|
10786
|
+
findingId: result.finding.id,
|
|
10787
|
+
fixTicketSeq: result.fixTicket?.seq ?? null,
|
|
10788
|
+
overCap: result.overCap
|
|
10789
|
+
};
|
|
10790
|
+
}
|
|
9931
10791
|
async function toolRetryDrive(ctx, session) {
|
|
9932
10792
|
if (session.kind !== "drive-fix") {
|
|
9933
10793
|
throw new GateError(`retrying a drive belongs to a drive-fix session, and this is a ${session.kind} session. ` + "It stops and restarts a test drive on the human's machine \u2014 only the session opened on " + "that failed drive may do that; everyone else asks the human to drive from the review panel.");
|
|
@@ -10186,6 +11046,16 @@ function buildMcpServer(audience) {
|
|
|
10186
11046
|
return ok(toolAddTestNote(rc.ctx, rc.caller, args));
|
|
10187
11047
|
});
|
|
10188
11048
|
}
|
|
11049
|
+
if (wants("report_finding")) {
|
|
11050
|
+
server.registerTool("report_finding", {
|
|
11051
|
+
title: "Report a structured review finding",
|
|
11052
|
+
description: "Report one typed finding for the feature under review. Actionable defects mint fix " + "tickets automatically up to the review cap; observations are retained for the digest.",
|
|
11053
|
+
inputSchema: ReviewFindingInput.shape
|
|
11054
|
+
}, async (args, extra) => {
|
|
11055
|
+
const rc = await resolveRunCaller(extra);
|
|
11056
|
+
return ok(toolReportFinding(rc.ctx, rc.caller, ReviewFindingInput.parse(args)));
|
|
11057
|
+
});
|
|
11058
|
+
}
|
|
10189
11059
|
if (wants("retry_drive")) {
|
|
10190
11060
|
server.registerTool("retry_drive", {
|
|
10191
11061
|
title: "Retry this feature\u2019s test drive",
|
|
@@ -10481,6 +11351,7 @@ var init_server = __esm(() => {
|
|
|
10481
11351
|
init_features();
|
|
10482
11352
|
init_events();
|
|
10483
11353
|
init_findings();
|
|
11354
|
+
init_review_findings();
|
|
10484
11355
|
init_git();
|
|
10485
11356
|
init_knowledge();
|
|
10486
11357
|
init_repo();
|
|
@@ -10520,7 +11391,8 @@ var init_server = __esm(() => {
|
|
|
10520
11391
|
dry_run_drive: ["prepare"],
|
|
10521
11392
|
retry_drive: ["drive-fix"],
|
|
10522
11393
|
review_drive: ["run"],
|
|
10523
|
-
|
|
11394
|
+
report_finding: ["run"],
|
|
11395
|
+
add_test_note: [],
|
|
10524
11396
|
record_event: [...FEATURE_KINDS, ...PROJECT_KINDS]
|
|
10525
11397
|
};
|
|
10526
11398
|
CompletablePhase = Phase.exclude(["shipped"]);
|
|
@@ -10610,7 +11482,7 @@ var init_ws = __esm(() => {
|
|
|
10610
11482
|
});
|
|
10611
11483
|
|
|
10612
11484
|
// src/services/prep.ts
|
|
10613
|
-
import { and as
|
|
11485
|
+
import { and as and9, desc as desc6, eq as eq13, inArray as inArray4 } from "drizzle-orm";
|
|
10614
11486
|
function keysToPrepare(ctx, project) {
|
|
10615
11487
|
return unsetPreparedKeys(project).filter((key) => isOverwritable(ctx, project.id, key));
|
|
10616
11488
|
}
|
|
@@ -10620,10 +11492,10 @@ function isPrepared(ctx, project) {
|
|
|
10620
11492
|
return hasCompletedProjectSession(ctx, project.id, "prepare");
|
|
10621
11493
|
}
|
|
10622
11494
|
function preparedAt(ctx, projectId) {
|
|
10623
|
-
const prepareSessions = new Set(ctx.db.select({ id: sessions.id }).from(sessions).where(
|
|
11495
|
+
const prepareSessions = new Set(ctx.db.select({ id: sessions.id }).from(sessions).where(and9(eq13(sessions.projectId, projectId), eq13(sessions.kind, "prepare"))).all().map((r) => r.id));
|
|
10624
11496
|
if (prepareSessions.size === 0)
|
|
10625
11497
|
return null;
|
|
10626
|
-
const ends = ctx.db.select({ ts: events.ts, data: events.data }).from(events).where(
|
|
11498
|
+
const ends = ctx.db.select({ ts: events.ts, data: events.data }).from(events).where(and9(eq13(events.projectId, projectId), inArray4(events.type, ["session.ended", "session.reconciled"]))).orderBy(desc6(events.ts)).all();
|
|
10627
11499
|
for (const end of ends) {
|
|
10628
11500
|
const sessionId = end.data?.sessionId;
|
|
10629
11501
|
if (sessionId && prepareSessions.has(sessionId))
|
|
@@ -10878,7 +11750,7 @@ var init_hooks = __esm(() => {
|
|
|
10878
11750
|
});
|
|
10879
11751
|
|
|
10880
11752
|
// src/routes/reviews.ts
|
|
10881
|
-
import { createReadStream, statSync as
|
|
11753
|
+
import { createReadStream, statSync as statSync4 } from "fs";
|
|
10882
11754
|
import { Readable } from "stream";
|
|
10883
11755
|
import { Hono as Hono3 } from "hono";
|
|
10884
11756
|
function walkthroughUrl(ticketId) {
|
|
@@ -10886,7 +11758,7 @@ function walkthroughUrl(ticketId) {
|
|
|
10886
11758
|
}
|
|
10887
11759
|
function fileSize(path) {
|
|
10888
11760
|
try {
|
|
10889
|
-
const stat =
|
|
11761
|
+
const stat = statSync4(path);
|
|
10890
11762
|
return stat.isFile() ? stat.size : undefined;
|
|
10891
11763
|
} catch {
|
|
10892
11764
|
return;
|
|
@@ -11064,7 +11936,7 @@ var init_stream = __esm(() => {
|
|
|
11064
11936
|
});
|
|
11065
11937
|
|
|
11066
11938
|
// src/routes/web.ts
|
|
11067
|
-
import { existsSync as existsSync20, statSync as
|
|
11939
|
+
import { existsSync as existsSync20, statSync as statSync5 } from "fs";
|
|
11068
11940
|
import { readFile } from "fs/promises";
|
|
11069
11941
|
import { extname, join as join17, normalize, resolve as resolve10, sep as sep3 } from "path";
|
|
11070
11942
|
function contentType(path) {
|
|
@@ -11072,7 +11944,7 @@ function contentType(path) {
|
|
|
11072
11944
|
}
|
|
11073
11945
|
function isFile(path) {
|
|
11074
11946
|
try {
|
|
11075
|
-
return
|
|
11947
|
+
return statSync5(path).isFile();
|
|
11076
11948
|
} catch {
|
|
11077
11949
|
return false;
|
|
11078
11950
|
}
|
|
@@ -11146,11 +12018,11 @@ var init_web = __esm(() => {
|
|
|
11146
12018
|
// src/version.ts
|
|
11147
12019
|
import { readFileSync as readFileSync12 } from "fs";
|
|
11148
12020
|
import { dirname as dirname9, join as join18 } from "path";
|
|
11149
|
-
import { fileURLToPath as
|
|
12021
|
+
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
11150
12022
|
function runcastleVersion() {
|
|
11151
12023
|
if (cached2)
|
|
11152
12024
|
return cached2;
|
|
11153
|
-
const here = dirname9(
|
|
12025
|
+
const here = dirname9(fileURLToPath8(import.meta.url));
|
|
11154
12026
|
for (const pkgPath of [join18(here, "..", "package.json"), join18(here, "package.json")]) {
|
|
11155
12027
|
try {
|
|
11156
12028
|
const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
|
|
@@ -11375,7 +12247,7 @@ var init_events2 = __esm(() => {
|
|
|
11375
12247
|
// src/launcher/launcher.ts
|
|
11376
12248
|
import { existsSync as existsSync22 } from "fs";
|
|
11377
12249
|
import { join as join19 } from "path";
|
|
11378
|
-
import { and as
|
|
12250
|
+
import { and as and10, eq as eq14 } from "drizzle-orm";
|
|
11379
12251
|
function ptyExitMessage(exitCode) {
|
|
11380
12252
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
11381
12253
|
return `terminal exited (code ${label})`;
|
|
@@ -11395,7 +12267,7 @@ function renderCommand(runtime, spec) {
|
|
|
11395
12267
|
return [...env, runtime.binary, ...spec.argv].join(" ");
|
|
11396
12268
|
}
|
|
11397
12269
|
function activeRunFor(ctx, featureId) {
|
|
11398
|
-
const row = ctx.db.select().from(runs).where(
|
|
12270
|
+
const row = ctx.db.select().from(runs).where(and10(eq14(runs.featureId, featureId), eq14(runs.status, "running"))).limit(1).get();
|
|
11399
12271
|
return row ? rowToRun(row) : null;
|
|
11400
12272
|
}
|
|
11401
12273
|
function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
@@ -12196,7 +13068,7 @@ var init_feature = __esm(() => {
|
|
|
12196
13068
|
});
|
|
12197
13069
|
|
|
12198
13070
|
// src/services/fsbrowse.ts
|
|
12199
|
-
import { existsSync as existsSync23, readdirSync as readdirSync6, statSync as
|
|
13071
|
+
import { existsSync as existsSync23, readdirSync as readdirSync6, statSync as statSync6 } from "fs";
|
|
12200
13072
|
import { homedir as homedir3 } from "os";
|
|
12201
13073
|
import { basename as basename3, dirname as dirname11, isAbsolute as isAbsolute2, join as join21, resolve as resolve11, sep as sep4 } from "path";
|
|
12202
13074
|
function expandPath(input, home = homedir3()) {
|
|
@@ -12272,7 +13144,7 @@ function browseDir(input, showHidden = false, home = homedir3()) {
|
|
|
12272
13144
|
}
|
|
12273
13145
|
let dir = target;
|
|
12274
13146
|
try {
|
|
12275
|
-
if (!
|
|
13147
|
+
if (!statSync6(target).isDirectory())
|
|
12276
13148
|
dir = dirname11(target);
|
|
12277
13149
|
} catch (e) {
|
|
12278
13150
|
throw new InvalidInputError(`cannot read path: ${target} (${errMsg4(e)})`);
|
|
@@ -12302,7 +13174,7 @@ function browseDir(input, showHidden = false, home = homedir3()) {
|
|
|
12302
13174
|
let isDirectory = dirent.isDirectory();
|
|
12303
13175
|
if (isSymlink) {
|
|
12304
13176
|
try {
|
|
12305
|
-
isDirectory =
|
|
13177
|
+
isDirectory = statSync6(path).isDirectory();
|
|
12306
13178
|
} catch {
|
|
12307
13179
|
continue;
|
|
12308
13180
|
}
|
|
@@ -12494,7 +13366,7 @@ var init_conversations = __esm(() => {
|
|
|
12494
13366
|
// src/services/projects.ts
|
|
12495
13367
|
import { existsSync as existsSync24 } from "fs";
|
|
12496
13368
|
import { basename as basename4, join as join22 } from "path";
|
|
12497
|
-
import { eq as
|
|
13369
|
+
import { eq as eq15, isNull } from "drizzle-orm";
|
|
12498
13370
|
function listProjects(ctx) {
|
|
12499
13371
|
return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
|
|
12500
13372
|
}
|
|
@@ -12515,7 +13387,7 @@ async function openProject(ctx, rawPath) {
|
|
|
12515
13387
|
const name = basename4(repoPath) || repoPath;
|
|
12516
13388
|
const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
|
|
12517
13389
|
if (existing) {
|
|
12518
|
-
ctx.db.update(projects).set({ repoPath, closedAt: null }).where(
|
|
13390
|
+
ctx.db.update(projects).set({ repoPath, closedAt: null }).where(eq15(projects.id, existing.id)).run();
|
|
12519
13391
|
const reopened = { ...existing, repoPath, closedAt: undefined };
|
|
12520
13392
|
emitProject(ctx, existing.id, {
|
|
12521
13393
|
type: "project.opened",
|
|
@@ -12545,7 +13417,7 @@ function closeProject(ctx, projectId) {
|
|
|
12545
13417
|
if (projectHasActiveRun(ctx, projectId)) {
|
|
12546
13418
|
throw new InvalidInputError(`cannot close ${project.name}: a run is in flight \u2014 wait for it to finish or cancel it first`);
|
|
12547
13419
|
}
|
|
12548
|
-
ctx.db.update(projects).set({ closedAt: Date.now() }).where(
|
|
13420
|
+
ctx.db.update(projects).set({ closedAt: Date.now() }).where(eq15(projects.id, projectId)).run();
|
|
12549
13421
|
emitProject(ctx, projectId, {
|
|
12550
13422
|
type: "project.closed",
|
|
12551
13423
|
message: `project ${project.name} closed`
|
|
@@ -12554,7 +13426,7 @@ function closeProject(ctx, projectId) {
|
|
|
12554
13426
|
}
|
|
12555
13427
|
function renameProject(ctx, projectId, name) {
|
|
12556
13428
|
const project = requireProjectById(ctx, projectId);
|
|
12557
|
-
ctx.db.update(projects).set({ name }).where(
|
|
13429
|
+
ctx.db.update(projects).set({ name }).where(eq15(projects.id, projectId)).run();
|
|
12558
13430
|
emitProject(ctx, projectId, {
|
|
12559
13431
|
type: "project.renamed",
|
|
12560
13432
|
message: `project renamed ${project.name} \u2192 ${name}`,
|
|
@@ -12625,8 +13497,26 @@ var init_project = __esm(() => {
|
|
|
12625
13497
|
});
|
|
12626
13498
|
});
|
|
12627
13499
|
|
|
12628
|
-
// src/trpc/routers/
|
|
13500
|
+
// src/trpc/routers/review-findings.ts
|
|
12629
13501
|
import * as z9 from "zod";
|
|
13502
|
+
var reviewFindingsRouter;
|
|
13503
|
+
var init_review_findings2 = __esm(() => {
|
|
13504
|
+
init_features();
|
|
13505
|
+
init_review_findings();
|
|
13506
|
+
init_context();
|
|
13507
|
+
reviewFindingsRouter = router({
|
|
13508
|
+
listByFeature: publicProcedure.input(z9.object({ featureId: z9.string() })).query(({ ctx, input }) => viewByFeature(ctx, input.featureId)),
|
|
13509
|
+
dismiss: publicProcedure.input(z9.object({ findingId: z9.string() })).mutation(({ ctx, input }) => dismiss(ctx, input.findingId)),
|
|
13510
|
+
fixOpenDefects: publicProcedure.input(z9.object({ featureId: z9.string() })).mutation(async ({ ctx, input }) => {
|
|
13511
|
+
const promoted = promoteOpenDefects(ctx, input.featureId);
|
|
13512
|
+
const { runId } = await burn(ctx, input.featureId);
|
|
13513
|
+
return { ...promoted, runId };
|
|
13514
|
+
})
|
|
13515
|
+
});
|
|
13516
|
+
});
|
|
13517
|
+
|
|
13518
|
+
// src/trpc/routers/run.ts
|
|
13519
|
+
import * as z10 from "zod";
|
|
12630
13520
|
var runRouter;
|
|
12631
13521
|
var init_run = __esm(() => {
|
|
12632
13522
|
init_agent_stream();
|
|
@@ -12634,9 +13524,9 @@ var init_run = __esm(() => {
|
|
|
12634
13524
|
init_runner();
|
|
12635
13525
|
init_context();
|
|
12636
13526
|
runRouter = router({
|
|
12637
|
-
get: publicProcedure.input(
|
|
12638
|
-
agentTranscript: publicProcedure.input(
|
|
12639
|
-
cancel: publicProcedure.input(
|
|
13527
|
+
get: publicProcedure.input(z10.object({ runId: z10.string() })).query(({ ctx, input }) => getRunRow(ctx, input.runId)),
|
|
13528
|
+
agentTranscript: publicProcedure.input(z10.object({ ticketId: z10.string(), after: z10.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
|
|
13529
|
+
cancel: publicProcedure.input(z10.object({ runId: z10.string() })).mutation(({ ctx, input }) => {
|
|
12640
13530
|
getRunRow(ctx, input.runId);
|
|
12641
13531
|
cancelRun(input.runId);
|
|
12642
13532
|
return { ok: true };
|
|
@@ -12645,47 +13535,21 @@ var init_run = __esm(() => {
|
|
|
12645
13535
|
});
|
|
12646
13536
|
|
|
12647
13537
|
// src/trpc/routers/settings.ts
|
|
12648
|
-
import * as
|
|
13538
|
+
import * as z11 from "zod";
|
|
12649
13539
|
var settingsRouter;
|
|
12650
13540
|
var init_settings2 = __esm(() => {
|
|
12651
13541
|
init_src();
|
|
12652
13542
|
init_settings();
|
|
12653
13543
|
init_context();
|
|
12654
13544
|
settingsRouter = router({
|
|
12655
|
-
get: publicProcedure.input(
|
|
13545
|
+
get: publicProcedure.input(z11.object({ projectId: z11.string().optional() }).optional()).query(({ ctx, input }) => getSettings(ctx, input?.projectId)),
|
|
12656
13546
|
update: publicProcedure.input(SettingsUpdateInput).mutation(({ ctx, input }) => updateSettings(ctx, input))
|
|
12657
13547
|
});
|
|
12658
13548
|
});
|
|
12659
13549
|
|
|
12660
|
-
// src/doctor/system-exec.ts
|
|
12661
|
-
import { spawn as spawn4 } from "child_process";
|
|
12662
|
-
function createSystemExec(opts = {}) {
|
|
12663
|
-
return (command, args) => new Promise((resolve12) => {
|
|
12664
|
-
const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
|
|
12665
|
-
let stdout = "";
|
|
12666
|
-
let stderr = "";
|
|
12667
|
-
const child = spawn4(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
|
|
12668
|
-
child.stdout?.on("data", (d) => {
|
|
12669
|
-
stdout += d.toString();
|
|
12670
|
-
});
|
|
12671
|
-
child.stderr?.on("data", (d) => {
|
|
12672
|
-
stderr += d.toString();
|
|
12673
|
-
});
|
|
12674
|
-
child.on("error", (err) => {
|
|
12675
|
-
resolve12({ ok: false, code: null, stdout, stderr: stderr || String(err) });
|
|
12676
|
-
});
|
|
12677
|
-
child.on("close", (code) => {
|
|
12678
|
-
resolve12({ ok: true, code: code ?? 0, stdout, stderr });
|
|
12679
|
-
});
|
|
12680
|
-
});
|
|
12681
|
-
}
|
|
12682
|
-
var init_system_exec = __esm(() => {
|
|
12683
|
-
init_resolve_executable();
|
|
12684
|
-
});
|
|
12685
|
-
|
|
12686
13550
|
// src/trpc/routers/setup.ts
|
|
12687
|
-
import { isNotNull as
|
|
12688
|
-
import * as
|
|
13551
|
+
import { isNotNull as isNotNull3 } from "drizzle-orm";
|
|
13552
|
+
import * as z12 from "zod";
|
|
12689
13553
|
var setupRouter;
|
|
12690
13554
|
var init_setup2 = __esm(() => {
|
|
12691
13555
|
init_src();
|
|
@@ -12698,7 +13562,7 @@ var init_setup2 = __esm(() => {
|
|
|
12698
13562
|
init_context();
|
|
12699
13563
|
setupRouter = router({
|
|
12700
13564
|
doctor: publicProcedure.query(({ ctx }) => {
|
|
12701
|
-
const projectModels = ctx.db.select({ model: projects.model }).from(projects).where(
|
|
13565
|
+
const projectModels = ctx.db.select({ model: projects.model }).from(projects).where(isNotNull3(projects.model)).all().map((p) => p.model);
|
|
12702
13566
|
return runDoctor({
|
|
12703
13567
|
exec: createSystemExec(),
|
|
12704
13568
|
runtimes: configuredRuntimes(ctx.config, projectModels),
|
|
@@ -12706,10 +13570,10 @@ var init_setup2 = __esm(() => {
|
|
|
12706
13570
|
});
|
|
12707
13571
|
}),
|
|
12708
13572
|
runtimeGuide: publicProcedure.query(() => runtimeInstallGuide(process.platform)),
|
|
12709
|
-
gitIdentity: publicProcedure.input(
|
|
12710
|
-
afkToken: publicProcedure.input(
|
|
12711
|
-
seedModelDefaults: publicProcedure.input(
|
|
12712
|
-
startTerminal: publicProcedure.input(
|
|
13573
|
+
gitIdentity: publicProcedure.input(z12.object({ name: z12.string(), email: z12.string() })).mutation(({ input }) => writeGitIdentity(createSystemExec(), input)),
|
|
13574
|
+
afkToken: publicProcedure.input(z12.object({ token: z12.string(), runtime: AgentRuntime.default("claude-code") })).mutation(({ input }) => saveAfkCredential(fileAfkTokenIo(createSystemExec(), input.runtime), input.token, input.runtime)),
|
|
13575
|
+
seedModelDefaults: publicProcedure.input(z12.object({ runtimes: z12.array(AgentRuntime) })).mutation(({ ctx, input }) => seedModelDefaults(ctx, input.runtimes)),
|
|
13576
|
+
startTerminal: publicProcedure.input(z12.object({ kind: z12.enum(["setup-token", "build-image", "claude-login", "codex-login"]) })).mutation(async ({ ctx, input }) => {
|
|
12713
13577
|
const exec = createSystemExec();
|
|
12714
13578
|
const preferred = ctx.config.sandbox === "podman" ? "podman" : "docker";
|
|
12715
13579
|
const runtime = await resolveRuntime(exec, preferred);
|
|
@@ -12731,40 +13595,80 @@ var init_setup2 = __esm(() => {
|
|
|
12731
13595
|
});
|
|
12732
13596
|
|
|
12733
13597
|
// src/trpc/routers/system.ts
|
|
12734
|
-
|
|
13598
|
+
import * as z13 from "zod";
|
|
13599
|
+
function burnCacheEngine(sandbox) {
|
|
13600
|
+
return sandbox === "docker" || sandbox === "podman" ? sandbox : null;
|
|
13601
|
+
}
|
|
13602
|
+
var projectInput, systemRouter;
|
|
12735
13603
|
var init_system = __esm(() => {
|
|
13604
|
+
init_src();
|
|
13605
|
+
init_system_exec();
|
|
13606
|
+
init_errors();
|
|
13607
|
+
init_events();
|
|
12736
13608
|
init_update_check();
|
|
13609
|
+
init_burn_cache();
|
|
12737
13610
|
init_version();
|
|
12738
13611
|
init_context();
|
|
13612
|
+
projectInput = z13.object({ projectId: z13.string() });
|
|
12739
13613
|
systemRouter = router({
|
|
12740
13614
|
version: publicProcedure.query(() => ({ version: runcastleVersion() })),
|
|
12741
|
-
checkUpdate: publicProcedure.query(() => getUpdateInfo(runcastleVersion()))
|
|
13615
|
+
checkUpdate: publicProcedure.query(() => getUpdateInfo(runcastleVersion())),
|
|
13616
|
+
burnCache: router({
|
|
13617
|
+
status: publicProcedure.input(projectInput).query(async ({ ctx, input }) => {
|
|
13618
|
+
const mode = resolveBurnCacheMode(ctx.config);
|
|
13619
|
+
const engine = burnCacheEngine(ctx.config.sandbox);
|
|
13620
|
+
const sizeBytes = mode === "volume" && engine ? await burnCacheVolumeSize({
|
|
13621
|
+
engine,
|
|
13622
|
+
projectId: input.projectId,
|
|
13623
|
+
exec: createSystemExec()
|
|
13624
|
+
}) : null;
|
|
13625
|
+
return { mode, engine, volumeName: burnCacheVolumeName(input.projectId), sizeBytes };
|
|
13626
|
+
}),
|
|
13627
|
+
clear: publicProcedure.input(projectInput).mutation(async ({ ctx, input }) => {
|
|
13628
|
+
const engine = burnCacheEngine(ctx.config.sandbox);
|
|
13629
|
+
if (!engine) {
|
|
13630
|
+
throw new InvalidInputError(`the burn cache needs a docker or podman sandbox; this one is "${ctx.config.sandbox}"`);
|
|
13631
|
+
}
|
|
13632
|
+
const volumeName = burnCacheVolumeName(input.projectId);
|
|
13633
|
+
await removeBurnCacheVolume({
|
|
13634
|
+
engine,
|
|
13635
|
+
projectId: input.projectId,
|
|
13636
|
+
exec: createSystemExec(),
|
|
13637
|
+
slots: getBurnSlotAllocator(ctx.config.burnConcurrency).held()
|
|
13638
|
+
});
|
|
13639
|
+
emitProject(ctx, input.projectId, {
|
|
13640
|
+
type: "burn-cache.cleared",
|
|
13641
|
+
message: `cleared the burn cache volume ${volumeName}`
|
|
13642
|
+
});
|
|
13643
|
+
return { volumeName };
|
|
13644
|
+
})
|
|
13645
|
+
})
|
|
12742
13646
|
});
|
|
12743
13647
|
});
|
|
12744
13648
|
|
|
12745
13649
|
// src/trpc/routers/test-notes.ts
|
|
12746
|
-
import * as
|
|
13650
|
+
import * as z14 from "zod";
|
|
12747
13651
|
var testNotesRouter;
|
|
12748
13652
|
var init_test_notes2 = __esm(() => {
|
|
12749
13653
|
init_test_notes();
|
|
12750
13654
|
init_context();
|
|
12751
13655
|
testNotesRouter = router({
|
|
12752
|
-
list: publicProcedure.input(
|
|
12753
|
-
add: publicProcedure.input(
|
|
12754
|
-
featureId:
|
|
12755
|
-
text:
|
|
12756
|
-
videoTimestamp:
|
|
13656
|
+
list: publicProcedure.input(z14.object({ featureId: z14.string() })).query(({ ctx, input }) => listByFeature4(ctx, input.featureId)),
|
|
13657
|
+
add: publicProcedure.input(z14.object({
|
|
13658
|
+
featureId: z14.string(),
|
|
13659
|
+
text: z14.string().min(1),
|
|
13660
|
+
videoTimestamp: z14.number().nonnegative().optional()
|
|
12757
13661
|
})).mutation(({ ctx, input }) => addNote(ctx, input.featureId, input.text, "human", input.videoTimestamp)),
|
|
12758
|
-
edit: publicProcedure.input(
|
|
12759
|
-
remove: publicProcedure.input(
|
|
12760
|
-
toggle: publicProcedure.input(
|
|
12761
|
-
promote: publicProcedure.input(
|
|
12762
|
-
promoteMany: publicProcedure.input(
|
|
13662
|
+
edit: publicProcedure.input(z14.object({ noteId: z14.string(), text: z14.string().min(1) })).mutation(({ ctx, input }) => editNote(ctx, input.noteId, input.text)),
|
|
13663
|
+
remove: publicProcedure.input(z14.object({ noteId: z14.string() })).mutation(({ ctx, input }) => deleteNote(ctx, input.noteId)),
|
|
13664
|
+
toggle: publicProcedure.input(z14.object({ noteId: z14.string() })).mutation(({ ctx, input }) => toggleNote(ctx, input.noteId)),
|
|
13665
|
+
promote: publicProcedure.input(z14.object({ noteId: z14.string() })).mutation(({ ctx, input }) => promoteNote(ctx, input.noteId)),
|
|
13666
|
+
promoteMany: publicProcedure.input(z14.object({ noteIds: z14.array(z14.string()).min(1) })).mutation(({ ctx, input }) => promoteMany(ctx, input.noteIds))
|
|
12763
13667
|
});
|
|
12764
13668
|
});
|
|
12765
13669
|
|
|
12766
13670
|
// src/trpc/routers/ticket.ts
|
|
12767
|
-
import * as
|
|
13671
|
+
import * as z15 from "zod";
|
|
12768
13672
|
var ticketRouter;
|
|
12769
13673
|
var init_ticket = __esm(() => {
|
|
12770
13674
|
init_features();
|
|
@@ -12773,8 +13677,8 @@ var init_ticket = __esm(() => {
|
|
|
12773
13677
|
init_ticket_burner();
|
|
12774
13678
|
init_context();
|
|
12775
13679
|
ticketRouter = router({
|
|
12776
|
-
retry: publicProcedure.input(
|
|
12777
|
-
stop: publicProcedure.input(
|
|
13680
|
+
retry: publicProcedure.input(z15.object({ ticketId: z15.string(), fresh: z15.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
|
|
13681
|
+
stop: publicProcedure.input(z15.object({ ticketId: z15.string() })).mutation(({ ctx, input }) => {
|
|
12778
13682
|
if (stopTicketRun(input.ticketId))
|
|
12779
13683
|
return { stopped: true, swept: false };
|
|
12780
13684
|
const ticket = getTicket(ctx, input.ticketId);
|
|
@@ -12784,14 +13688,14 @@ var init_ticket = __esm(() => {
|
|
|
12784
13688
|
}
|
|
12785
13689
|
return { stopped: false, swept: false };
|
|
12786
13690
|
}),
|
|
12787
|
-
cancel: publicProcedure.input(
|
|
12788
|
-
edit: publicProcedure.input(
|
|
12789
|
-
ticketId:
|
|
12790
|
-
title:
|
|
12791
|
-
goal:
|
|
12792
|
-
context:
|
|
12793
|
-
acceptanceCriteria:
|
|
12794
|
-
model:
|
|
13691
|
+
cancel: publicProcedure.input(z15.object({ ticketId: z15.string(), reason: z15.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason)),
|
|
13692
|
+
edit: publicProcedure.input(z15.object({
|
|
13693
|
+
ticketId: z15.string(),
|
|
13694
|
+
title: z15.string().min(1).optional(),
|
|
13695
|
+
goal: z15.string().min(1).optional(),
|
|
13696
|
+
context: z15.string().optional(),
|
|
13697
|
+
acceptanceCriteria: z15.array(z15.string().min(1)).optional(),
|
|
13698
|
+
model: z15.string().optional()
|
|
12795
13699
|
})).mutation(({ ctx, input }) => {
|
|
12796
13700
|
const { ticketId, ...patch } = input;
|
|
12797
13701
|
return editTicket(ctx, ticketId, patch);
|
|
@@ -12807,6 +13711,7 @@ var init_router = __esm(() => {
|
|
|
12807
13711
|
init_events2();
|
|
12808
13712
|
init_feature();
|
|
12809
13713
|
init_project();
|
|
13714
|
+
init_review_findings2();
|
|
12810
13715
|
init_run();
|
|
12811
13716
|
init_settings2();
|
|
12812
13717
|
init_setup2();
|
|
@@ -12819,6 +13724,7 @@ var init_router = __esm(() => {
|
|
|
12819
13724
|
run: runRouter,
|
|
12820
13725
|
ticket: ticketRouter,
|
|
12821
13726
|
notes: testNotesRouter,
|
|
13727
|
+
findings: reviewFindingsRouter,
|
|
12822
13728
|
events: eventsRouter,
|
|
12823
13729
|
docs: docsRouter,
|
|
12824
13730
|
settings: settingsRouter,
|
|
@@ -12828,14 +13734,14 @@ var init_router = __esm(() => {
|
|
|
12828
13734
|
});
|
|
12829
13735
|
|
|
12830
13736
|
// src/workflows/reconcile-runs.ts
|
|
12831
|
-
import { eq as
|
|
13737
|
+
import { eq as eq16 } from "drizzle-orm";
|
|
12832
13738
|
async function reconcileStaleRuns(ctx) {
|
|
12833
|
-
const stale = ctx.db.select().from(runs).where(
|
|
13739
|
+
const stale = ctx.db.select().from(runs).where(eq16(runs.status, "running")).all().map(rowToRun);
|
|
12834
13740
|
const reconciled = [];
|
|
12835
13741
|
for (const run4 of stale) {
|
|
12836
13742
|
if (isRunActive(run4.id))
|
|
12837
13743
|
continue;
|
|
12838
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(
|
|
13744
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq16(runs.id, run4.id)).run();
|
|
12839
13745
|
const released = releaseForSession(ctx, run4.id);
|
|
12840
13746
|
const swept = workflowClaimsFeatureBranch(run4.workflow) ? sweepOrphanedBurning(ctx, run4.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
|
|
12841
13747
|
if (workflowClaimsFeatureBranch(run4.workflow)) {
|