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