runcastle 1.2.9 → 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 +1444 -425
- package/drizzle/0030_productive_marvex.sql +1 -0
- package/drizzle/0031_backfill_feature_base_branch.sql +10 -0
- package/drizzle/0032_gigantic_tempest.sql +1 -0
- package/drizzle/0033_robust_saracen.sql +20 -0
- package/drizzle/meta/0030_snapshot.json +956 -0
- package/drizzle/meta/0031_snapshot.json +956 -0
- package/drizzle/meta/0032_snapshot.json +949 -0
- package/drizzle/meta/0033_snapshot.json +1078 -0
- package/drizzle/meta/_journal.json +29 -1
- package/index.js +1439 -421
- 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/project/SKILL.md +11 -1
- 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-BRjcVBgj.js +0 -142
- package/web/assets/index-CNSn8X1O.css +0 -1
package/index.js
CHANGED
|
@@ -48,6 +48,12 @@ import * as z from "zod";
|
|
|
48
48
|
function resolveSandboxImage(config) {
|
|
49
49
|
return config.sandboxImage ?? DEFAULT_SANDBOX_IMAGE;
|
|
50
50
|
}
|
|
51
|
+
function resolveBurnCacheMode(config) {
|
|
52
|
+
return BURN_CACHE_SANDBOXES.includes(config.sandbox) ? config.burnCache : "off";
|
|
53
|
+
}
|
|
54
|
+
function resolveDefaultBurnConcurrency(logicalCpus) {
|
|
55
|
+
return Number.isFinite(logicalCpus) && logicalCpus > SMALL_HOST_LOGICAL_CPUS ? 3 : 1;
|
|
56
|
+
}
|
|
51
57
|
function resolvePreparedSettings(config, project) {
|
|
52
58
|
const pick = (a, b) => {
|
|
53
59
|
const own = a?.trim();
|
|
@@ -99,7 +105,7 @@ var MODEL_STEPS, ModelStep, AGENT_RUNTIMES, AgentRuntime, DEFAULT_RUNTIME = "cla
|
|
|
99
105
|
}
|
|
100
106
|
delete obj.smokeModel;
|
|
101
107
|
return obj;
|
|
102
|
-
}, RuncastleConfig, DEFAULT_SANDBOX_IMAGE = "sandcastle:runcastle";
|
|
108
|
+
}, RuncastleConfig, DEFAULT_SANDBOX_IMAGE = "sandcastle:runcastle", BURN_CACHE_SANDBOXES, SMALL_HOST_LOGICAL_CPUS = 8;
|
|
103
109
|
var init_config = __esm(() => {
|
|
104
110
|
MODEL_STEPS = [
|
|
105
111
|
"ideation",
|
|
@@ -109,6 +115,7 @@ var init_config = __esm(() => {
|
|
|
109
115
|
"revisit",
|
|
110
116
|
"research",
|
|
111
117
|
"implement",
|
|
118
|
+
"review",
|
|
112
119
|
"prepare",
|
|
113
120
|
"project",
|
|
114
121
|
"smoke"
|
|
@@ -146,7 +153,6 @@ var init_config = __esm(() => {
|
|
|
146
153
|
models: z.array(ModelEntry).default([]),
|
|
147
154
|
sandbox: z.enum(["docker", "podman", "noSandbox"]).default("docker"),
|
|
148
155
|
sessionMcp: z.enum(["inherit", "runcastleOnly"]).default("inherit"),
|
|
149
|
-
mainBranch: z.string().default("main"),
|
|
150
156
|
sandboxImage: z.string().optional(),
|
|
151
157
|
burnConcurrency: z.number().int().min(1).max(8).default(3),
|
|
152
158
|
burnCpus: z.number().positive().max(256).optional(),
|
|
@@ -157,8 +163,10 @@ var init_config = __esm(() => {
|
|
|
157
163
|
setupCommand: z.string().optional(),
|
|
158
164
|
verifyCommands: z.string().optional(),
|
|
159
165
|
knownFailures: z.string().optional(),
|
|
160
|
-
burnWorkspace: z.enum(["auto", "mounted", "isolated"]).default("auto")
|
|
166
|
+
burnWorkspace: z.enum(["auto", "mounted", "isolated"]).default("auto"),
|
|
167
|
+
burnCache: z.enum(["volume", "off"]).default("volume")
|
|
161
168
|
}));
|
|
169
|
+
BURN_CACHE_SANDBOXES = ["docker", "podman"];
|
|
162
170
|
});
|
|
163
171
|
|
|
164
172
|
// ../core/src/schemas.ts
|
|
@@ -166,7 +174,7 @@ import * as z2 from "zod";
|
|
|
166
174
|
function isProjectSessionKind(kind) {
|
|
167
175
|
return PROJECT_SESSION_KINDS.includes(kind);
|
|
168
176
|
}
|
|
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;
|
|
177
|
+
var Phase, TicketStatus, SessionKind, PROJECT_SESSION_KINDS, SessionPurpose, MergeBranchPair, RunStatus, FeatureStatus, SessionStatus, TicketKind, FindingKind, FindingSeverity, FindingStatus, FindingOpenReason, ReviewFindingInput, ReviewFinding, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNoteAuthor, TestNote, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
170
178
|
var init_schemas = __esm(() => {
|
|
171
179
|
init_config();
|
|
172
180
|
Phase = z2.enum([
|
|
@@ -198,6 +206,35 @@ var init_schemas = __esm(() => {
|
|
|
198
206
|
FeatureStatus = z2.enum(["draft", "active", "shipped", "archived"]);
|
|
199
207
|
SessionStatus = z2.enum(["launching", "live", "ended"]);
|
|
200
208
|
TicketKind = z2.enum(["implementation", "review"]);
|
|
209
|
+
FindingKind = z2.enum(["defect", "observation"]);
|
|
210
|
+
FindingSeverity = z2.enum(["high", "medium", "low"]);
|
|
211
|
+
FindingStatus = z2.enum(["open", "fixing", "fixed", "failed", "dismissed"]);
|
|
212
|
+
FindingOpenReason = z2.enum(["over-cap", "fix-failed"]);
|
|
213
|
+
ReviewFindingInput = z2.object({
|
|
214
|
+
kind: FindingKind,
|
|
215
|
+
severity: FindingSeverity,
|
|
216
|
+
title: z2.string().min(1),
|
|
217
|
+
location: z2.string().min(1),
|
|
218
|
+
citation: z2.string().min(1),
|
|
219
|
+
detail: z2.string().min(1),
|
|
220
|
+
reproStep: z2.string().optional()
|
|
221
|
+
}).superRefine((finding, ctx) => {
|
|
222
|
+
if (finding.kind === "defect" && !finding.reproStep?.trim()) {
|
|
223
|
+
ctx.addIssue({ code: "custom", path: ["reproStep"], message: "defects require a repro step" });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
ReviewFinding = ReviewFindingInput.safeExtend({
|
|
227
|
+
id: z2.string(),
|
|
228
|
+
featureId: z2.string(),
|
|
229
|
+
lap: z2.number(),
|
|
230
|
+
reviewTicketId: z2.string(),
|
|
231
|
+
reproStep: z2.string(),
|
|
232
|
+
status: FindingStatus,
|
|
233
|
+
openReason: FindingOpenReason.nullable(),
|
|
234
|
+
failureReason: z2.string().nullable(),
|
|
235
|
+
fixTicketId: z2.string().nullable(),
|
|
236
|
+
createdAt: z2.number()
|
|
237
|
+
});
|
|
201
238
|
TicketInput = z2.object({
|
|
202
239
|
title: z2.string(),
|
|
203
240
|
goal: z2.string(),
|
|
@@ -206,7 +243,8 @@ var init_schemas = __esm(() => {
|
|
|
206
243
|
seams: z2.array(z2.string()),
|
|
207
244
|
blockedBy: z2.array(z2.number()),
|
|
208
245
|
kind: TicketKind.default("implementation"),
|
|
209
|
-
model: z2.string().optional()
|
|
246
|
+
model: z2.string().optional(),
|
|
247
|
+
originFindingId: z2.string().optional()
|
|
210
248
|
});
|
|
211
249
|
Ticket = TicketInput.extend({
|
|
212
250
|
id: z2.string(),
|
|
@@ -276,7 +314,7 @@ var init_schemas = __esm(() => {
|
|
|
276
314
|
id: z2.string(),
|
|
277
315
|
name: z2.string(),
|
|
278
316
|
repoPath: z2.string(),
|
|
279
|
-
|
|
317
|
+
sessionBranch: z2.string().optional(),
|
|
280
318
|
devCommand: z2.string().optional(),
|
|
281
319
|
model: z2.string().optional(),
|
|
282
320
|
setupCommand: z2.string().optional(),
|
|
@@ -537,13 +575,13 @@ var init_pipeline = __esm(() => {
|
|
|
537
575
|
|
|
538
576
|
// ../core/src/db-schema.ts
|
|
539
577
|
import { index, integer, primaryKey, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
540
|
-
var projects, projectFindings, features, sessions, tickets, testNotes, waypoints, runs, events, gateOverrides;
|
|
578
|
+
var projects, projectFindings, features, sessions, tickets, reviewFindings, testNotes, waypoints, runs, events, gateOverrides;
|
|
541
579
|
var init_db_schema = __esm(() => {
|
|
542
580
|
projects = sqliteTable("projects", {
|
|
543
581
|
id: text("id").primaryKey(),
|
|
544
582
|
name: text("name").notNull(),
|
|
545
583
|
repoPath: text("repo_path").notNull(),
|
|
546
|
-
|
|
584
|
+
sessionBranch: text("session_branch"),
|
|
547
585
|
devCommand: text("dev_command"),
|
|
548
586
|
model: text("model"),
|
|
549
587
|
sandbox: text("sandbox"),
|
|
@@ -616,7 +654,26 @@ var init_db_schema = __esm(() => {
|
|
|
616
654
|
error: text("error"),
|
|
617
655
|
attemptBranch: text("attempt_branch"),
|
|
618
656
|
conflictFiles: text("conflict_files", { mode: "json" }).$type(),
|
|
619
|
-
digest: text("digest")
|
|
657
|
+
digest: text("digest"),
|
|
658
|
+
originFindingId: text("origin_finding_id")
|
|
659
|
+
});
|
|
660
|
+
reviewFindings = sqliteTable("review_findings", {
|
|
661
|
+
id: text("id").primaryKey(),
|
|
662
|
+
featureId: text("feature_id").notNull(),
|
|
663
|
+
lap: integer("lap").notNull(),
|
|
664
|
+
reviewTicketId: text("review_ticket_id").notNull(),
|
|
665
|
+
kind: text("kind").notNull().$type(),
|
|
666
|
+
severity: text("severity").notNull().$type(),
|
|
667
|
+
title: text("title").notNull(),
|
|
668
|
+
location: text("location").notNull(),
|
|
669
|
+
citation: text("citation").notNull(),
|
|
670
|
+
detail: text("detail").notNull(),
|
|
671
|
+
reproStep: text("repro_step").notNull(),
|
|
672
|
+
status: text("status").notNull().$type(),
|
|
673
|
+
openReason: text("open_reason").$type(),
|
|
674
|
+
failureReason: text("failure_reason"),
|
|
675
|
+
fixTicketId: text("fix_ticket_id"),
|
|
676
|
+
createdAt: integer("created_at").notNull()
|
|
620
677
|
});
|
|
621
678
|
testNotes = sqliteTable("test_notes", {
|
|
622
679
|
id: text("id").primaryKey(),
|
|
@@ -789,10 +846,12 @@ var init_paths = () => {};
|
|
|
789
846
|
// ../core/src/config-load.ts
|
|
790
847
|
var exports_config_load = {};
|
|
791
848
|
__export(exports_config_load, {
|
|
792
|
-
loadConfig: () => loadConfig
|
|
849
|
+
loadConfig: () => loadConfig,
|
|
850
|
+
hostLogicalCpus: () => hostLogicalCpus
|
|
793
851
|
});
|
|
794
852
|
import { existsSync, readFileSync } from "fs";
|
|
795
|
-
|
|
853
|
+
import { availableParallelism } from "os";
|
|
854
|
+
function loadConfig(env = process.env, logicalCpus = hostLogicalCpus()) {
|
|
796
855
|
let fileConfig = {};
|
|
797
856
|
const path = configPath();
|
|
798
857
|
if (existsSync(path)) {
|
|
@@ -816,8 +875,6 @@ function loadConfig(env = process.env) {
|
|
|
816
875
|
overrides.sandbox = env.RUNCASTLE_SANDBOX;
|
|
817
876
|
if (env.RUNCASTLE_SESSION_MCP)
|
|
818
877
|
overrides.sessionMcp = env.RUNCASTLE_SESSION_MCP;
|
|
819
|
-
if (env.RUNCASTLE_MAIN_BRANCH)
|
|
820
|
-
overrides.mainBranch = env.RUNCASTLE_MAIN_BRANCH;
|
|
821
878
|
if (env.RUNCASTLE_SANDBOX_IMAGE)
|
|
822
879
|
overrides.sandboxImage = env.RUNCASTLE_SANDBOX_IMAGE;
|
|
823
880
|
num("burnConcurrency", env.RUNCASTLE_BURN_CONCURRENCY);
|
|
@@ -836,8 +893,17 @@ function loadConfig(env = process.env) {
|
|
|
836
893
|
overrides.knownFailures = env.RUNCASTLE_KNOWN_FAILURES;
|
|
837
894
|
if (env.RUNCASTLE_BURN_WORKSPACE)
|
|
838
895
|
overrides.burnWorkspace = env.RUNCASTLE_BURN_WORKSPACE;
|
|
896
|
+
if (env.RUNCASTLE_BURN_CACHE)
|
|
897
|
+
overrides.burnCache = env.RUNCASTLE_BURN_CACHE;
|
|
839
898
|
const base = typeof fileConfig === "object" && fileConfig !== null ? fileConfig : {};
|
|
840
|
-
|
|
899
|
+
const merged = { ...base, ...overrides };
|
|
900
|
+
if (merged.burnConcurrency === undefined) {
|
|
901
|
+
merged.burnConcurrency = resolveDefaultBurnConcurrency(logicalCpus);
|
|
902
|
+
}
|
|
903
|
+
return RuncastleConfig.parse(merged);
|
|
904
|
+
}
|
|
905
|
+
function hostLogicalCpus() {
|
|
906
|
+
return availableParallelism();
|
|
841
907
|
}
|
|
842
908
|
var init_config_load = __esm(() => {
|
|
843
909
|
init_config();
|
|
@@ -866,6 +932,7 @@ var init_schema = __esm(() => {
|
|
|
866
932
|
init_src();
|
|
867
933
|
schema = {
|
|
868
934
|
projects,
|
|
935
|
+
reviewFindings,
|
|
869
936
|
features,
|
|
870
937
|
sessions,
|
|
871
938
|
tickets,
|
|
@@ -1574,6 +1641,198 @@ var init_registry = __esm(() => {
|
|
|
1574
1641
|
REGISTRY_KEY = Symbol.for("runcastle.pty.registry");
|
|
1575
1642
|
});
|
|
1576
1643
|
|
|
1644
|
+
// src/workflows/burn-cache.ts
|
|
1645
|
+
function burnCacheVolumeName(projectId) {
|
|
1646
|
+
return `runcastle-${projectId}`;
|
|
1647
|
+
}
|
|
1648
|
+
function slotRepoPath(slot) {
|
|
1649
|
+
return `${slotDirPath(slot)}/repo`;
|
|
1650
|
+
}
|
|
1651
|
+
function slotDirPath(slot) {
|
|
1652
|
+
return `${BURN_CACHE_MOUNT}/slots/${slot}`;
|
|
1653
|
+
}
|
|
1654
|
+
function slotStampPath(slot) {
|
|
1655
|
+
return `${slotDirPath(slot)}/.runcastle-stamp`;
|
|
1656
|
+
}
|
|
1657
|
+
function storePath(pm) {
|
|
1658
|
+
return `${BURN_CACHE_MOUNT}/store/${pm}`;
|
|
1659
|
+
}
|
|
1660
|
+
function burnCacheEnv(pm) {
|
|
1661
|
+
const store = storePath(pm);
|
|
1662
|
+
return {
|
|
1663
|
+
npm_config_store_dir: store,
|
|
1664
|
+
pnpm_config_store_dir: store,
|
|
1665
|
+
BUN_INSTALL_CACHE_DIR: store,
|
|
1666
|
+
npm_config_cache: store,
|
|
1667
|
+
YARN_GLOBAL_FOLDER: store,
|
|
1668
|
+
TMPDIR: `${BURN_CACHE_MOUNT}/tmp`,
|
|
1669
|
+
NODE_COMPILE_CACHE: `${BURN_CACHE_MOUNT}/node-compile`
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
async function execOrThrow(exec, engine, args) {
|
|
1673
|
+
const out = await exec(engine, args);
|
|
1674
|
+
if (!out.ok || out.code !== 0) {
|
|
1675
|
+
throw new Error(`${engine} ${args.join(" ")} failed: ${out.stderr.trim() || `exit ${out.code}`}`);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
async function ensureBurnCacheVolume(opts) {
|
|
1679
|
+
const { engine, exec } = opts;
|
|
1680
|
+
const name = burnCacheVolumeName(opts.projectId);
|
|
1681
|
+
const inspect = await exec(engine, ["volume", "inspect", name]);
|
|
1682
|
+
const existed = inspect.ok && inspect.code === 0;
|
|
1683
|
+
await execOrThrow(exec, engine, ["volume", "create", name]);
|
|
1684
|
+
if (existed)
|
|
1685
|
+
return;
|
|
1686
|
+
await execOrThrow(exec, engine, [
|
|
1687
|
+
"run",
|
|
1688
|
+
"--rm",
|
|
1689
|
+
"--user",
|
|
1690
|
+
"root",
|
|
1691
|
+
"-v",
|
|
1692
|
+
`${name}:${BURN_CACHE_MOUNT}`,
|
|
1693
|
+
opts.imageName,
|
|
1694
|
+
"chown",
|
|
1695
|
+
"-R",
|
|
1696
|
+
CONTAINER_USER,
|
|
1697
|
+
BURN_CACHE_MOUNT
|
|
1698
|
+
]);
|
|
1699
|
+
}
|
|
1700
|
+
async function removeBurnCacheVolume(opts) {
|
|
1701
|
+
if (opts.slots.length > 0)
|
|
1702
|
+
throw new BurnCacheBusyError([...opts.slots].sort((a, b) => a - b));
|
|
1703
|
+
await execOrThrow(opts.exec, opts.engine, ["volume", "rm", burnCacheVolumeName(opts.projectId)]);
|
|
1704
|
+
}
|
|
1705
|
+
async function burnCacheVolumeSize(opts) {
|
|
1706
|
+
const out = await opts.exec(opts.engine, ["system", "df", "-v", "--format", "json"]);
|
|
1707
|
+
if (!out.ok || out.code !== 0)
|
|
1708
|
+
return null;
|
|
1709
|
+
return findVolumeSize(out.stdout, burnCacheVolumeName(opts.projectId));
|
|
1710
|
+
}
|
|
1711
|
+
function parseJsonValues(stdout) {
|
|
1712
|
+
const values = [];
|
|
1713
|
+
const whole = tryParseJson(stdout);
|
|
1714
|
+
if (whole !== undefined)
|
|
1715
|
+
return [whole];
|
|
1716
|
+
for (const line of stdout.split(`
|
|
1717
|
+
`)) {
|
|
1718
|
+
const value = tryParseJson(line);
|
|
1719
|
+
if (value !== undefined)
|
|
1720
|
+
values.push(value);
|
|
1721
|
+
}
|
|
1722
|
+
return values;
|
|
1723
|
+
}
|
|
1724
|
+
function tryParseJson(text2) {
|
|
1725
|
+
const trimmed = text2.trim();
|
|
1726
|
+
if (!trimmed)
|
|
1727
|
+
return;
|
|
1728
|
+
try {
|
|
1729
|
+
return JSON.parse(trimmed);
|
|
1730
|
+
} catch {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
function findVolumeSize(stdout, name) {
|
|
1735
|
+
const pending = parseJsonValues(stdout);
|
|
1736
|
+
while (pending.length > 0) {
|
|
1737
|
+
const node = pending.shift();
|
|
1738
|
+
if (Array.isArray(node)) {
|
|
1739
|
+
pending.push(...node);
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
if (typeof node !== "object" || node === null)
|
|
1743
|
+
continue;
|
|
1744
|
+
const record = node;
|
|
1745
|
+
if (VOLUME_NAME_KEYS.some((key) => record[key] === name)) {
|
|
1746
|
+
const size = readSize(record);
|
|
1747
|
+
if (size !== null)
|
|
1748
|
+
return size;
|
|
1749
|
+
}
|
|
1750
|
+
pending.push(...Object.values(record));
|
|
1751
|
+
}
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1754
|
+
function readSize(record) {
|
|
1755
|
+
const usage = record.UsageData;
|
|
1756
|
+
const nested = typeof usage === "object" && usage !== null ? usage.Size : undefined;
|
|
1757
|
+
return parseSize(record.Size) ?? parseSize(nested);
|
|
1758
|
+
}
|
|
1759
|
+
function parseSize(value) {
|
|
1760
|
+
if (typeof value === "number")
|
|
1761
|
+
return Number.isFinite(value) ? value : null;
|
|
1762
|
+
if (typeof value !== "string")
|
|
1763
|
+
return null;
|
|
1764
|
+
const match = /^\s*(-?[\d.]+)\s*([a-z]*)\s*$/i.exec(value);
|
|
1765
|
+
if (!match)
|
|
1766
|
+
return null;
|
|
1767
|
+
const amount = Number(match[1]);
|
|
1768
|
+
const unit = SIZE_UNITS[(match[2] || "b").toLowerCase()];
|
|
1769
|
+
if (!Number.isFinite(amount) || unit === undefined)
|
|
1770
|
+
return null;
|
|
1771
|
+
return Math.round(amount * unit);
|
|
1772
|
+
}
|
|
1773
|
+
function createSlotAllocator(capacity) {
|
|
1774
|
+
const heldSlots = new Set;
|
|
1775
|
+
let width = capacity;
|
|
1776
|
+
return {
|
|
1777
|
+
claim() {
|
|
1778
|
+
for (let slot = 1;slot <= width; slot++) {
|
|
1779
|
+
if (!heldSlots.has(slot)) {
|
|
1780
|
+
heldSlots.add(slot);
|
|
1781
|
+
return slot;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
throw new BurnSlotsExhaustedError(width);
|
|
1785
|
+
},
|
|
1786
|
+
release(slot) {
|
|
1787
|
+
heldSlots.delete(slot);
|
|
1788
|
+
},
|
|
1789
|
+
held() {
|
|
1790
|
+
return [...heldSlots].sort((a, b) => a - b);
|
|
1791
|
+
},
|
|
1792
|
+
resize(next) {
|
|
1793
|
+
width = next;
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
function getBurnSlotAllocator(capacity) {
|
|
1798
|
+
if (!sharedAllocator)
|
|
1799
|
+
sharedAllocator = createSlotAllocator(capacity);
|
|
1800
|
+
else
|
|
1801
|
+
sharedAllocator.resize(capacity);
|
|
1802
|
+
return sharedAllocator;
|
|
1803
|
+
}
|
|
1804
|
+
var BURN_CACHE_MOUNT = "/home/agent/cache", BurnCacheBusyError, CONTAINER_USER = "1000:1000", VOLUME_NAME_KEYS, SIZE_UNITS, BurnSlotsExhaustedError, sharedAllocator;
|
|
1805
|
+
var init_burn_cache = __esm(() => {
|
|
1806
|
+
BurnCacheBusyError = class BurnCacheBusyError extends Error {
|
|
1807
|
+
slots;
|
|
1808
|
+
constructor(slots) {
|
|
1809
|
+
super(`burn cache is in use \u2014 slots ${slots.join(", ")} are held; stop those burns before clearing it`);
|
|
1810
|
+
this.name = "BurnCacheBusyError";
|
|
1811
|
+
this.slots = slots;
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
VOLUME_NAME_KEYS = ["Name", "VolumeName"];
|
|
1815
|
+
SIZE_UNITS = {
|
|
1816
|
+
b: 1,
|
|
1817
|
+
kb: 1000,
|
|
1818
|
+
mb: 1e6,
|
|
1819
|
+
gb: 1e9,
|
|
1820
|
+
tb: 1000000000000,
|
|
1821
|
+
pb: 1000000000000000,
|
|
1822
|
+
kib: 1024,
|
|
1823
|
+
mib: 1024 ** 2,
|
|
1824
|
+
gib: 1024 ** 3,
|
|
1825
|
+
tib: 1024 ** 4,
|
|
1826
|
+
pib: 1024 ** 5
|
|
1827
|
+
};
|
|
1828
|
+
BurnSlotsExhaustedError = class BurnSlotsExhaustedError extends Error {
|
|
1829
|
+
constructor(capacity) {
|
|
1830
|
+
super(`all ${capacity} burn cache slots are in use`);
|
|
1831
|
+
this.name = "BurnSlotsExhaustedError";
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1577
1836
|
// src/errors.ts
|
|
1578
1837
|
import { TRPCError } from "@trpc/server";
|
|
1579
1838
|
function isNotImplemented(e) {
|
|
@@ -1588,6 +1847,8 @@ function toTRPCError(e) {
|
|
|
1588
1847
|
return new TRPCError({ code: "NOT_FOUND", message: e.message, cause: e });
|
|
1589
1848
|
if (e instanceof GateError)
|
|
1590
1849
|
return new TRPCError({ code: "PRECONDITION_FAILED", message: e.message, cause: e });
|
|
1850
|
+
if (e instanceof BurnCacheBusyError)
|
|
1851
|
+
return new TRPCError({ code: "PRECONDITION_FAILED", message: e.message, cause: e });
|
|
1591
1852
|
if (e instanceof InvalidInputError)
|
|
1592
1853
|
return new TRPCError({ code: "BAD_REQUEST", message: e.message, cause: e });
|
|
1593
1854
|
if (e instanceof Error)
|
|
@@ -1596,6 +1857,7 @@ function toTRPCError(e) {
|
|
|
1596
1857
|
}
|
|
1597
1858
|
var NotImplementedError, GateError, InvalidInputError, NotFoundError;
|
|
1598
1859
|
var init_errors = __esm(() => {
|
|
1860
|
+
init_burn_cache();
|
|
1599
1861
|
NotImplementedError = class NotImplementedError extends Error {
|
|
1600
1862
|
wave;
|
|
1601
1863
|
constructor(wave) {
|
|
@@ -1759,7 +2021,7 @@ function rowToProject(row) {
|
|
|
1759
2021
|
id: row.id,
|
|
1760
2022
|
name: row.name,
|
|
1761
2023
|
repoPath: row.repoPath,
|
|
1762
|
-
|
|
2024
|
+
sessionBranch: row.sessionBranch ?? undefined,
|
|
1763
2025
|
devCommand: row.devCommand ?? undefined,
|
|
1764
2026
|
model: row.model ?? undefined,
|
|
1765
2027
|
setupCommand: row.setupCommand ?? undefined,
|
|
@@ -2497,11 +2759,12 @@ function markVerified(ctx, projectId, keys, sha) {
|
|
|
2497
2759
|
async function listFindings(ctx, project) {
|
|
2498
2760
|
const rows = ctx.db.select().from(projectFindings).where(eq4(projectFindings.projectId, project.id)).all();
|
|
2499
2761
|
const distances = new Map;
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2762
|
+
const shas = new Set(rows.flatMap((row) => row.establishedSha ? [row.establishedSha] : []));
|
|
2763
|
+
if (shas.size > 0) {
|
|
2764
|
+
const mainLine = await detectMainBranch(project.repoPath);
|
|
2765
|
+
for (const sha of shas) {
|
|
2766
|
+
distances.set(sha, await commitsSince(project.repoPath, sha, mainLine));
|
|
2767
|
+
}
|
|
2505
2768
|
}
|
|
2506
2769
|
const order = new Map(PREPARED_KEYS.map((k, i) => [k, i]));
|
|
2507
2770
|
return rows.filter((row) => isPreparedKey(row.key)).sort((a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0)).map((row) => {
|
|
@@ -2570,8 +2833,11 @@ function migrationPaths(paths) {
|
|
|
2570
2833
|
function featureBranch(slug) {
|
|
2571
2834
|
return `feature/${slug}`;
|
|
2572
2835
|
}
|
|
2573
|
-
function
|
|
2574
|
-
|
|
2836
|
+
function featureBase(feature) {
|
|
2837
|
+
if (!feature.baseBranch) {
|
|
2838
|
+
throw new GateError(`feature ${feature.slug} has no recorded base branch \u2014 it has not been cut from one yet`);
|
|
2839
|
+
}
|
|
2840
|
+
return feature.baseBranch;
|
|
2575
2841
|
}
|
|
2576
2842
|
function git(repoPath) {
|
|
2577
2843
|
return simpleGit(repoPath);
|
|
@@ -2621,9 +2887,31 @@ async function detectMainBranch(repoPath) {
|
|
|
2621
2887
|
} catch {}
|
|
2622
2888
|
return "main";
|
|
2623
2889
|
}
|
|
2890
|
+
async function resolveSessionBranch(project) {
|
|
2891
|
+
const stored = project.sessionBranch?.trim();
|
|
2892
|
+
if (!stored)
|
|
2893
|
+
return detectMainBranch(project.repoPath);
|
|
2894
|
+
const local = await git(project.repoPath).branchLocal();
|
|
2895
|
+
if (!local.all.includes(stored)) {
|
|
2896
|
+
throw new GateError(`the configured session landing branch \`${stored}\` no longer exists in ${project.repoPath} \u2014 pick where this chat's work should land`);
|
|
2897
|
+
}
|
|
2898
|
+
return stored;
|
|
2899
|
+
}
|
|
2900
|
+
async function sessionBranchView(project) {
|
|
2901
|
+
const stored = project.sessionBranch?.trim() || null;
|
|
2902
|
+
const detected = await detectMainBranch(project.repoPath);
|
|
2903
|
+
return { stored, effective: stored ?? detected, detected };
|
|
2904
|
+
}
|
|
2905
|
+
async function currentCheckoutBranch(project) {
|
|
2906
|
+
const local = await git(project.repoPath).branchLocal();
|
|
2907
|
+
if (local.detached || !local.current) {
|
|
2908
|
+
throw new InvalidInputError(`${project.repoPath} has no branch checked out (detached HEAD) \u2014 name the branch to fork from`);
|
|
2909
|
+
}
|
|
2910
|
+
return local.current;
|
|
2911
|
+
}
|
|
2624
2912
|
async function createFeatureBranch(project, slug, base) {
|
|
2625
2913
|
const branch = featureBranch(slug);
|
|
2626
|
-
const from = base
|
|
2914
|
+
const from = base.trim();
|
|
2627
2915
|
const g = git(project.repoPath);
|
|
2628
2916
|
const branches = await g.branchLocal();
|
|
2629
2917
|
if (branches.all.includes(branch))
|
|
@@ -2637,7 +2925,9 @@ async function createFeatureBranch(project, slug, base) {
|
|
|
2637
2925
|
async function listBranches(project) {
|
|
2638
2926
|
const g = git(project.repoPath);
|
|
2639
2927
|
const local = await g.branchLocal();
|
|
2640
|
-
const
|
|
2928
|
+
const detachedRef = local.detached ? local.current : null;
|
|
2929
|
+
const current = local.detached ? "" : local.current;
|
|
2930
|
+
const branches = local.all.filter((name) => !name.startsWith("feature/") && name !== detachedRef);
|
|
2641
2931
|
const localSet = new Set(local.all);
|
|
2642
2932
|
let remoteBranches = [];
|
|
2643
2933
|
try {
|
|
@@ -2647,7 +2937,7 @@ async function listBranches(project) {
|
|
|
2647
2937
|
return !short.startsWith("feature/") && !localSet.has(short);
|
|
2648
2938
|
});
|
|
2649
2939
|
} catch {}
|
|
2650
|
-
return { current
|
|
2940
|
+
return { current, branches, remoteBranches };
|
|
2651
2941
|
}
|
|
2652
2942
|
async function resolveBaseBranch(project, base) {
|
|
2653
2943
|
const g = git(project.repoPath);
|
|
@@ -2673,7 +2963,7 @@ async function ensureTalkWorktree(project, feature) {
|
|
|
2673
2963
|
const worktreePath = worktreeDir(project.id, feature.slug);
|
|
2674
2964
|
const branch = featureBranch(feature.slug);
|
|
2675
2965
|
const g = git(project.repoPath);
|
|
2676
|
-
await ensureBranchExists(g, branch, feature
|
|
2966
|
+
await ensureBranchExists(g, branch, featureBase(feature));
|
|
2677
2967
|
if (await worktreeIsValid(g, worktreePath, branch))
|
|
2678
2968
|
return worktreePath;
|
|
2679
2969
|
if (existsSync6(worktreePath) && (await registeredWorktrees(g)).has(canon(worktreePath))) {
|
|
@@ -2755,8 +3045,8 @@ function slash(path) {
|
|
|
2755
3045
|
async function ensureProjectWorktree(project, onLanded) {
|
|
2756
3046
|
const worktreePath = worktreeDir(project.id, PROJECT_WORKTREE_SLUG);
|
|
2757
3047
|
const g = git(project.repoPath);
|
|
2758
|
-
const base = project
|
|
2759
|
-
const landed = await landProjectBranch(project);
|
|
3048
|
+
const base = await resolveSessionBranch(project);
|
|
3049
|
+
const landed = await landProjectBranch(project, base);
|
|
2760
3050
|
if (landed)
|
|
2761
3051
|
onLanded?.(landed);
|
|
2762
3052
|
const stillAhead = await branchCommitsAhead(project.repoPath, base, PROJECT_BRANCH);
|
|
@@ -2765,12 +3055,13 @@ async function ensureProjectWorktree(project, onLanded) {
|
|
|
2765
3055
|
await g.raw(["branch", PROJECT_BRANCH, base]);
|
|
2766
3056
|
}
|
|
2767
3057
|
if (await worktreeIsValid(g, worktreePath, PROJECT_BRANCH))
|
|
2768
|
-
return worktreePath;
|
|
3058
|
+
return { worktreePath, base };
|
|
2769
3059
|
if (existsSync6(worktreePath) && (await registeredWorktrees(g)).has(canon(worktreePath))) {
|
|
2770
3060
|
if (await checkoutInWorktree(worktreePath, PROJECT_BRANCH))
|
|
2771
|
-
return worktreePath;
|
|
3061
|
+
return { worktreePath, base };
|
|
2772
3062
|
}
|
|
2773
|
-
|
|
3063
|
+
const added = await addWorktree(g, worktreePath, PROJECT_BRANCH, "project worktree");
|
|
3064
|
+
return { worktreePath: added, base };
|
|
2774
3065
|
}
|
|
2775
3066
|
async function checkoutInWorktree(worktreePath, branch) {
|
|
2776
3067
|
const gw = git(worktreePath);
|
|
@@ -2934,7 +3225,7 @@ async function branchCommitsAhead(repoPath, base, branch) {
|
|
|
2934
3225
|
}
|
|
2935
3226
|
}
|
|
2936
3227
|
async function reviewCommitCount(project, feature) {
|
|
2937
|
-
const base =
|
|
3228
|
+
const base = featureBase(feature);
|
|
2938
3229
|
const branch = featureBranch(feature.slug);
|
|
2939
3230
|
try {
|
|
2940
3231
|
const out = (await git(project.repoPath).raw(["rev-list", "--count", `${base}..${branch}`])).trim();
|
|
@@ -2945,7 +3236,7 @@ async function reviewCommitCount(project, feature) {
|
|
|
2945
3236
|
}
|
|
2946
3237
|
}
|
|
2947
3238
|
async function featureBranchDelta(project, feature) {
|
|
2948
|
-
const base =
|
|
3239
|
+
const base = featureBase(feature);
|
|
2949
3240
|
const branch = featureBranch(feature.slug);
|
|
2950
3241
|
try {
|
|
2951
3242
|
const out = await git(project.repoPath).raw(["diff", "--stat", `${base}...${branch}`]);
|
|
@@ -3120,12 +3411,13 @@ async function deleteBranchDetachingWorktrees(g, repoPath, branch) {
|
|
|
3120
3411
|
return false;
|
|
3121
3412
|
}
|
|
3122
3413
|
}
|
|
3123
|
-
async function landProjectBranch(project) {
|
|
3124
|
-
const ahead = await branchCommitsAhead(project.repoPath,
|
|
3414
|
+
async function landProjectBranch(project, base) {
|
|
3415
|
+
const ahead = await branchCommitsAhead(project.repoPath, base, PROJECT_BRANCH);
|
|
3125
3416
|
if (ahead.length === 0)
|
|
3126
3417
|
return null;
|
|
3127
|
-
const res = await mergeTempBranch(project.repoPath,
|
|
3418
|
+
const res = await mergeTempBranch(project.repoPath, base, PROJECT_BRANCH);
|
|
3128
3419
|
return {
|
|
3420
|
+
base,
|
|
3129
3421
|
commits: ahead.length,
|
|
3130
3422
|
landed: res.ok,
|
|
3131
3423
|
...res.conflict ? { conflict: true } : {},
|
|
@@ -3226,6 +3518,11 @@ async function commitDocs(worktreePath, message) {
|
|
|
3226
3518
|
return;
|
|
3227
3519
|
await g.commit(message, [DOCS_PATHSPEC]);
|
|
3228
3520
|
}
|
|
3521
|
+
async function commitPipelineDocs(repoPath) {
|
|
3522
|
+
try {
|
|
3523
|
+
await commitDocs(repoPath, "runcastle: docs the pipeline left uncommitted");
|
|
3524
|
+
} catch {}
|
|
3525
|
+
}
|
|
3229
3526
|
function activeTestDriveFeatureId() {
|
|
3230
3527
|
return testDriveState?.kind === "feature" ? testDriveState.featureId : undefined;
|
|
3231
3528
|
}
|
|
@@ -3325,6 +3622,7 @@ async function testDrive(ctx, project, feature, action, purpose = "human") {
|
|
|
3325
3622
|
...teardown?.failure ? { hookFailure: teardown.failure } : {}
|
|
3326
3623
|
};
|
|
3327
3624
|
}
|
|
3625
|
+
await commitPipelineDocs(project.repoPath);
|
|
3328
3626
|
const porcelain = (await g.raw(["status", "--porcelain"])).trim();
|
|
3329
3627
|
if (porcelain !== "")
|
|
3330
3628
|
return { ok: false, deniedReason: DENY_DIRTY };
|
|
@@ -3506,7 +3804,7 @@ async function stopDryRun(ctx, project, state) {
|
|
|
3506
3804
|
});
|
|
3507
3805
|
const { participating, failure } = dryRunVerdict(project, state);
|
|
3508
3806
|
if (!failure && participating.length > 0) {
|
|
3509
|
-
const sha = await headSha(project.repoPath, project.
|
|
3807
|
+
const sha = await headSha(project.repoPath, await detectMainBranch(project.repoPath)) ?? null;
|
|
3510
3808
|
markVerified(ctx, project.id, participating, sha);
|
|
3511
3809
|
emitProject(ctx, project.id, {
|
|
3512
3810
|
type: "prep.dryrun.verified",
|
|
@@ -3668,7 +3966,7 @@ async function detectDbDrift(ctx, project, feature, previousBranch, driveBranch)
|
|
|
3668
3966
|
return drift;
|
|
3669
3967
|
}
|
|
3670
3968
|
async function mergeFeature(project, feature) {
|
|
3671
|
-
const target =
|
|
3969
|
+
const target = featureBase(feature);
|
|
3672
3970
|
if (testDriveState) {
|
|
3673
3971
|
throw new GateError("Cannot merge while a test drive is active \u2014 stop it first");
|
|
3674
3972
|
}
|
|
@@ -4272,7 +4570,7 @@ function renderDriveFixPrompt(brief) {
|
|
|
4272
4570
|
`);
|
|
4273
4571
|
}
|
|
4274
4572
|
function renderProjectPrompt(brief, runtime = DEFAULT_RUNTIME) {
|
|
4275
|
-
const { project, branch, worktreePath } = brief;
|
|
4573
|
+
const { project, branch, worktreePath, base } = brief;
|
|
4276
4574
|
return [
|
|
4277
4575
|
`# runcastle \u2014 ${project.name} (project session)`,
|
|
4278
4576
|
"",
|
|
@@ -4298,8 +4596,8 @@ function renderProjectPrompt(brief, runtime = DEFAULT_RUNTIME) {
|
|
|
4298
4596
|
"## Where you are working \u2014 and where your writes go",
|
|
4299
4597
|
`- Working directory: \`${worktreePath}\` \u2014 a runcastle-owned worktree, NOT the human's`,
|
|
4300
4598
|
` checkout at \`${project.repoPath}\`.`,
|
|
4301
|
-
`- Branch: \`${branch}\`, cut from \`${
|
|
4302
|
-
`- **Your commits land on \`${
|
|
4599
|
+
`- Branch: \`${branch}\`, cut from \`${base}\` when this session launched.`,
|
|
4600
|
+
`- **Your commits land on \`${base}\` when this terminal closes**, arriving`,
|
|
4303
4601
|
" in the human's checkout the way a `git pull` does. Write real code and real docs \u2014",
|
|
4304
4602
|
" there is no sandbox here \u2014 and commit everything you write before you finish.",
|
|
4305
4603
|
" A session that edits without committing leaves a dirty tree, which is exactly what",
|
|
@@ -4586,10 +4884,24 @@ var init_claude = __esm(() => {
|
|
|
4586
4884
|
};
|
|
4587
4885
|
});
|
|
4588
4886
|
|
|
4589
|
-
// src/
|
|
4887
|
+
// src/services/codex-auth.ts
|
|
4590
4888
|
import { existsSync as existsSync8 } from "fs";
|
|
4591
4889
|
import { homedir as homedir2 } from "os";
|
|
4592
4890
|
import { join as join9 } from "path";
|
|
4891
|
+
function codexHomeDir(env = process.env) {
|
|
4892
|
+
return env.CODEX_HOME ?? join9(env.HOME ?? env.USERPROFILE ?? homedir2(), ".codex");
|
|
4893
|
+
}
|
|
4894
|
+
function codexAuthFile(env = process.env) {
|
|
4895
|
+
return join9(codexHomeDir(env), "auth.json");
|
|
4896
|
+
}
|
|
4897
|
+
function codexLoggedIn(env = process.env, fileExists = existsSync8) {
|
|
4898
|
+
return fileExists(codexAuthFile(env));
|
|
4899
|
+
}
|
|
4900
|
+
var init_codex_auth = () => {};
|
|
4901
|
+
|
|
4902
|
+
// src/doctor/doctor.ts
|
|
4903
|
+
import { existsSync as existsSync9, statSync } from "fs";
|
|
4904
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
4593
4905
|
async function versionProbe(exec, id, label, bin, fix) {
|
|
4594
4906
|
const out = await exec(bin, ["--version"]);
|
|
4595
4907
|
if (out.ok && out.code === 0) {
|
|
@@ -4621,9 +4933,6 @@ async function nodeProbe(exec) {
|
|
|
4621
4933
|
async function gitProbe(exec) {
|
|
4622
4934
|
return versionProbe(exec, "git", "Git", "git", "Install Git: winget install Git.Git / brew install git / apt install git");
|
|
4623
4935
|
}
|
|
4624
|
-
function codexAuthFile(env) {
|
|
4625
|
-
return join9(env.CODEX_HOME ?? join9(env.HOME ?? env.USERPROFILE ?? homedir2(), ".codex"), "auth.json");
|
|
4626
|
-
}
|
|
4627
4936
|
function statusUnsupported(out) {
|
|
4628
4937
|
return /unknown command|unrecognized subcommand|unexpected argument|invalid subcommand/i.test(`${out.stderr} ${out.stdout}`);
|
|
4629
4938
|
}
|
|
@@ -4655,23 +4964,38 @@ async function runtimeAuthProbe(spec, exec, env, fileExists, severity) {
|
|
|
4655
4964
|
fix: spec.installFix
|
|
4656
4965
|
};
|
|
4657
4966
|
}
|
|
4967
|
+
const authFile = spec.authFile?.(env);
|
|
4968
|
+
if (authFile) {
|
|
4969
|
+
const present = fileExists(authFile);
|
|
4970
|
+
const statusCommand = `${spec.bin} ${spec.authStatusArgs.join(" ")}`;
|
|
4971
|
+
let coda;
|
|
4972
|
+
if (statusUnsupported(out)) {
|
|
4973
|
+
coda = ` \u2014 this ${spec.bin} cannot report login state`;
|
|
4974
|
+
} else if (out.code === 0 === present) {
|
|
4975
|
+
coda = ` \u2014 \`${statusCommand}\` agrees`;
|
|
4976
|
+
} else if (present) {
|
|
4977
|
+
coda = `, but \`${statusCommand}\` reports logged out \u2014 burns borrow the file, so this host counts as signed in`;
|
|
4978
|
+
} else {
|
|
4979
|
+
coda = `, but \`${statusCommand}\` reports a login \u2014 a burn borrows the file, and it is not there`;
|
|
4980
|
+
}
|
|
4981
|
+
return present ? { ...base, status: "ok", detail: `credentials found at ${authFile}${coda}` } : { ...loggedOut, detail: `no credentials at ${authFile}${coda}` };
|
|
4982
|
+
}
|
|
4658
4983
|
if (out.code === 0)
|
|
4659
4984
|
return { ...base, status: "ok", detail: `${spec.bin} reports a login` };
|
|
4660
4985
|
if (!statusUnsupported(out))
|
|
4661
4986
|
return loggedOut;
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
detail: `this ${spec.bin} cannot report login state \u2014 it prompts at launch`
|
|
4668
|
-
};
|
|
4669
|
-
}
|
|
4670
|
-
return fileExists(authFile) ? { ...base, status: "ok", detail: `credentials found at ${authFile}` } : { ...loggedOut, detail: `no credentials at ${authFile}` };
|
|
4987
|
+
return {
|
|
4988
|
+
...base,
|
|
4989
|
+
status: "ok",
|
|
4990
|
+
detail: `this ${spec.bin} cannot report login state \u2014 it prompts at launch`
|
|
4991
|
+
};
|
|
4671
4992
|
}
|
|
4672
4993
|
function runtimeAfkKeyProbe(spec, env, severity = "error") {
|
|
4994
|
+
const id = spec.ids.afkKey;
|
|
4995
|
+
if (!id)
|
|
4996
|
+
return;
|
|
4673
4997
|
const base = {
|
|
4674
|
-
id
|
|
4998
|
+
id,
|
|
4675
4999
|
label: `${spec.label} AFK ${spec.afkNoun} (${spec.afkKey})`,
|
|
4676
5000
|
tier: 2,
|
|
4677
5001
|
severity,
|
|
@@ -4690,11 +5014,12 @@ function runtimeAfkKeyProbe(spec, env, severity = "error") {
|
|
|
4690
5014
|
};
|
|
4691
5015
|
}
|
|
4692
5016
|
async function runtimeProbes(spec, deps) {
|
|
4693
|
-
const fileExists = deps.fileExists ??
|
|
5017
|
+
const fileExists = deps.fileExists ?? existsSync9;
|
|
5018
|
+
const afkKeyProbe = runtimeAfkKeyProbe(spec, deps.env, deps.severity);
|
|
4694
5019
|
return [
|
|
4695
5020
|
await runtimeBinaryProbe(spec, deps.exec, deps.severity),
|
|
4696
5021
|
await runtimeAuthProbe(spec, deps.exec, deps.env, fileExists, deps.severity),
|
|
4697
|
-
|
|
5022
|
+
...afkKeyProbe ? [afkKeyProbe] : []
|
|
4698
5023
|
];
|
|
4699
5024
|
}
|
|
4700
5025
|
async function gitIdentityProbe(exec, cwd) {
|
|
@@ -4783,15 +5108,29 @@ async function containerRuntimeProbe(exec) {
|
|
|
4783
5108
|
fix: "Install Docker Desktop or Podman (see docs/research/PREREQS-NOTES.md \xA74). Not needed for interactive-only use."
|
|
4784
5109
|
};
|
|
4785
5110
|
}
|
|
4786
|
-
async function sandcastleImageProbe(exec, imageName) {
|
|
5111
|
+
async function sandcastleImageProbe(exec, imageName, burnerDockerfile, fileMtime) {
|
|
4787
5112
|
const id = "sandcastle-image";
|
|
4788
5113
|
const label = "Sandcastle container image";
|
|
4789
5114
|
for (const runtime of ["docker", "podman"]) {
|
|
4790
5115
|
const present = await exec(runtime, ["--version"]);
|
|
4791
5116
|
if (!(present.ok && present.code === 0))
|
|
4792
5117
|
continue;
|
|
4793
|
-
const inspect = await exec(runtime, ["image", "inspect", imageName]);
|
|
5118
|
+
const inspect = await exec(runtime, ["image", "inspect", "--format", "{{.Created}}", imageName]);
|
|
4794
5119
|
if (inspect.ok && inspect.code === 0) {
|
|
5120
|
+
const created = new Date(inspect.stdout.trim());
|
|
5121
|
+
const dockerfileChanged = fileMtime(burnerDockerfile);
|
|
5122
|
+
if (dockerfileChanged > created) {
|
|
5123
|
+
const date = (value) => value.toISOString().slice(0, 10);
|
|
5124
|
+
return {
|
|
5125
|
+
id,
|
|
5126
|
+
label,
|
|
5127
|
+
tier: 2,
|
|
5128
|
+
status: "stale",
|
|
5129
|
+
severity: "error",
|
|
5130
|
+
detail: `${imageName} built ${date(created)}, burner Dockerfile changed ${date(dockerfileChanged)} \u2014 rebuild`,
|
|
5131
|
+
fix: 'Open Settings \u2192 AFK burns and click "Rebuild image".'
|
|
5132
|
+
};
|
|
5133
|
+
}
|
|
4795
5134
|
return { id, label, tier: 2, status: "ok", severity: "error", detail: `${imageName} present` };
|
|
4796
5135
|
}
|
|
4797
5136
|
return {
|
|
@@ -4818,6 +5157,8 @@ async function runDoctor(env) {
|
|
|
4818
5157
|
const { exec } = env;
|
|
4819
5158
|
const processEnv = env.env ?? process.env;
|
|
4820
5159
|
const imageName = env.imageName ?? DEFAULT_SANDBOX_IMAGE;
|
|
5160
|
+
const burnerDockerfile = env.burnerDockerfile ?? fileURLToPath4(new URL("../assets/sandcastle/Dockerfile", import.meta.url));
|
|
5161
|
+
const fileMtime = env.fileMtime ?? ((path) => statSync(path).mtime);
|
|
4821
5162
|
const required = new Set(env.runtimes ?? [DEFAULT_RUNTIME]);
|
|
4822
5163
|
const results = [
|
|
4823
5164
|
await bunProbe(exec),
|
|
@@ -4832,7 +5173,7 @@ async function runDoctor(env) {
|
|
|
4832
5173
|
severity: required.has(runtime) ? "error" : "info"
|
|
4833
5174
|
}));
|
|
4834
5175
|
}
|
|
4835
|
-
results.push(await gitIdentityProbe(exec, env.cwd), await containerRuntimeProbe(exec), await sandcastleImageProbe(exec, imageName));
|
|
5176
|
+
results.push(await gitIdentityProbe(exec, env.cwd), await containerRuntimeProbe(exec), await sandcastleImageProbe(exec, imageName, burnerDockerfile, fileMtime));
|
|
4836
5177
|
const counts = (r) => r.severity === "error";
|
|
4837
5178
|
return {
|
|
4838
5179
|
results,
|
|
@@ -4848,6 +5189,7 @@ function exitCodeFor(report, mode) {
|
|
|
4848
5189
|
var RUNTIME_SPECS;
|
|
4849
5190
|
var init_doctor = __esm(() => {
|
|
4850
5191
|
init_src();
|
|
5192
|
+
init_codex_auth();
|
|
4851
5193
|
RUNTIME_SPECS = {
|
|
4852
5194
|
"claude-code": {
|
|
4853
5195
|
runtime: "claude-code",
|
|
@@ -4867,13 +5209,13 @@ var init_doctor = __esm(() => {
|
|
|
4867
5209
|
runtime: "codex",
|
|
4868
5210
|
label: "Codex",
|
|
4869
5211
|
bin: "codex",
|
|
4870
|
-
ids: { binary: "codex", auth: "codex-auth"
|
|
5212
|
+
ids: { binary: "codex", auth: "codex-auth" },
|
|
4871
5213
|
authStatusArgs: ["login", "status"],
|
|
4872
5214
|
loginCommand: "codex login",
|
|
4873
5215
|
loginArgs: ["login"],
|
|
4874
5216
|
afkKey: "CODEX_API_KEY",
|
|
4875
5217
|
afkNoun: "API key",
|
|
4876
|
-
afkFix: "
|
|
5218
|
+
afkFix: "Set CODEX_API_KEY in ~/.runcastle/.env to bill an OpenAI API key instead of your ChatGPT login",
|
|
4877
5219
|
installFix: "Install Codex: npm install -g @openai/codex (or: brew install codex)",
|
|
4878
5220
|
binOverrideEnv: "RUNCASTLE_CODEX_BIN",
|
|
4879
5221
|
authFile: codexAuthFile
|
|
@@ -4884,31 +5226,33 @@ var init_doctor = __esm(() => {
|
|
|
4884
5226
|
// src/launcher/runtimes/codex.ts
|
|
4885
5227
|
import {
|
|
4886
5228
|
copyFileSync,
|
|
4887
|
-
existsSync as
|
|
5229
|
+
existsSync as existsSync10,
|
|
4888
5230
|
mkdirSync as mkdirSync4,
|
|
4889
5231
|
readFileSync as readFileSync4,
|
|
4890
5232
|
readdirSync as readdirSync2,
|
|
4891
|
-
statSync,
|
|
5233
|
+
statSync as statSync2,
|
|
4892
5234
|
writeFileSync as writeFileSync3
|
|
4893
5235
|
} from "fs";
|
|
4894
|
-
import { homedir as homedir3 } from "os";
|
|
4895
5236
|
import { basename, dirname as dirname3, join as join10, resolve as resolve7 } from "path";
|
|
4896
|
-
function
|
|
5237
|
+
function codexHomeDir2(sessionId) {
|
|
4897
5238
|
return join10(sessionDir(sessionId), "codex-home");
|
|
4898
5239
|
}
|
|
4899
5240
|
function realCodexAuthFile(env = process.env) {
|
|
4900
|
-
return
|
|
5241
|
+
return codexAuthFile(env);
|
|
4901
5242
|
}
|
|
4902
5243
|
function toml(value) {
|
|
4903
5244
|
return JSON.stringify(value);
|
|
4904
5245
|
}
|
|
5246
|
+
function approvalPolicyFor(permissionMode) {
|
|
5247
|
+
return permissionMode === undefined || permissionMode === "acceptEdits" ? "never" : "untrusted";
|
|
5248
|
+
}
|
|
4905
5249
|
function renderCodexConfig(input) {
|
|
4906
|
-
const { session, config: config2, model, worktreePath, inheritedMcpServers } = input;
|
|
5250
|
+
const { session, config: config2, model, worktreePath, inheritedMcpServers, permissionMode } = input;
|
|
4907
5251
|
return [
|
|
4908
5252
|
`# Generated by runcastle for session ${session.id} \u2014 rewritten at every launch.`,
|
|
4909
5253
|
`model = ${toml(model)}`,
|
|
4910
5254
|
'sandbox_mode = "workspace-write"',
|
|
4911
|
-
|
|
5255
|
+
`approval_policy = ${toml(approvalPolicyFor(permissionMode))}`,
|
|
4912
5256
|
"",
|
|
4913
5257
|
`[projects.${toml(worktreePath)}]`,
|
|
4914
5258
|
'trust_level = "trusted"',
|
|
@@ -4940,7 +5284,7 @@ function mcpServerTables(userConfigToml) {
|
|
|
4940
5284
|
}
|
|
4941
5285
|
function readInheritedMcpServers(env = process.env) {
|
|
4942
5286
|
const path = join10(dirname3(realCodexAuthFile(env)), "config.toml");
|
|
4943
|
-
if (!
|
|
5287
|
+
if (!existsSync10(path))
|
|
4944
5288
|
return "";
|
|
4945
5289
|
try {
|
|
4946
5290
|
return mcpServerTables(readFileSync4(path, "utf8"));
|
|
@@ -4964,15 +5308,15 @@ function renderCodexHooks(hookClient, kind, platform = process.platform) {
|
|
|
4964
5308
|
};
|
|
4965
5309
|
}
|
|
4966
5310
|
function writeCodexSkills(worktreePath, packDir) {
|
|
4967
|
-
if (!
|
|
5311
|
+
if (!existsSync10(worktreePath))
|
|
4968
5312
|
return [];
|
|
4969
5313
|
const source = join10(packDir, "skills");
|
|
4970
|
-
if (!
|
|
5314
|
+
if (!existsSync10(source))
|
|
4971
5315
|
return [];
|
|
4972
5316
|
const written = [];
|
|
4973
5317
|
for (const name of readdirSync2(source)) {
|
|
4974
5318
|
const skill = join10(source, name, "SKILL.md");
|
|
4975
|
-
if (!
|
|
5319
|
+
if (!existsSync10(skill))
|
|
4976
5320
|
continue;
|
|
4977
5321
|
const target = join10(worktreePath, CODEX_SKILLS_REL, name);
|
|
4978
5322
|
mkdirSync4(target, { recursive: true });
|
|
@@ -4991,7 +5335,7 @@ function excludeFromGit(worktreePath, entry) {
|
|
|
4991
5335
|
return;
|
|
4992
5336
|
mkdirSync4(info, { recursive: true });
|
|
4993
5337
|
const path = join10(info, "exclude");
|
|
4994
|
-
const existing =
|
|
5338
|
+
const existing = existsSync10(path) ? readFileSync4(path, "utf8") : "";
|
|
4995
5339
|
if (existing.split(/\r?\n/).some((line) => line.trim() === entry))
|
|
4996
5340
|
return;
|
|
4997
5341
|
const head = existing.length > 0 && !existing.endsWith(`
|
|
@@ -5003,9 +5347,9 @@ function excludeFromGit(worktreePath, entry) {
|
|
|
5003
5347
|
}
|
|
5004
5348
|
function gitInfoDir(worktreePath) {
|
|
5005
5349
|
const dotGit = join10(worktreePath, ".git");
|
|
5006
|
-
if (!
|
|
5350
|
+
if (!existsSync10(dotGit))
|
|
5007
5351
|
return null;
|
|
5008
|
-
if (
|
|
5352
|
+
if (statSync2(dotGit).isDirectory())
|
|
5009
5353
|
return join10(dotGit, "info");
|
|
5010
5354
|
const named = /^gitdir:\s*(.+)$/m.exec(readFileSync4(dotGit, "utf8"));
|
|
5011
5355
|
if (!named)
|
|
@@ -5024,6 +5368,7 @@ var CODEX_BIN = "codex", CODEX, CODEX_SKILLS_REL, CODEX_SKILLS_IGNORE = ".agents
|
|
|
5024
5368
|
var init_codex = __esm(() => {
|
|
5025
5369
|
init_paths();
|
|
5026
5370
|
init_doctor();
|
|
5371
|
+
init_codex_auth();
|
|
5027
5372
|
init_resolve_executable();
|
|
5028
5373
|
init_artifacts();
|
|
5029
5374
|
init_edit_guard();
|
|
@@ -5046,7 +5391,7 @@ var init_codex = __esm(() => {
|
|
|
5046
5391
|
doctorHint: 'Run `runcastle doctor` and fix its "Codex CLI" probe, then restart runcastle from a ' + "terminal where `codex --version` works."
|
|
5047
5392
|
};
|
|
5048
5393
|
}
|
|
5049
|
-
if (!
|
|
5394
|
+
if (!existsSync10(realCodexAuthFile())) {
|
|
5050
5395
|
return {
|
|
5051
5396
|
ok: false,
|
|
5052
5397
|
reason: "Codex is installed but not logged in \u2014 runcastle copies your own credentials into " + `each session's home, and there are none at ${realCodexAuthFile()}`,
|
|
@@ -5056,7 +5401,7 @@ var init_codex = __esm(() => {
|
|
|
5056
5401
|
return { ok: true };
|
|
5057
5402
|
},
|
|
5058
5403
|
async writeArtifacts(input) {
|
|
5059
|
-
const home =
|
|
5404
|
+
const home = codexHomeDir2(input.session.id);
|
|
5060
5405
|
mkdirSync4(home, { recursive: true });
|
|
5061
5406
|
const configPath2 = join10(home, "config.toml");
|
|
5062
5407
|
const hooksPath = join10(home, "hooks.json");
|
|
@@ -5066,13 +5411,14 @@ var init_codex = __esm(() => {
|
|
|
5066
5411
|
config: input.config,
|
|
5067
5412
|
model: input.model,
|
|
5068
5413
|
worktreePath: input.worktreePath,
|
|
5414
|
+
...input.permissionMode ? { permissionMode: input.permissionMode } : {},
|
|
5069
5415
|
...input.config.sessionMcp === "inherit" ? { inheritedMcpServers: readInheritedMcpServers() } : {}
|
|
5070
5416
|
}), "utf8");
|
|
5071
5417
|
writeFileSync3(hooksPath, JSON.stringify(renderCodexHooks(hookClientPath(), input.session.kind), null, 2), "utf8");
|
|
5072
5418
|
writeFileSync3(agentsPath, renderSessionPrompt({ ...input, runtime: this.id }), "utf8");
|
|
5073
5419
|
const files = [configPath2, hooksPath, agentsPath];
|
|
5074
5420
|
const auth = realCodexAuthFile();
|
|
5075
|
-
if (
|
|
5421
|
+
if (existsSync10(auth)) {
|
|
5076
5422
|
const copied = join10(home, "auth.json");
|
|
5077
5423
|
copyFileSync(auth, copied);
|
|
5078
5424
|
files.push(copied);
|
|
@@ -5115,7 +5461,7 @@ var init_runtimes = __esm(() => {
|
|
|
5115
5461
|
});
|
|
5116
5462
|
|
|
5117
5463
|
// src/launcher/sessions.ts
|
|
5118
|
-
import { statSync as
|
|
5464
|
+
import { statSync as statSync3 } from "fs";
|
|
5119
5465
|
import { and as and5, desc as desc3, eq as eq5, inArray, isNotNull, sql as sql2 } from "drizzle-orm";
|
|
5120
5466
|
function createSessionRow(ctx, input) {
|
|
5121
5467
|
const row = ctx.db.insert(sessions).values({
|
|
@@ -5374,20 +5720,18 @@ function landProjectSession(ctx, session) {
|
|
|
5374
5720
|
const project = getProjectById(ctx, session.projectId);
|
|
5375
5721
|
if (!project)
|
|
5376
5722
|
return;
|
|
5377
|
-
const
|
|
5378
|
-
|
|
5379
|
-
reportProjectLanding(ctx, project, res, { sessionId: session.id });
|
|
5380
|
-
}).catch((e) => {
|
|
5723
|
+
const landFailed = (base, e) => {
|
|
5724
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
5381
5725
|
emitProject(ctx, project.id, {
|
|
5382
5726
|
type: "project.land_failed",
|
|
5383
|
-
message: `could not land the project session's work onto ${
|
|
5384
|
-
data: {
|
|
5385
|
-
sessionId: session.id,
|
|
5386
|
-
branch: PROJECT_BRANCH,
|
|
5387
|
-
error: e instanceof Error ? e.message : String(e)
|
|
5388
|
-
}
|
|
5727
|
+
message: `could not land the project session's work${base ? ` onto ${base}` : ""}: ${error} \u2014 ${LANDING_KEPT}`,
|
|
5728
|
+
data: { sessionId: session.id, branch: PROJECT_BRANCH, error }
|
|
5389
5729
|
});
|
|
5390
|
-
}
|
|
5730
|
+
};
|
|
5731
|
+
const landing = resolveSessionBranch(project).then((base) => landProjectBranch(project, base).then((res) => {
|
|
5732
|
+
if (res)
|
|
5733
|
+
reportProjectLanding(ctx, project, res, { sessionId: session.id });
|
|
5734
|
+
}, (e) => landFailed(base, e)), (e) => landFailed(null, e));
|
|
5391
5735
|
inFlightLandings.add(landing);
|
|
5392
5736
|
landing.finally(() => inFlightLandings.delete(landing));
|
|
5393
5737
|
}
|
|
@@ -5400,11 +5744,11 @@ function reportProjectLanding(ctx, project, res, data = {}) {
|
|
|
5400
5744
|
});
|
|
5401
5745
|
};
|
|
5402
5746
|
if (res.landed) {
|
|
5403
|
-
report("project.landed", `landed ${res.commits} commit(s) from the project session onto ${
|
|
5747
|
+
report("project.landed", `landed ${res.commits} commit(s) from the project session onto ${res.base}`, { commits: res.commits });
|
|
5404
5748
|
} else if (res.conflict) {
|
|
5405
|
-
report("project.land_conflict", `the project session's ${res.commits} commit(s) conflict with ${
|
|
5749
|
+
report("project.land_conflict", `the project session's ${res.commits} commit(s) conflict with ${res.base} \u2014 nothing was overwritten; ${LANDING_KEPT}`, { commits: res.commits, files: res.files ?? [] });
|
|
5406
5750
|
} else {
|
|
5407
|
-
report("project.land_failed", `could not land the project session's ${res.commits} commit(s) onto ${
|
|
5751
|
+
report("project.land_failed", `could not land the project session's ${res.commits} commit(s) onto ${res.base}: ${res.error ?? "unknown error"} \u2014 ${LANDING_KEPT}`, { commits: res.commits, error: res.error ?? null });
|
|
5408
5752
|
}
|
|
5409
5753
|
}
|
|
5410
5754
|
function markSessionEnded(ctx, id) {
|
|
@@ -5468,7 +5812,7 @@ function transcriptBytes(session) {
|
|
|
5468
5812
|
if (!session?.transcriptPath)
|
|
5469
5813
|
return;
|
|
5470
5814
|
try {
|
|
5471
|
-
return
|
|
5815
|
+
return statSync3(session.transcriptPath).size;
|
|
5472
5816
|
} catch {
|
|
5473
5817
|
return;
|
|
5474
5818
|
}
|
|
@@ -5583,7 +5927,8 @@ function rowToTicket(row) {
|
|
|
5583
5927
|
error: row.error ?? undefined,
|
|
5584
5928
|
attemptBranch: row.attemptBranch ?? undefined,
|
|
5585
5929
|
conflictFiles: row.conflictFiles ?? undefined,
|
|
5586
|
-
digest: row.digest ?? undefined
|
|
5930
|
+
digest: row.digest ?? undefined,
|
|
5931
|
+
originFindingId: row.originFindingId ?? undefined
|
|
5587
5932
|
});
|
|
5588
5933
|
}
|
|
5589
5934
|
function resolveBlocking(inputs, startSeq) {
|
|
@@ -5608,13 +5953,13 @@ function normalizeModel(ctx, value) {
|
|
|
5608
5953
|
function listByFeature2(ctx, featureId) {
|
|
5609
5954
|
return ctx.db.select().from(tickets).where(eq6(tickets.featureId, featureId)).orderBy(asc3(tickets.seq)).all().map(rowToTicket);
|
|
5610
5955
|
}
|
|
5611
|
-
function storeTickets(ctx, featureId, inputs) {
|
|
5956
|
+
function storeTickets(ctx, featureId, inputs, options = {}) {
|
|
5612
5957
|
if (inputs.length === 0)
|
|
5613
5958
|
return [];
|
|
5614
5959
|
const { lap } = getFeatureRow(ctx, featureId);
|
|
5615
5960
|
const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq6(tickets.featureId, featureId)).all();
|
|
5616
5961
|
const startSeq = existing.reduce((max2, r) => Math.max(max2, r.seq), 0) + 1;
|
|
5617
|
-
const resolved = resolveBlocking(inputs, startSeq);
|
|
5962
|
+
const resolved = options.blockedByAreGlobal ? inputs.map((input, index2) => ({ seq: startSeq + index2, blockedBy: input.blockedBy })) : resolveBlocking(inputs, startSeq);
|
|
5618
5963
|
const rows = inputs.map((t, i) => ({
|
|
5619
5964
|
id: newId("tkt"),
|
|
5620
5965
|
featureId,
|
|
@@ -5633,7 +5978,8 @@ function storeTickets(ctx, featureId, inputs) {
|
|
|
5633
5978
|
error: null,
|
|
5634
5979
|
attemptBranch: null,
|
|
5635
5980
|
conflictFiles: null,
|
|
5636
|
-
digest: null
|
|
5981
|
+
digest: null,
|
|
5982
|
+
originFindingId: t.originFindingId ?? null
|
|
5637
5983
|
}));
|
|
5638
5984
|
ctx.db.insert(tickets).values(rows).run();
|
|
5639
5985
|
emit(ctx, featureId, {
|
|
@@ -5745,7 +6091,7 @@ var init_tickets = __esm(() => {
|
|
|
5745
6091
|
});
|
|
5746
6092
|
|
|
5747
6093
|
// src/services/gates.ts
|
|
5748
|
-
import { existsSync as
|
|
6094
|
+
import { existsSync as existsSync11 } from "fs";
|
|
5749
6095
|
import { and as and7, desc as desc4, eq as eq7, inArray as inArray3 } from "drizzle-orm";
|
|
5750
6096
|
function checkGate(ctx, check, feature) {
|
|
5751
6097
|
switch (check) {
|
|
@@ -5795,7 +6141,7 @@ function notYetTerminal(noun, statuses, order) {
|
|
|
5795
6141
|
}
|
|
5796
6142
|
function fileGate(ctx, feature, fileName, reason) {
|
|
5797
6143
|
const project = projectForFeature(ctx, feature);
|
|
5798
|
-
return
|
|
6144
|
+
return existsSync11(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
|
|
5799
6145
|
}
|
|
5800
6146
|
function docGate(ctx, feature, fileName, reason) {
|
|
5801
6147
|
const file = fileGate(ctx, feature, fileName, reason);
|
|
@@ -5859,14 +6205,14 @@ var init_gates = __esm(() => {
|
|
|
5859
6205
|
});
|
|
5860
6206
|
|
|
5861
6207
|
// src/services/knowledge.ts
|
|
5862
|
-
import { existsSync as
|
|
6208
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5863
6209
|
import { join as join11, relative as relative2, resolve as resolve8, sep } from "path";
|
|
5864
6210
|
function scaffoldDocs(ctx, feature, opts = {}) {
|
|
5865
6211
|
const project = projectForFeature(ctx, feature);
|
|
5866
6212
|
const dir = featureDocsDir(project, feature);
|
|
5867
6213
|
mkdirSync5(dir, { recursive: true });
|
|
5868
6214
|
const briefPath = featureDocPath(project, feature, "brief.md");
|
|
5869
|
-
if (!
|
|
6215
|
+
if (!existsSync12(briefPath)) {
|
|
5870
6216
|
const override = opts.brief?.trim();
|
|
5871
6217
|
const created = new Date().toISOString();
|
|
5872
6218
|
const brief = override ? `${override}
|
|
@@ -5893,7 +6239,7 @@ function scaffoldDocs(ctx, feature, opts = {}) {
|
|
|
5893
6239
|
function scaffoldMapDoc(project, feature, seed) {
|
|
5894
6240
|
mkdirSync5(featureDocsDir(project, feature), { recursive: true });
|
|
5895
6241
|
const mapPath = featureDocPath(project, feature, "map.md");
|
|
5896
|
-
if (
|
|
6242
|
+
if (existsSync12(mapPath))
|
|
5897
6243
|
return;
|
|
5898
6244
|
writeFileSync4(mapPath, mapDocBody(feature, seed), "utf8");
|
|
5899
6245
|
}
|
|
@@ -5914,7 +6260,7 @@ function mapDocBody(feature, seed) {
|
|
|
5914
6260
|
}
|
|
5915
6261
|
function readCharter(repoRoot) {
|
|
5916
6262
|
const path = join11(repoRoot, CHARTER_FILE);
|
|
5917
|
-
if (!
|
|
6263
|
+
if (!existsSync12(path))
|
|
5918
6264
|
return;
|
|
5919
6265
|
try {
|
|
5920
6266
|
return readFileSync5(path, "utf8");
|
|
@@ -5924,7 +6270,7 @@ function readCharter(repoRoot) {
|
|
|
5924
6270
|
}
|
|
5925
6271
|
function listLiveAdrs(repoRoot) {
|
|
5926
6272
|
const dir = join11(repoRoot, ADR_DIR_REL);
|
|
5927
|
-
if (!
|
|
6273
|
+
if (!existsSync12(dir))
|
|
5928
6274
|
return [];
|
|
5929
6275
|
const docs2 = [];
|
|
5930
6276
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
@@ -5945,7 +6291,7 @@ function listLiveAdrs(repoRoot) {
|
|
|
5945
6291
|
function listDocs(ctx, feature) {
|
|
5946
6292
|
const project = projectForFeature(ctx, feature);
|
|
5947
6293
|
const dir = featureDocsDir(project, feature);
|
|
5948
|
-
if (!
|
|
6294
|
+
if (!existsSync12(dir))
|
|
5949
6295
|
return [];
|
|
5950
6296
|
return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join11(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
5951
6297
|
}
|
|
@@ -5958,7 +6304,7 @@ function readDoc(ctx, feature, relPath) {
|
|
|
5958
6304
|
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
5959
6305
|
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
5960
6306
|
}
|
|
5961
|
-
if (!
|
|
6307
|
+
if (!existsSync12(target))
|
|
5962
6308
|
throw new NotFoundError(`doc not found: ${relPath}`);
|
|
5963
6309
|
return { content: readFileSync5(target, "utf8") };
|
|
5964
6310
|
}
|
|
@@ -6015,13 +6361,197 @@ var init_end_session = __esm(() => {
|
|
|
6015
6361
|
init_registry();
|
|
6016
6362
|
});
|
|
6017
6363
|
|
|
6364
|
+
// src/services/review-findings.ts
|
|
6365
|
+
import { and as and8, asc as asc4, eq as eq8, isNotNull as isNotNull2 } from "drizzle-orm";
|
|
6366
|
+
function rowToFinding(row) {
|
|
6367
|
+
return ReviewFinding.parse(row);
|
|
6368
|
+
}
|
|
6369
|
+
function getFinding(ctx, id) {
|
|
6370
|
+
const row = ctx.db.select().from(reviewFindings).where(eq8(reviewFindings.id, id)).get();
|
|
6371
|
+
if (!row)
|
|
6372
|
+
throw new NotFoundError(`review finding ${id} not found`);
|
|
6373
|
+
return rowToFinding(row);
|
|
6374
|
+
}
|
|
6375
|
+
function listByFeature3(ctx, featureId) {
|
|
6376
|
+
return ctx.db.select().from(reviewFindings).where(eq8(reviewFindings.featureId, featureId)).orderBy(asc4(reviewFindings.createdAt), asc4(reviewFindings.id)).all().map(rowToFinding);
|
|
6377
|
+
}
|
|
6378
|
+
function buildFixTicket(finding) {
|
|
6379
|
+
return {
|
|
6380
|
+
title: finding.title,
|
|
6381
|
+
goal: `Fix: ${finding.title}`,
|
|
6382
|
+
context: [
|
|
6383
|
+
`Location: ${finding.location}`,
|
|
6384
|
+
`Citation: ${finding.citation}`,
|
|
6385
|
+
`Detail: ${finding.detail}`,
|
|
6386
|
+
`Repro step: ${finding.reproStep}`
|
|
6387
|
+
].join(`
|
|
6388
|
+
|
|
6389
|
+
`),
|
|
6390
|
+
acceptanceCriteria: [
|
|
6391
|
+
`The repro step no longer reproduces / the cited criterion holds: ${finding.reproStep}`
|
|
6392
|
+
],
|
|
6393
|
+
seams: [],
|
|
6394
|
+
blockedBy: [],
|
|
6395
|
+
kind: "implementation",
|
|
6396
|
+
originFindingId: finding.id
|
|
6397
|
+
};
|
|
6398
|
+
}
|
|
6399
|
+
function reportFinding(ctx, args) {
|
|
6400
|
+
const { featureId, reviewTicket, input } = args;
|
|
6401
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
6402
|
+
const id = newId("finding");
|
|
6403
|
+
const ticketCount = ctx.db.select({ id: reviewFindings.id }).from(reviewFindings).where(and8(eq8(reviewFindings.reviewTicketId, reviewTicket.id), isNotNull2(reviewFindings.fixTicketId))).all().length;
|
|
6404
|
+
const overCap = input.kind === "defect" && ticketCount >= AUTO_FIX_CAP;
|
|
6405
|
+
ctx.db.insert(reviewFindings).values({
|
|
6406
|
+
id,
|
|
6407
|
+
featureId,
|
|
6408
|
+
lap: feature.lap,
|
|
6409
|
+
reviewTicketId: reviewTicket.id,
|
|
6410
|
+
...input,
|
|
6411
|
+
reproStep: input.reproStep ?? "",
|
|
6412
|
+
status: "open",
|
|
6413
|
+
openReason: overCap ? "over-cap" : null,
|
|
6414
|
+
failureReason: null,
|
|
6415
|
+
fixTicketId: null,
|
|
6416
|
+
createdAt: Date.now()
|
|
6417
|
+
}).run();
|
|
6418
|
+
let fixTicket = null;
|
|
6419
|
+
if (input.kind === "defect" && !overCap) {
|
|
6420
|
+
const finding2 = getFinding(ctx, id);
|
|
6421
|
+
fixTicket = storeTickets(ctx, featureId, [
|
|
6422
|
+
{ ...buildFixTicket(finding2), blockedBy: [reviewTicket.seq] }
|
|
6423
|
+
], { blockedByAreGlobal: true })[0];
|
|
6424
|
+
ctx.db.update(reviewFindings).set({ fixTicketId: fixTicket.id }).where(eq8(reviewFindings.id, id)).run();
|
|
6425
|
+
}
|
|
6426
|
+
const finding = getFinding(ctx, id);
|
|
6427
|
+
emit(ctx, featureId, {
|
|
6428
|
+
type: "finding.reported",
|
|
6429
|
+
message: `${input.kind} reported: ${input.title}`,
|
|
6430
|
+
ticketId: fixTicket?.id,
|
|
6431
|
+
data: { findingId: id, fixTicketId: fixTicket?.id, overCap }
|
|
6432
|
+
});
|
|
6433
|
+
return { finding, fixTicket, overCap };
|
|
6434
|
+
}
|
|
6435
|
+
function updateStatus(ctx, findingId, patch) {
|
|
6436
|
+
const current2 = getFinding(ctx, findingId);
|
|
6437
|
+
ctx.db.update(reviewFindings).set(patch).where(eq8(reviewFindings.id, findingId)).run();
|
|
6438
|
+
emit(ctx, current2.featureId, {
|
|
6439
|
+
type: "finding.updated",
|
|
6440
|
+
message: `finding ${findingId} marked ${patch.status}`,
|
|
6441
|
+
data: { findingId, status: patch.status, reason: patch.failureReason }
|
|
6442
|
+
});
|
|
6443
|
+
return getFinding(ctx, findingId);
|
|
6444
|
+
}
|
|
6445
|
+
function markFixing(ctx, findingId) {
|
|
6446
|
+
return updateStatus(ctx, findingId, {
|
|
6447
|
+
status: "fixing",
|
|
6448
|
+
openReason: null,
|
|
6449
|
+
failureReason: null
|
|
6450
|
+
});
|
|
6451
|
+
}
|
|
6452
|
+
function markFixed(ctx, findingId) {
|
|
6453
|
+
return updateStatus(ctx, findingId, {
|
|
6454
|
+
status: "fixed",
|
|
6455
|
+
openReason: null,
|
|
6456
|
+
failureReason: null
|
|
6457
|
+
});
|
|
6458
|
+
}
|
|
6459
|
+
function markFailed(ctx, findingId, reason) {
|
|
6460
|
+
return updateStatus(ctx, findingId, {
|
|
6461
|
+
status: "failed",
|
|
6462
|
+
openReason: "fix-failed",
|
|
6463
|
+
failureReason: reason ?? null
|
|
6464
|
+
});
|
|
6465
|
+
}
|
|
6466
|
+
function markFixProgress(ctx, findingId, progress, reason) {
|
|
6467
|
+
if (progress === "fixing")
|
|
6468
|
+
return markFixing(ctx, findingId);
|
|
6469
|
+
if (progress === "fixed")
|
|
6470
|
+
return markFixed(ctx, findingId);
|
|
6471
|
+
return markFailed(ctx, findingId, reason);
|
|
6472
|
+
}
|
|
6473
|
+
function dismiss(ctx, findingId) {
|
|
6474
|
+
return updateStatus(ctx, findingId, {
|
|
6475
|
+
status: "dismissed",
|
|
6476
|
+
openReason: null,
|
|
6477
|
+
failureReason: null
|
|
6478
|
+
});
|
|
6479
|
+
}
|
|
6480
|
+
function defectState(finding, fixTicket) {
|
|
6481
|
+
if (finding.status === "dismissed")
|
|
6482
|
+
return "dismissed";
|
|
6483
|
+
if (finding.status === "fixed" || fixTicket?.status === "done")
|
|
6484
|
+
return "fixed";
|
|
6485
|
+
if (fixTicket && fixTicket.status !== "failed" && fixTicket.status !== "cancelled") {
|
|
6486
|
+
return "fixing";
|
|
6487
|
+
}
|
|
6488
|
+
return finding.status === "open" || finding.status === "failed" ? "open" : "fixing";
|
|
6489
|
+
}
|
|
6490
|
+
function viewByFeature(ctx, featureId) {
|
|
6491
|
+
const findings = listByFeature3(ctx, featureId);
|
|
6492
|
+
const tickets2 = listByFeature2(ctx, featureId);
|
|
6493
|
+
const summary = { found: 0, fixed: 0, open: 0, observations: 0 };
|
|
6494
|
+
const openDefects = [];
|
|
6495
|
+
for (const finding of findings) {
|
|
6496
|
+
if (finding.kind === "observation") {
|
|
6497
|
+
summary.observations += 1;
|
|
6498
|
+
continue;
|
|
6499
|
+
}
|
|
6500
|
+
summary.found += 1;
|
|
6501
|
+
const state = defectState(finding, fixTicketOf(finding, tickets2));
|
|
6502
|
+
if (state === "fixed")
|
|
6503
|
+
summary.fixed += 1;
|
|
6504
|
+
else if (state === "open") {
|
|
6505
|
+
summary.open += 1;
|
|
6506
|
+
openDefects.push(finding);
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
return { findings, openDefects, summary };
|
|
6510
|
+
}
|
|
6511
|
+
function fixTicketOf(finding, tickets2) {
|
|
6512
|
+
return finding.fixTicketId ? tickets2.find((t) => t.id === finding.fixTicketId) : undefined;
|
|
6513
|
+
}
|
|
6514
|
+
function promoteOpenDefects(ctx, featureId) {
|
|
6515
|
+
const defects = viewByFeature(ctx, featureId).openDefects;
|
|
6516
|
+
if (defects.length === 0)
|
|
6517
|
+
throw new InvalidInputError("no open defects to fix");
|
|
6518
|
+
const tickets2 = storeTickets(ctx, featureId, defects.map(buildFixTicket));
|
|
6519
|
+
const findings = defects.map((defect, index2) => {
|
|
6520
|
+
ctx.db.update(reviewFindings).set({
|
|
6521
|
+
status: "fixing",
|
|
6522
|
+
openReason: null,
|
|
6523
|
+
failureReason: null,
|
|
6524
|
+
fixTicketId: tickets2[index2].id
|
|
6525
|
+
}).where(eq8(reviewFindings.id, defect.id)).run();
|
|
6526
|
+
return getFinding(ctx, defect.id);
|
|
6527
|
+
});
|
|
6528
|
+
emit(ctx, featureId, {
|
|
6529
|
+
type: "finding.fixing",
|
|
6530
|
+
message: `${findings.length} open defect${findings.length === 1 ? "" : "s"} promoted to fix ticket${tickets2.length === 1 ? ` ${tickets2[0].seq}` : `s ${tickets2.map((t) => t.seq).join(", ")}`}`,
|
|
6531
|
+
data: { findingIds: findings.map((f) => f.id), seqs: tickets2.map((t) => t.seq) }
|
|
6532
|
+
});
|
|
6533
|
+
return { findings, tickets: tickets2 };
|
|
6534
|
+
}
|
|
6535
|
+
var AUTO_FIX_CAP = 8;
|
|
6536
|
+
var init_review_findings = __esm(() => {
|
|
6537
|
+
init_src();
|
|
6538
|
+
init_schema();
|
|
6539
|
+
init_errors();
|
|
6540
|
+
init_events();
|
|
6541
|
+
init_repo();
|
|
6542
|
+
init_tickets();
|
|
6543
|
+
});
|
|
6544
|
+
|
|
6018
6545
|
// src/services/settings.ts
|
|
6019
|
-
import { existsSync as
|
|
6546
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
6020
6547
|
import { dirname as dirname4 } from "path";
|
|
6021
6548
|
import * as z3 from "zod";
|
|
6022
|
-
import { eq as
|
|
6549
|
+
import { eq as eq9 } from "drizzle-orm";
|
|
6550
|
+
function hostDefaults(logicalCpus) {
|
|
6551
|
+
return { ...DEFAULTS, burnConcurrency: resolveDefaultBurnConcurrency(logicalCpus) };
|
|
6552
|
+
}
|
|
6023
6553
|
function readRawConfig(configFile) {
|
|
6024
|
-
if (!
|
|
6554
|
+
if (!existsSync13(configFile))
|
|
6025
6555
|
return {};
|
|
6026
6556
|
try {
|
|
6027
6557
|
const parsed = JSON.parse(readFileSync6(configFile, "utf8"));
|
|
@@ -6051,6 +6581,7 @@ function projectOverrides(ctx, projectId) {
|
|
|
6051
6581
|
const row = ctx.db.select({
|
|
6052
6582
|
model: projects.model,
|
|
6053
6583
|
sandbox: projects.sandbox,
|
|
6584
|
+
sessionBranch: projects.sessionBranch,
|
|
6054
6585
|
devCommand: projects.devCommand,
|
|
6055
6586
|
setupCommand: projects.setupCommand,
|
|
6056
6587
|
verifyCommands: projects.verifyCommands,
|
|
@@ -6058,10 +6589,11 @@ function projectOverrides(ctx, projectId) {
|
|
|
6058
6589
|
dbResetCommand: projects.dbResetCommand,
|
|
6059
6590
|
driveSetupCommand: projects.driveSetupCommand,
|
|
6060
6591
|
driveStopCommand: projects.driveStopCommand
|
|
6061
|
-
}).from(projects).where(
|
|
6592
|
+
}).from(projects).where(eq9(projects.id, projectId)).get();
|
|
6062
6593
|
return {
|
|
6063
6594
|
model: row?.model ?? null,
|
|
6064
6595
|
sandbox: row?.sandbox ?? null,
|
|
6596
|
+
sessionBranch: row?.sessionBranch ?? null,
|
|
6065
6597
|
devCommand: row?.devCommand ?? null,
|
|
6066
6598
|
setupCommand: row?.setupCommand ?? null,
|
|
6067
6599
|
verifyCommands: row?.verifyCommands ?? null,
|
|
@@ -6090,15 +6622,16 @@ function resolveField(desc5, layers) {
|
|
|
6090
6622
|
if (desc5.configKey && Object.prototype.hasOwnProperty.call(layers.fileRaw, desc5.configKey)) {
|
|
6091
6623
|
return { ...base, value: layers.fileRaw[desc5.configKey], source: "file", editable: true };
|
|
6092
6624
|
}
|
|
6093
|
-
const value = desc5.configKey ?
|
|
6625
|
+
const value = desc5.configKey ? layers.defaults[desc5.configKey] ?? null : null;
|
|
6094
6626
|
return { ...base, value, source: "default", editable: true };
|
|
6095
6627
|
}
|
|
6096
6628
|
function getSettings(ctx, projectId, io = {}) {
|
|
6097
6629
|
const env = io.env ?? process.env;
|
|
6098
6630
|
const fileRaw = readRawConfig(io.configFile ?? configPath());
|
|
6099
6631
|
const overrides = projectId !== undefined ? projectOverrides(ctx, projectId) : null;
|
|
6632
|
+
const defaults = hostDefaults(io.logicalCpus ?? hostLogicalCpus());
|
|
6100
6633
|
const visible = DESCRIPTORS.filter((d) => projectId !== undefined || d.configKey !== undefined);
|
|
6101
|
-
const fields = visible.map((d) => resolveField(d, { env, fileRaw, overrides }));
|
|
6634
|
+
const fields = visible.map((d) => resolveField(d, { env, fileRaw, overrides, defaults }));
|
|
6102
6635
|
return { projectId, fields: [...fields, ...stepModelFields(fileRaw)] };
|
|
6103
6636
|
}
|
|
6104
6637
|
function updateSettings(ctx, input, io = {}) {
|
|
@@ -6121,7 +6654,7 @@ function updateSettings(ctx, input, io = {}) {
|
|
|
6121
6654
|
if (!toProject)
|
|
6122
6655
|
throw new InvalidInputError(`${desc5.key} cannot be cleared`);
|
|
6123
6656
|
const project = requireProjectById(ctx, input.projectId);
|
|
6124
|
-
ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(
|
|
6657
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(eq9(projects.id, project.id)).run();
|
|
6125
6658
|
recordHuman(ctx, project.id, desc5.key, null);
|
|
6126
6659
|
emitProject(ctx, project.id, {
|
|
6127
6660
|
type: "settings.updated",
|
|
@@ -6137,7 +6670,7 @@ function updateSettings(ctx, input, io = {}) {
|
|
|
6137
6670
|
const value = parsed.data;
|
|
6138
6671
|
if (toProject) {
|
|
6139
6672
|
const project = requireProjectById(ctx, input.projectId);
|
|
6140
|
-
ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(
|
|
6673
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(eq9(projects.id, project.id)).run();
|
|
6141
6674
|
recordHuman(ctx, project.id, desc5.key, String(value));
|
|
6142
6675
|
emitProject(ctx, project.id, {
|
|
6143
6676
|
type: "settings.updated",
|
|
@@ -6216,6 +6749,7 @@ function field(view, key) {
|
|
|
6216
6749
|
var GLOBAL_EVENT_KEY = "global", idEnv = (raw) => raw, DESCRIPTORS, DEFAULTS, STEP_SET;
|
|
6217
6750
|
var init_settings = __esm(() => {
|
|
6218
6751
|
init_src();
|
|
6752
|
+
init_config_load();
|
|
6219
6753
|
init_paths();
|
|
6220
6754
|
init_schema();
|
|
6221
6755
|
init_errors();
|
|
@@ -6340,16 +6874,15 @@ var init_settings = __esm(() => {
|
|
|
6340
6874
|
parseEnv: idEnv
|
|
6341
6875
|
},
|
|
6342
6876
|
{
|
|
6343
|
-
key: "
|
|
6344
|
-
|
|
6345
|
-
envVar: "RUNCASTLE_MAIN_BRANCH",
|
|
6877
|
+
key: "devCommand",
|
|
6878
|
+
projectColumn: "devCommand",
|
|
6346
6879
|
restartRequired: false,
|
|
6347
6880
|
valueSchema: z3.string().min(1),
|
|
6348
6881
|
parseEnv: idEnv
|
|
6349
6882
|
},
|
|
6350
6883
|
{
|
|
6351
|
-
key: "
|
|
6352
|
-
projectColumn: "
|
|
6884
|
+
key: "sessionBranch",
|
|
6885
|
+
projectColumn: "sessionBranch",
|
|
6353
6886
|
restartRequired: false,
|
|
6354
6887
|
valueSchema: z3.string().min(1),
|
|
6355
6888
|
parseEnv: idEnv
|
|
@@ -6381,9 +6914,9 @@ var init_settings = __esm(() => {
|
|
|
6381
6914
|
});
|
|
6382
6915
|
|
|
6383
6916
|
// src/services/setup.ts
|
|
6384
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
6917
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
6385
6918
|
import { dirname as dirname5, join as join12 } from "path";
|
|
6386
|
-
import { fileURLToPath as
|
|
6919
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
6387
6920
|
async function writeGitIdentity(exec, identity) {
|
|
6388
6921
|
const name = identity.name.trim();
|
|
6389
6922
|
const email = identity.email.trim();
|
|
@@ -6486,10 +7019,10 @@ async function resolveRuntime(exec, preferred) {
|
|
|
6486
7019
|
function resolveSandcastleBin() {
|
|
6487
7020
|
try {
|
|
6488
7021
|
const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
|
|
6489
|
-
let dir = dirname5(
|
|
7022
|
+
let dir = dirname5(fileURLToPath5(mainUrl));
|
|
6490
7023
|
for (let hops = 0;hops < 6; hops++) {
|
|
6491
7024
|
const manifestPath = join12(dir, "package.json");
|
|
6492
|
-
if (
|
|
7025
|
+
if (existsSync14(manifestPath)) {
|
|
6493
7026
|
const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
|
|
6494
7027
|
if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
|
|
6495
7028
|
return join12(dir, manifest.bin.sandcastle);
|
|
@@ -6521,22 +7054,19 @@ function terminalSpec(kind, opts) {
|
|
|
6521
7054
|
};
|
|
6522
7055
|
}
|
|
6523
7056
|
function sandcastleTemplateDir() {
|
|
6524
|
-
return resolveAsset(ASSET_ENV.sandcastleTemplate,
|
|
7057
|
+
return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath5(new URL("../assets/sandcastle", import.meta.url)));
|
|
6525
7058
|
}
|
|
6526
|
-
function
|
|
7059
|
+
function refreshSandcastleConfig(templateDir, targetDir) {
|
|
6527
7060
|
const dir = join12(targetDir, ".sandcastle");
|
|
6528
|
-
if (existsSync13(dir))
|
|
6529
|
-
return { scaffolded: false, dir };
|
|
6530
7061
|
mkdirSync7(dir, { recursive: true });
|
|
6531
7062
|
for (const name of readdirSync4(templateDir)) {
|
|
6532
7063
|
copyFileSync2(join12(templateDir, name), join12(dir, name));
|
|
6533
7064
|
}
|
|
6534
|
-
return
|
|
7065
|
+
return dir;
|
|
6535
7066
|
}
|
|
6536
|
-
function prepareSandboxBuildContext() {
|
|
6537
|
-
const target = sandboxBuildDir();
|
|
7067
|
+
function prepareSandboxBuildContext(templateDir = sandcastleTemplateDir(), target = sandboxBuildDir()) {
|
|
6538
7068
|
mkdirSync7(target, { recursive: true });
|
|
6539
|
-
|
|
7069
|
+
refreshSandcastleConfig(templateDir, target);
|
|
6540
7070
|
return target;
|
|
6541
7071
|
}
|
|
6542
7072
|
function createCredentialVerifier(exec, runtime = DEFAULT_RUNTIME) {
|
|
@@ -6572,7 +7102,7 @@ function createCredentialVerifier(exec, runtime = DEFAULT_RUNTIME) {
|
|
|
6572
7102
|
function fileAfkTokenIo(exec, runtime = DEFAULT_RUNTIME) {
|
|
6573
7103
|
const path = envPath();
|
|
6574
7104
|
return {
|
|
6575
|
-
read: () =>
|
|
7105
|
+
read: () => existsSync14(path) ? readFileSync7(path, "utf8") : "",
|
|
6576
7106
|
write: (content) => {
|
|
6577
7107
|
mkdirSync7(dirname5(path), { recursive: true });
|
|
6578
7108
|
writeFileSync6(path, content, "utf8");
|
|
@@ -6596,7 +7126,7 @@ var init_setup = __esm(() => {
|
|
|
6596
7126
|
};
|
|
6597
7127
|
RUNTIME_AUTH_SETUP_HINT = {
|
|
6598
7128
|
"claude-code": RUNTIME_SPECS["claude-code"].afkFix,
|
|
6599
|
-
codex: RUNTIME_SPECS.codex.
|
|
7129
|
+
codex: `Run \`${RUNTIME_SPECS.codex.loginCommand}\` on this host, then burn again`
|
|
6600
7130
|
};
|
|
6601
7131
|
LOGIN_TERMINAL_KIND = {
|
|
6602
7132
|
"claude-code": "claude-login",
|
|
@@ -6604,6 +7134,32 @@ var init_setup = __esm(() => {
|
|
|
6604
7134
|
};
|
|
6605
7135
|
});
|
|
6606
7136
|
|
|
7137
|
+
// src/doctor/system-exec.ts
|
|
7138
|
+
import { spawn as spawn4 } from "child_process";
|
|
7139
|
+
function createSystemExec(opts = {}) {
|
|
7140
|
+
return (command, args) => new Promise((resolve9) => {
|
|
7141
|
+
const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
|
|
7142
|
+
let stdout = "";
|
|
7143
|
+
let stderr = "";
|
|
7144
|
+
const child = spawn4(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
|
|
7145
|
+
child.stdout?.on("data", (d) => {
|
|
7146
|
+
stdout += d.toString();
|
|
7147
|
+
});
|
|
7148
|
+
child.stderr?.on("data", (d) => {
|
|
7149
|
+
stderr += d.toString();
|
|
7150
|
+
});
|
|
7151
|
+
child.on("error", (err) => {
|
|
7152
|
+
resolve9({ ok: false, code: null, stdout, stderr: stderr || String(err) });
|
|
7153
|
+
});
|
|
7154
|
+
child.on("close", (code) => {
|
|
7155
|
+
resolve9({ ok: true, code: code ?? 0, stdout, stderr });
|
|
7156
|
+
});
|
|
7157
|
+
});
|
|
7158
|
+
}
|
|
7159
|
+
var init_system_exec = __esm(() => {
|
|
7160
|
+
init_resolve_executable();
|
|
7161
|
+
});
|
|
7162
|
+
|
|
6607
7163
|
// src/services/agent-stream.ts
|
|
6608
7164
|
function beginTranscript(ticketId) {
|
|
6609
7165
|
transcripts.set(ticketId, { live: true, chunks: [], nextIndex: 0, bytes: 0 });
|
|
@@ -6659,13 +7215,15 @@ function renderGuardScript(rules = GUARD_RULES) {
|
|
|
6659
7215
|
"command -v jq >/dev/null 2>&1 || exit 0",
|
|
6660
7216
|
'tool=$(printf %s "$payload" | jq -r ".tool_name // empty" 2>/dev/null || true)',
|
|
6661
7217
|
'[ "$tool" = "Bash" ] || exit 0',
|
|
6662
|
-
'
|
|
6663
|
-
'[ -n "$
|
|
7218
|
+
'raw_cmd=$(printf %s "$payload" | jq -r ".tool_input.command // empty" 2>/dev/null || true)',
|
|
7219
|
+
'[ -n "$raw_cmd" ] || exit 0',
|
|
6664
7220
|
"",
|
|
6665
7221
|
"# Match against the command with quoted spans blanked, so an argument is",
|
|
6666
7222
|
'# never read as a command: `grep -rn "git stash" docs/` searches for a',
|
|
6667
|
-
"# string, it does not run one.",
|
|
6668
|
-
|
|
7223
|
+
"# string, it does not run one. Flatten newlines so one grep sees a whole",
|
|
7224
|
+
"# multiline loop.",
|
|
7225
|
+
"cmd=$raw_cmd",
|
|
7226
|
+
`cmd=$(printf %s "$cmd" | sed "s/'[^']*'/ /g; s/\\"[^\\"]*\\"/ /g" | tr '\\n' ' ')`,
|
|
6669
7227
|
"",
|
|
6670
7228
|
"deny() {",
|
|
6671
7229
|
` printf %s "$1" | jq -R -s '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:.}}'`,
|
|
@@ -6674,7 +7232,8 @@ function renderGuardScript(rules = GUARD_RULES) {
|
|
|
6674
7232
|
""
|
|
6675
7233
|
];
|
|
6676
7234
|
for (const rule of rules) {
|
|
6677
|
-
|
|
7235
|
+
const commandVariable = rule.inspectQuotedArguments ? "$raw_cmd" : "$cmd";
|
|
7236
|
+
lines.push(`# ${rule.id}`, `if printf %s "${commandVariable}" | grep -Eq -- ${shSingleQuote(rule.pattern)}; then`, ` deny ${shSingleQuote(rule.reason)}`, "fi", "");
|
|
6678
7237
|
}
|
|
6679
7238
|
lines.push("exit 0", "");
|
|
6680
7239
|
return lines.join(`
|
|
@@ -6724,11 +7283,37 @@ var init_burn_guard = __esm(() => {
|
|
|
6724
7283
|
pattern: "--maxWorkers|--runInBand|--pool=|--shard=|singleFork|--poolOptions",
|
|
6725
7284
|
reason: `Do not override the test runner's concurrency. Measured in this sandbox, a suite that runs in ~55s at its configured concurrency takes 10-20 minutes serialised, so this "safe" flag is the single most expensive habit in a burn. Run the repo's test command as configured. If it is killed for memory, run only the test files your change touches and say so in your final message.`
|
|
6726
7285
|
},
|
|
7286
|
+
{
|
|
7287
|
+
id: "no-long-sleep",
|
|
7288
|
+
pattern: `${CMD_START}sleep[[:space:]]+((3[1-9]|[4-9][0-9]|[1-9][0-9]{2,})s?|[1-9][0-9]*(m|h|d))([^A-Za-z0-9_]|$)`,
|
|
7289
|
+
reason: "Polling and long sleeps are never needed in a burn. Run the command directly and read its output."
|
|
7290
|
+
},
|
|
7291
|
+
{
|
|
7292
|
+
id: "no-verification-polling-loop",
|
|
7293
|
+
pattern: `${CMD_START}(until|while)[[:space:]].*(tsc|typecheck|vitest|jest|pytest|mocha|(npm|pnpm|yarn|bun)[[:space:]]+(run[[:space:]]+)?test|turbo[[:space:]]+run[[:space:]]+test).*[;[:space:]][[:space:]]*done`,
|
|
7294
|
+
reason: "Polling and long sleeps are never needed in a burn. Run the command directly and read its output."
|
|
7295
|
+
},
|
|
6727
7296
|
{
|
|
6728
7297
|
id: "no-interpreter-heredoc-edit",
|
|
6729
7298
|
pattern: `${CMD_START}(python3?|node|bun|perl|ruby)[[:space:]]+-[[:space:]]*<<`,
|
|
6730
7299
|
reason: "Do not rewrite files by piping a heredoc into an interpreter \u2014 individual such calls have been measured at 29s, 57s, 120s and 761s, and a half-written file survives if this process dies mid-command. Use the Edit tool, which is faster, atomic, and fails loudly when its target text is not found."
|
|
6731
7300
|
},
|
|
7301
|
+
{
|
|
7302
|
+
id: "no-interpreter-inline-edit",
|
|
7303
|
+
pattern: `${CMD_START}node[[:space:]]+-e([[:space:]]|$)`,
|
|
7304
|
+
reason: "Do not rewrite files with a `node -e` program. Use the Edit tool, which is faster, atomic, and fails loudly when its target text is not found."
|
|
7305
|
+
},
|
|
7306
|
+
{
|
|
7307
|
+
id: "no-perl-in-place-edit",
|
|
7308
|
+
pattern: `${CMD_START}perl[[:space:]]+-[0-9p]*i[^[:space:]]*`,
|
|
7309
|
+
reason: "Do not rewrite files with Perl in-place editing. Use the Edit tool, which is faster, atomic, and fails loudly when its target text is not found."
|
|
7310
|
+
},
|
|
7311
|
+
{
|
|
7312
|
+
id: "no-sed-multi-range-edit",
|
|
7313
|
+
pattern: `${CMD_START}sed[[:space:]]+-[^[:space:]]*i[^[:space:]]*[[:space:]]+[^;]*,[^;]*d;[^;]*,[^;]*d`,
|
|
7314
|
+
inspectQuotedArguments: true,
|
|
7315
|
+
reason: "Do not perform multi-range file surgery with `sed -i`. Use the Edit tool, which is atomic and fails loudly when its target text is not found."
|
|
7316
|
+
},
|
|
6732
7317
|
{
|
|
6733
7318
|
id: "no-cat-heredoc-edit",
|
|
6734
7319
|
pattern: `${CMD_START}cat[[:space:]]+>>?[[:space:]]*[^[:space:]|]+[[:space:]]*<<`,
|
|
@@ -6753,7 +7338,7 @@ var init_burn_guard = __esm(() => {
|
|
|
6753
7338
|
});
|
|
6754
7339
|
|
|
6755
7340
|
// src/workflows/review-ticket.ts
|
|
6756
|
-
import { existsSync as
|
|
7341
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync8, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
6757
7342
|
import { delimiter, join as join13 } from "path";
|
|
6758
7343
|
import { run } from "@ai-hero/sandcastle";
|
|
6759
7344
|
import { noSandbox } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
|
|
@@ -6769,12 +7354,43 @@ function findOnPath(bin, env = process.env, platform = process.platform) {
|
|
|
6769
7354
|
for (const dir of dirs) {
|
|
6770
7355
|
for (const suffix of suffixes) {
|
|
6771
7356
|
const candidate = join13(dir, `${bin}${suffix}`);
|
|
6772
|
-
if (
|
|
7357
|
+
if (existsSync15(candidate))
|
|
6773
7358
|
return candidate;
|
|
6774
7359
|
}
|
|
6775
7360
|
}
|
|
6776
7361
|
return;
|
|
6777
7362
|
}
|
|
7363
|
+
function buildDriveAvailability(browserPath, devCommand) {
|
|
7364
|
+
const missing = [];
|
|
7365
|
+
if (!browserPath) {
|
|
7366
|
+
missing.push(`\`${AGENT_BROWSER_BIN}\` is not on this machine's PATH, so there is no browser to walk the app with`);
|
|
7367
|
+
}
|
|
7368
|
+
if (!devCommand?.trim()) {
|
|
7369
|
+
missing.push("this project has no dev command configured, so a drive has no app to boot");
|
|
7370
|
+
}
|
|
7371
|
+
if (missing.length === 0) {
|
|
7372
|
+
return `A drive **is** available: \`${AGENT_BROWSER_BIN}\` is on this machine's PATH and the ` + "project has a dev command, so `review_drive` can boot the app and you can walk it. Drive " + "mode is open to you \u2014 take it if, and only if, this lap has a surface a human could operate.";
|
|
7373
|
+
}
|
|
7374
|
+
return `A drive is **not** available: ${missing.join(", and ")}. So the mode is already decided \u2014 ` + "run Gates mode, whatever this lap touched, and do not call `review_drive`. This is not a " + "degraded review; it is the whole review this lap gets.";
|
|
7375
|
+
}
|
|
7376
|
+
function buildGateNotes(config2) {
|
|
7377
|
+
const commands = config2.verifyCommands?.trim();
|
|
7378
|
+
const failures = config2.knownFailures?.trim();
|
|
7379
|
+
const out = [];
|
|
7380
|
+
if (commands) {
|
|
7381
|
+
out.push("Run exactly these, once each \u2014 they are this project's own verify commands, so do not go looking for alternatives, add concurrency flags, or re-run one to re-read its output (redirect to a file and read that instead):", "", "```", commands, "```");
|
|
7382
|
+
} else {
|
|
7383
|
+
out.push("This project has no verify commands configured, so there are no gates to run. Do not go hunting for them \u2014 say so in one line of your summary note and spend the whole mode on the diff.");
|
|
7384
|
+
}
|
|
7385
|
+
out.push("");
|
|
7386
|
+
if (failures) {
|
|
7387
|
+
out.push("These already fail on this repo without this lap's help:", "", "```", failures, "```", "", "Subtract that baseline: a failure inside it is not this lap's and is not a finding. A failure outside it is one this lap introduced, and it is the finding worth the human's attention above every other.");
|
|
7388
|
+
} else {
|
|
7389
|
+
out.push("No pre-existing-failure baseline is configured, so a red gate may well predate this lap. Run it once, then check whether the failure touches the diff before writing it up as this lap's \u2014 and never re-run a suite to establish what was already red.");
|
|
7390
|
+
}
|
|
7391
|
+
return out.join(`
|
|
7392
|
+
`);
|
|
7393
|
+
}
|
|
6778
7394
|
function writeReviewArtifacts(ticket, runId, config2) {
|
|
6779
7395
|
const dir = reviewDir(ticket.id);
|
|
6780
7396
|
rmSync2(dir, { recursive: true, force: true });
|
|
@@ -6796,9 +7412,18 @@ async function releaseDriveQuietly() {
|
|
|
6796
7412
|
} catch {}
|
|
6797
7413
|
}
|
|
6798
7414
|
async function executeReviewTicket(ctx, ticket, deps) {
|
|
7415
|
+
const startedAt = Date.now();
|
|
7416
|
+
const timer = createToolTimer();
|
|
7417
|
+
try {
|
|
7418
|
+
return await reviewTicketOutcome(ctx, ticket, deps, timer);
|
|
7419
|
+
} finally {
|
|
7420
|
+
emitTicketTiming(ctx, ticket, buildTicketTiming(timer.summary(), startedAt, Date.now()));
|
|
7421
|
+
}
|
|
7422
|
+
}
|
|
7423
|
+
async function reviewTicketOutcome(ctx, ticket, deps, timer) {
|
|
6799
7424
|
const { project, feature } = ctx;
|
|
6800
|
-
if (!
|
|
6801
|
-
return couldNotReview(ticket,
|
|
7425
|
+
if (!feature.baseBranch) {
|
|
7426
|
+
return couldNotReview(ticket, `feature ${feature.slug} has no recorded base branch, so there is nothing to diff \`${feature.branch}\` against.`);
|
|
6802
7427
|
}
|
|
6803
7428
|
const artifacts = writeReviewArtifacts(ticket, ctx.runId, deps.config);
|
|
6804
7429
|
const prompt = renderReviewPrompt(readFileSync8(reviewTemplatePath(), "utf8"), {
|
|
@@ -6807,7 +7432,9 @@ async function executeReviewTicket(ctx, ticket, deps) {
|
|
|
6807
7432
|
DOCS_DIGEST: deps.docsDigest,
|
|
6808
7433
|
LAP_DIGESTS: buildLapDigestsBlock(deps.lapDigests),
|
|
6809
7434
|
FEATURE_BRANCH: feature.branch,
|
|
6810
|
-
BASE_BRANCH:
|
|
7435
|
+
BASE_BRANCH: feature.baseBranch,
|
|
7436
|
+
DRIVE_AVAILABILITY: buildDriveAvailability(findOnPath(AGENT_BROWSER_BIN), project.devCommand),
|
|
7437
|
+
GATE_NOTES: buildGateNotes(deps.config),
|
|
6811
7438
|
DIGEST_PATH: artifacts.digestPath,
|
|
6812
7439
|
BLOCKED_PATH: artifacts.blockedPath,
|
|
6813
7440
|
WALKTHROUGH_PATH: artifacts.walkthroughPath
|
|
@@ -6817,6 +7444,7 @@ async function executeReviewTicket(ctx, ticket, deps) {
|
|
|
6817
7444
|
beginTranscript(ticket.id);
|
|
6818
7445
|
const onStreamEvent = (event) => {
|
|
6819
7446
|
throttle.onEvent(event);
|
|
7447
|
+
timer.onEvent(event);
|
|
6820
7448
|
if (event.type === "text") {
|
|
6821
7449
|
appendTranscript(ticket.id, { kind: "text", text: event.message });
|
|
6822
7450
|
} else if (event.type === "toolCall") {
|
|
@@ -6889,13 +7517,22 @@ var init_review_ticket = __esm(() => {
|
|
|
6889
7517
|
});
|
|
6890
7518
|
|
|
6891
7519
|
// src/workflows/ticket-burner.ts
|
|
6892
|
-
import { existsSync as
|
|
7520
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
|
|
6893
7521
|
import { dirname as dirname6, join as join14, posix as posixPath, win32 as winPath } from "path";
|
|
6894
|
-
import { fileURLToPath as
|
|
7522
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
6895
7523
|
import { claudeCode, codex, run as run2 } from "@ai-hero/sandcastle";
|
|
6896
7524
|
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
6897
7525
|
import { podman } from "@ai-hero/sandcastle/sandboxes/podman";
|
|
6898
7526
|
import { noSandbox as noSandbox2 } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
|
|
7527
|
+
function missingImageRuntimeMessage(runtime, image) {
|
|
7528
|
+
const binary = RUNTIME_BINARY[runtime];
|
|
7529
|
+
return `${binary} is not installed in image ${image} \u2014 the image predates the burner Dockerfile. Rebuild it from Settings \u2192 AFK burns (Rebuild image).`;
|
|
7530
|
+
}
|
|
7531
|
+
function burnAuthReady(runtime, token, loggedIn = codexLoggedIn) {
|
|
7532
|
+
if (token !== undefined)
|
|
7533
|
+
return true;
|
|
7534
|
+
return runtime === "codex" && loggedIn();
|
|
7535
|
+
}
|
|
6899
7536
|
function isReviewTicket(ticket) {
|
|
6900
7537
|
return ticket.kind === "review";
|
|
6901
7538
|
}
|
|
@@ -7036,10 +7673,10 @@ _(The "${DROPPED_MAP_SECTIONS.join('" and "')}" sections are omitted here: they
|
|
|
7036
7673
|
}
|
|
7037
7674
|
function buildProjectStandards(repoPath) {
|
|
7038
7675
|
const present = [];
|
|
7039
|
-
if (
|
|
7676
|
+
if (existsSync16(join14(repoPath, AGENT_CONVENTIONS_FILE))) {
|
|
7040
7677
|
present.push("`CLAUDE.md` \u2014 this repo's agent-facing conventions, and the highest authority on them");
|
|
7041
7678
|
}
|
|
7042
|
-
if (
|
|
7679
|
+
if (existsSync16(join14(repoPath, CHARTER_FILE))) {
|
|
7043
7680
|
present.push(`\`${CHARTER_FILE}\` \u2014 the project charter: what this codebase is for`);
|
|
7044
7681
|
}
|
|
7045
7682
|
const adrs = listLiveAdrs(repoPath).map((a) => a.relPath);
|
|
@@ -7070,7 +7707,7 @@ function buildDriveNotes(settings) {
|
|
|
7070
7707
|
add("stop", settings.driveStopCommand);
|
|
7071
7708
|
add("dev server", settings.devCommand);
|
|
7072
7709
|
add("db reset", settings.dbResetCommand);
|
|
7073
|
-
const hasDir =
|
|
7710
|
+
const hasDir = existsSync16(join14(settings.repoPath, ".runcastle"));
|
|
7074
7711
|
if (configured.length === 0 && !hasDir) {
|
|
7075
7712
|
return [
|
|
7076
7713
|
"This project has no test-drive machinery configured, so there is nothing here for you to keep in step \u2014 do not invent any.",
|
|
@@ -7093,7 +7730,9 @@ function buildDriveNotes(settings) {
|
|
|
7093
7730
|
`);
|
|
7094
7731
|
}
|
|
7095
7732
|
function buildGuardNotes(guardInstalled) {
|
|
7096
|
-
|
|
7733
|
+
const introduction = guardInstalled ? "The following commands are **denied before they run** by a tool hook. Each denial and its prescribed alternative comes verbatim from the guard that enforces it. A denial is policy, not a broken environment \u2014 follow the alternative and do not try to route around it." : "The deny hook is not installed in this burn, so the following guard rules are not machine-enforced. Follow them on discipline alone; each denial and its prescribed alternative comes verbatim from the guard used in protected burns.";
|
|
7734
|
+
return [introduction, "", ...GUARD_RULES.map((rule) => `- ${rule.reason}`)].join(`
|
|
7735
|
+
`);
|
|
7097
7736
|
}
|
|
7098
7737
|
function buildBlockersBlock(blockedBy, digests) {
|
|
7099
7738
|
if (blockedBy.length === 0) {
|
|
@@ -7116,6 +7755,16 @@ ${d.digest.trim()}`;
|
|
|
7116
7755
|
return [head, "", "In their own words:", "", ...bodies].join(`
|
|
7117
7756
|
`);
|
|
7118
7757
|
}
|
|
7758
|
+
function buildFixNotes(ticket) {
|
|
7759
|
+
if (!ticket.originFindingId)
|
|
7760
|
+
return "";
|
|
7761
|
+
return [
|
|
7762
|
+
"### This ticket fixes a review finding",
|
|
7763
|
+
"",
|
|
7764
|
+
"It was minted from a defect the review of this same lap reported, and its `context` carries the reviewer's repro step verbatim. **Before you declare done, re-run that repro step exactly, and say in your digest that you re-ran it and what happened.** Nothing reviews your change afterwards \u2014 that repro step is the check, and the verify commands run on top of it."
|
|
7765
|
+
].join(`
|
|
7766
|
+
`);
|
|
7767
|
+
}
|
|
7119
7768
|
function buildLapDigestsBlock(digests) {
|
|
7120
7769
|
if (digests.length === 0) {
|
|
7121
7770
|
return "_No implementation ticket in this burn left a digest \u2014 the diff and the feature docs are all you have._";
|
|
@@ -7152,13 +7801,13 @@ function parseEnvFile2(content) {
|
|
|
7152
7801
|
if (line.length === 0 || line.startsWith("#"))
|
|
7153
7802
|
continue;
|
|
7154
7803
|
const body = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
7155
|
-
const
|
|
7156
|
-
if (
|
|
7804
|
+
const eq10 = body.indexOf("=");
|
|
7805
|
+
if (eq10 === -1)
|
|
7157
7806
|
continue;
|
|
7158
|
-
const key = body.slice(0,
|
|
7807
|
+
const key = body.slice(0, eq10).trim();
|
|
7159
7808
|
if (key.length === 0)
|
|
7160
7809
|
continue;
|
|
7161
|
-
let value = body.slice(
|
|
7810
|
+
let value = body.slice(eq10 + 1).trim();
|
|
7162
7811
|
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
7163
7812
|
value = value.slice(1, -1);
|
|
7164
7813
|
}
|
|
@@ -7206,7 +7855,31 @@ function cacheMountFor(pm, hostPath) {
|
|
|
7206
7855
|
const sandboxPath = PM_CACHE_SANDBOX_PATHS[pm];
|
|
7207
7856
|
return sandboxPath ? { hostPath, sandboxPath } : undefined;
|
|
7208
7857
|
}
|
|
7858
|
+
function buildBurnCacheMounts(slot, projectId, sandbox, pm) {
|
|
7859
|
+
if (slot !== undefined) {
|
|
7860
|
+
return {
|
|
7861
|
+
mounts: [{ volume: burnCacheVolumeName(projectId), sandboxPath: BURN_CACHE_MOUNT }],
|
|
7862
|
+
env: pm ? burnCacheEnv(pm) : {}
|
|
7863
|
+
};
|
|
7864
|
+
}
|
|
7865
|
+
const mount = sandbox !== "noSandbox" && pm ? cacheMountFor(pm, burnCacheDir(pm)) : undefined;
|
|
7866
|
+
return { mounts: mount ? [mount] : [], env: {} };
|
|
7867
|
+
}
|
|
7868
|
+
function codexAuthMountFor(runtime, sandbox, env = process.env, loggedIn = codexLoggedIn) {
|
|
7869
|
+
if (runtime !== "codex" || sandbox === "noSandbox" || !loggedIn(env))
|
|
7870
|
+
return;
|
|
7871
|
+
return { hostPath: codexHomeDir(env), sandboxPath: CODEX_HOST_MOUNT_PATH, readonly: true };
|
|
7872
|
+
}
|
|
7873
|
+
function buildCodexAuthCopyCommand() {
|
|
7874
|
+
return `mkdir -p "$HOME/.codex" && cp "${CODEX_HOST_MOUNT_PATH}/auth.json" "$HOME/.codex/auth.json"`;
|
|
7875
|
+
}
|
|
7876
|
+
function chainSetupCommands(...steps) {
|
|
7877
|
+
const present = steps.filter((step) => !!step);
|
|
7878
|
+
return present.length > 0 ? present.join(" && ") : undefined;
|
|
7879
|
+
}
|
|
7209
7880
|
function resolveBurnWorkspaceMode(config2, platform = process.platform) {
|
|
7881
|
+
if (resolveBurnCacheMode(config2) === "volume")
|
|
7882
|
+
return "slot";
|
|
7210
7883
|
if (config2.sandbox === "noSandbox")
|
|
7211
7884
|
return "mounted";
|
|
7212
7885
|
if (config2.burnWorkspace === "auto")
|
|
@@ -7214,26 +7887,84 @@ function resolveBurnWorkspaceMode(config2, platform = process.platform) {
|
|
|
7214
7887
|
return config2.burnWorkspace;
|
|
7215
7888
|
}
|
|
7216
7889
|
function buildIsolatedSetupCommand(tempBranch, setupCommand, pm) {
|
|
7217
|
-
const
|
|
7218
|
-
|
|
7219
|
-
const cloneAttachmentsDir = `${ISOLATED_REPO_PATH}/${ATTACHMENTS_DIR}`;
|
|
7220
|
-
const parts = [
|
|
7890
|
+
const steps = buildRepoSetupSteps(ISOLATED_REPO_PATH, tempBranch, setupCommand, pm);
|
|
7891
|
+
return [
|
|
7221
7892
|
`git config --global --add safe.directory '*'`,
|
|
7222
7893
|
`git clone ${SANDBOX_WORKSPACE_PATH} ${ISOLATED_REPO_PATH}`,
|
|
7223
|
-
|
|
7224
|
-
|
|
7894
|
+
...steps.pre,
|
|
7895
|
+
...steps.install,
|
|
7896
|
+
...steps.post
|
|
7897
|
+
].join(" && ");
|
|
7898
|
+
}
|
|
7899
|
+
function buildRepoSetupSteps(repoPath, tempBranch, setupCommand, pm) {
|
|
7900
|
+
const hookFile = `${repoPath}/.git/hooks/post-commit`;
|
|
7901
|
+
const attachmentsDir = `${SANDBOX_WORKSPACE_PATH}/${ATTACHMENTS_DIR}`;
|
|
7902
|
+
const repoAttachmentsDir = `${repoPath}/${ATTACHMENTS_DIR}`;
|
|
7903
|
+
const excludeFile = `${repoPath}/.git/info/exclude`;
|
|
7904
|
+
const pre = [
|
|
7905
|
+
`if [ -d "${attachmentsDir}" ]; then mkdir -p "${repoAttachmentsDir}" && cp -r "${attachmentsDir}/." "${repoAttachmentsDir}/" && mkdir -p "${repoPath}/.git/info" && { grep -qxF '${ATTACHMENTS_DIR}/' "${excludeFile}" 2>/dev/null || printf '%s\\n' '${ATTACHMENTS_DIR}/' >> "${excludeFile}"; }; fi`,
|
|
7906
|
+
`printf '#!/bin/sh\\nunset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE\\ngit push --quiet origin HEAD:%s && exit 0\\nsleep 2\\ngit push --quiet origin HEAD:%s && exit 0\\necho "runcastle: commit sync failed (will retry on your next commit); do not re-commit" >&2\\nexit 0\\n' '${tempBranch}' '${tempBranch}' > ${hookFile}`,
|
|
7225
7907
|
`chmod +x ${hookFile}`
|
|
7226
7908
|
];
|
|
7227
7909
|
if (pm === "pnpm" || pm === "yarn") {
|
|
7228
7910
|
const shim = `$HOME/.local/bin/${pm}`;
|
|
7229
|
-
|
|
7911
|
+
pre.push(`mkdir -p "$HOME/.local/bin"`, `printf '#!/bin/sh\\nexec corepack ${pm} "$@"\\n' > "${shim}"`, `chmod +x "${shim}"`);
|
|
7912
|
+
}
|
|
7913
|
+
return {
|
|
7914
|
+
pre,
|
|
7915
|
+
install: setupCommand ? [`cd ${repoPath} && ${setupCommand}`] : [],
|
|
7916
|
+
post: [`git -C ${repoPath} config core.hooksPath ${repoPath}/.git/hooks`]
|
|
7917
|
+
};
|
|
7918
|
+
}
|
|
7919
|
+
function parseSetupMarker(text2) {
|
|
7920
|
+
const m = new RegExp(String.raw`${SETUP_MARKER} cold=([01]) sync_ms=(\d+) install_ms=(\d+)`).exec(text2);
|
|
7921
|
+
if (!m)
|
|
7922
|
+
return;
|
|
7923
|
+
return { cold: m[1] === "1", syncMs: Number(m[2]), installMs: Number(m[3]) };
|
|
7924
|
+
}
|
|
7925
|
+
function takeSetupMarker(worktreePath) {
|
|
7926
|
+
const file = join14(worktreePath, SETUP_MARKER_FILE);
|
|
7927
|
+
let text2;
|
|
7928
|
+
try {
|
|
7929
|
+
text2 = readFileSync9(file, "utf8");
|
|
7930
|
+
} catch {
|
|
7931
|
+
return;
|
|
7230
7932
|
}
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7933
|
+
rmSync3(file, { force: true });
|
|
7934
|
+
return parseSetupMarker(text2);
|
|
7935
|
+
}
|
|
7936
|
+
function fmtSeconds(ms) {
|
|
7937
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
7938
|
+
}
|
|
7939
|
+
function buildSlotStamp(imageName, packageManagerField) {
|
|
7940
|
+
const pm = packageManagerField?.split("@")[0] ?? "none";
|
|
7941
|
+
const major = packageManagerField?.split("@")[1]?.split(".")[0] ?? "any";
|
|
7942
|
+
return `${imageName} node=$(node --version 2>/dev/null) pm=${pm}@${major}`;
|
|
7235
7943
|
}
|
|
7236
|
-
function
|
|
7944
|
+
function buildSlotSetupCommand(slot, tempBranch, setupCommand, pm, stamp) {
|
|
7945
|
+
const repo = slotRepoPath(slot);
|
|
7946
|
+
const stampFile = slotStampPath(slot);
|
|
7947
|
+
const steps = buildRepoSetupSteps(repo, tempBranch, setupCommand, pm);
|
|
7948
|
+
return [
|
|
7949
|
+
`git config --global --add safe.directory '*'`,
|
|
7950
|
+
`RC_COLD=0`,
|
|
7951
|
+
`RC_STAMP="${stamp}"`,
|
|
7952
|
+
`RC_SYNC_START=$(date +%s%3N)`,
|
|
7953
|
+
`mkdir -p ${slotDirPath(slot)}`,
|
|
7954
|
+
`rm -f ${repo}/.git/*.lock`,
|
|
7955
|
+
`if ! git -C ${repo} rev-parse --git-dir >/dev/null 2>&1; then rm -rf ${repo} && git clone ${SANDBOX_WORKSPACE_PATH} ${repo} && RC_COLD=1; else git -C ${repo} fetch ${SANDBOX_WORKSPACE_PATH} ${tempBranch} && git -C ${repo} reset --hard FETCH_HEAD && git -C ${repo} checkout -B ${tempBranch}; fi`,
|
|
7956
|
+
`git -C ${repo} clean -fd`,
|
|
7957
|
+
`if [ "$(cat ${stampFile} 2>/dev/null)" != "$RC_STAMP" ]; then rm -rf ${repo}/node_modules && git -C ${repo} clean -fdX && RC_COLD=1 && printf '%s\\n' "$RC_STAMP" > ${stampFile}; fi`,
|
|
7958
|
+
...steps.pre,
|
|
7959
|
+
`RC_SYNC_MS=$(( $(date +%s%3N) - RC_SYNC_START ))`,
|
|
7960
|
+
`RC_INSTALL_START=$(date +%s%3N)`,
|
|
7961
|
+
...steps.install,
|
|
7962
|
+
`RC_INSTALL_MS=$(( $(date +%s%3N) - RC_INSTALL_START ))`,
|
|
7963
|
+
...steps.post,
|
|
7964
|
+
`printf '${SETUP_MARKER} cold=%s sync_ms=%s install_ms=%s\\n' "$RC_COLD" "$RC_SYNC_MS" "$RC_INSTALL_MS" > ${SANDBOX_WORKSPACE_PATH}/${SETUP_MARKER_FILE}`
|
|
7965
|
+
].join(" && ");
|
|
7966
|
+
}
|
|
7967
|
+
function buildWorkspaceNotes(mode, repoPath = ISOLATED_REPO_PATH) {
|
|
7237
7968
|
if (mode === "mounted") {
|
|
7238
7969
|
return [
|
|
7239
7970
|
"Work in the current directory \u2014 it is the repo checkout on your branch.",
|
|
@@ -7245,13 +7976,13 @@ function buildWorkspaceNotes(mode) {
|
|
|
7245
7976
|
`);
|
|
7246
7977
|
}
|
|
7247
7978
|
return [
|
|
7248
|
-
`Your working repository is \`${
|
|
7979
|
+
`Your working repository is \`${repoPath}\` \u2014 a clone on the container's fast native filesystem, with dependencies already installed. Do ALL work there: \`cd ${repoPath}\` first; every file you read, edit, test, and commit lives under it.`,
|
|
7249
7980
|
"",
|
|
7250
|
-
`The directory you start in (\`${SANDBOX_WORKSPACE_PATH}\`) is a slow mounted mirror used only to collect your commits \u2014 never edit files, install, or run tests there. Your commits sync back automatically (a post-commit hook pushes them); just commit as normal. If you re-run the dependency install and it reconfigures git hooks (husky), run \`git -C ${
|
|
7981
|
+
`The directory you start in (\`${SANDBOX_WORKSPACE_PATH}\`) is a slow mounted mirror used only to collect your commits \u2014 never edit files, install, or run tests there. Your commits sync back automatically (a post-commit hook pushes them); just commit as normal. If you re-run the dependency install and it reconfigures git hooks (husky), run \`git -C ${repoPath} config core.hooksPath ${repoPath}/.git/hooks\` afterwards so the sync hook stays armed.`,
|
|
7251
7982
|
"",
|
|
7252
|
-
`If you are blocked and write \`BLOCKED.md\`, write it at \`${
|
|
7983
|
+
`If you are blocked and write \`BLOCKED.md\`, write it at \`${repoPath}/BLOCKED.md\` AND copy it to \`${SANDBOX_WORKSPACE_PATH}/BLOCKED.md\` so the orchestrator can see it.`,
|
|
7253
7984
|
"",
|
|
7254
|
-
`Write \`DIGEST.md\` at \`${SANDBOX_WORKSPACE_PATH}/DIGEST.md\` \u2014 the mounted mirror, so the host can see it \u2014 and NOT inside \`${
|
|
7985
|
+
`Write \`DIGEST.md\` at \`${SANDBOX_WORKSPACE_PATH}/DIGEST.md\` \u2014 the mounted mirror, so the host can see it \u2014 and NOT inside \`${repoPath}\`, where it would be committed with your work.`,
|
|
7255
7986
|
"",
|
|
7256
7987
|
"**Those paths are the only authoritative ones** \u2014 nothing later in this prompt overrides them."
|
|
7257
7988
|
].join(`
|
|
@@ -7268,7 +7999,7 @@ function attachedNoteIds(context) {
|
|
|
7268
7999
|
return ids2;
|
|
7269
8000
|
}
|
|
7270
8001
|
function attachmentSources(context) {
|
|
7271
|
-
return attachedNoteIds(context).map(annotationPath).filter((p) =>
|
|
8002
|
+
return attachedNoteIds(context).map(annotationPath).filter((p) => existsSync16(p));
|
|
7272
8003
|
}
|
|
7273
8004
|
function buildAttachmentCopyCommands(sources, platform = process.platform) {
|
|
7274
8005
|
if (sources.length === 0)
|
|
@@ -7311,7 +8042,7 @@ function buildVerifyNotes(config2) {
|
|
|
7311
8042
|
}
|
|
7312
8043
|
function readRepoToolchain(repoPath) {
|
|
7313
8044
|
const pkgPath = join14(repoPath, "package.json");
|
|
7314
|
-
const hasPackageJson =
|
|
8045
|
+
const hasPackageJson = existsSync16(pkgPath);
|
|
7315
8046
|
let packageManagerField;
|
|
7316
8047
|
if (hasPackageJson) {
|
|
7317
8048
|
try {
|
|
@@ -7324,10 +8055,10 @@ function readRepoToolchain(repoPath) {
|
|
|
7324
8055
|
hasPackageJson,
|
|
7325
8056
|
...packageManagerField !== undefined ? { packageManagerField } : {},
|
|
7326
8057
|
lockfiles: {
|
|
7327
|
-
bun:
|
|
7328
|
-
pnpm:
|
|
7329
|
-
yarn:
|
|
7330
|
-
npm:
|
|
8058
|
+
bun: existsSync16(join14(repoPath, "bun.lock")) || existsSync16(join14(repoPath, "bun.lockb")),
|
|
8059
|
+
pnpm: existsSync16(join14(repoPath, "pnpm-lock.yaml")),
|
|
8060
|
+
yarn: existsSync16(join14(repoPath, "yarn.lock")),
|
|
8061
|
+
npm: existsSync16(join14(repoPath, "package-lock.json"))
|
|
7331
8062
|
}
|
|
7332
8063
|
};
|
|
7333
8064
|
}
|
|
@@ -7396,8 +8127,9 @@ function createToolTimer() {
|
|
|
7396
8127
|
if (event.type === "raw")
|
|
7397
8128
|
return;
|
|
7398
8129
|
const at = event.timestamp.getTime();
|
|
7399
|
-
if (iteration !== null && event.iteration !== iteration)
|
|
7400
|
-
pending =
|
|
8130
|
+
if (iteration !== null && event.iteration !== iteration && pending) {
|
|
8131
|
+
pending = { category: "setup", at: pending.at };
|
|
8132
|
+
}
|
|
7401
8133
|
iteration = event.iteration;
|
|
7402
8134
|
if (pending) {
|
|
7403
8135
|
const gap = at - pending.at;
|
|
@@ -7414,7 +8146,11 @@ function createToolTimer() {
|
|
|
7414
8146
|
pending = { category: "model", at };
|
|
7415
8147
|
}
|
|
7416
8148
|
};
|
|
7417
|
-
|
|
8149
|
+
const beginSetup = (at = Date.now()) => {
|
|
8150
|
+
pending = { category: "setup", at };
|
|
8151
|
+
iteration = null;
|
|
8152
|
+
};
|
|
8153
|
+
return { onEvent, beginSetup, summary: () => ({ totalMs, calls, byCategory }) };
|
|
7418
8154
|
}
|
|
7419
8155
|
function formatTimingSummary(s) {
|
|
7420
8156
|
if (s.totalMs === 0)
|
|
@@ -7422,6 +8158,22 @@ function formatTimingSummary(s) {
|
|
|
7422
8158
|
const parts = Object.entries(s.byCategory).sort((a, b) => (b[1]?.ms ?? 0) - (a[1]?.ms ?? 0)).slice(0, 5).map(([c, t]) => `${c} ${Math.round((t?.ms ?? 0) / s.totalMs * 100)}%`);
|
|
7423
8159
|
return `${Math.round(s.totalMs / 60000)}min across ${s.calls} tool call(s) \u2014 ${parts.join(", ")}`;
|
|
7424
8160
|
}
|
|
8161
|
+
function buildTicketTiming(summary, startedAt, endedAt) {
|
|
8162
|
+
return { ...summary, startedAt, endedAt, wallMs: Math.max(0, endedAt - startedAt) };
|
|
8163
|
+
}
|
|
8164
|
+
function formatTicketTiming(t) {
|
|
8165
|
+
const clock = fmtClock(Math.round(t.wallMs / 1000));
|
|
8166
|
+
const bare = t.calls === 0 && t.totalMs === 0;
|
|
8167
|
+
return bare ? clock : `${clock} (${formatTimingSummary(t)})`;
|
|
8168
|
+
}
|
|
8169
|
+
function emitTicketTiming(ctx, ticket, timing) {
|
|
8170
|
+
ctx.emitEvent({
|
|
8171
|
+
type: "ticket.timing",
|
|
8172
|
+
message: `ticket ${ticket.seq} spent ${formatTicketTiming(timing)}`,
|
|
8173
|
+
ticketId: ticket.id,
|
|
8174
|
+
data: timing
|
|
8175
|
+
});
|
|
8176
|
+
}
|
|
7425
8177
|
function interpretRunResult(result, blockedContent) {
|
|
7426
8178
|
const commits = result.commits.map((c) => c.sha);
|
|
7427
8179
|
if (commits.length > 0)
|
|
@@ -7442,10 +8194,20 @@ function isMergeConflictError(err) {
|
|
|
7442
8194
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
7443
8195
|
return /conflict/i.test(msg) || /git branch -D/i.test(msg) || /automatic merge failed/i.test(msg);
|
|
7444
8196
|
}
|
|
8197
|
+
function missingAgentBinaryMessage(err, runtime, image) {
|
|
8198
|
+
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
8199
|
+
const binary = AGENT_BINARY[runtime];
|
|
8200
|
+
const missing = new RegExp(`\\b${binary}\\b[^\\n]*(?:exited with code 127|command not found|not found|no such file or directory)`, "i");
|
|
8201
|
+
if (!missing.test(msg))
|
|
8202
|
+
return;
|
|
8203
|
+
return `${binary} is not installed in image ${image} \u2014 the image predates the burner Dockerfile. Rebuild it from Settings \u2192 AFK burns (Rebuild image).`;
|
|
8204
|
+
}
|
|
7445
8205
|
function classifyTicketRunError(err, runtime) {
|
|
7446
8206
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
7447
8207
|
const runtimes = runtime ? [runtime] : ["claude-code", "codex"];
|
|
7448
8208
|
const forRuntimes = (table) => runtimes.flatMap((r) => table[r]);
|
|
8209
|
+
if (runtimes.some((r) => missingAgentBinaryMessage(msg, r, "image")))
|
|
8210
|
+
return "fatal";
|
|
7449
8211
|
if ([...FATAL_ERROR_PATTERNS, ...forRuntimes(RUNTIME_FATAL_ERROR_PATTERNS)].some((p) => p.test(msg)))
|
|
7450
8212
|
return "fatal";
|
|
7451
8213
|
if ([...RETRYABLE_ERROR_PATTERNS, ...forRuntimes(RUNTIME_RETRYABLE_ERROR_PATTERNS)].some((p) => p.test(msg)))
|
|
@@ -7559,13 +8321,42 @@ function failedBlockerNote(seqs) {
|
|
|
7559
8321
|
}
|
|
7560
8322
|
async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
7561
8323
|
const width = Math.max(1, Math.floor(concurrency));
|
|
7562
|
-
const
|
|
7563
|
-
const
|
|
7564
|
-
const
|
|
8324
|
+
const scheduled = [...tickets2];
|
|
8325
|
+
const bySeq = indexBySeq(scheduled);
|
|
8326
|
+
const status = new Map(scheduled.map((t) => [t.seq, t.status]));
|
|
8327
|
+
const pending = new Set(scheduled.filter((t) => t.status === "pending").map((t) => t.seq));
|
|
7565
8328
|
const inFlight = new Map;
|
|
7566
8329
|
const digests = [];
|
|
8330
|
+
let admitted = 0;
|
|
7567
8331
|
const satisfied = (t, s) => s === "done" || s === "cancelled" || isReviewTicket(t) && s === "failed";
|
|
7568
|
-
const implementationsSettled = () =>
|
|
8332
|
+
const implementationsSettled = () => scheduled.every((t) => isReviewTicket(t) || !pending.has(t.seq) && !inFlight.has(t.seq));
|
|
8333
|
+
const admitNewTickets = () => {
|
|
8334
|
+
const added = [];
|
|
8335
|
+
for (const t of ctx.listTickets?.() ?? []) {
|
|
8336
|
+
if (bySeq.has(t.seq))
|
|
8337
|
+
continue;
|
|
8338
|
+
scheduled.push(t);
|
|
8339
|
+
bySeq.set(t.seq, t);
|
|
8340
|
+
status.set(t.seq, t.status);
|
|
8341
|
+
if (t.status !== "pending")
|
|
8342
|
+
continue;
|
|
8343
|
+
pending.add(t.seq);
|
|
8344
|
+
added.push(t.seq);
|
|
8345
|
+
}
|
|
8346
|
+
if (added.length === 0)
|
|
8347
|
+
return;
|
|
8348
|
+
admitted += added.length;
|
|
8349
|
+
ctx.emitEvent({
|
|
8350
|
+
type: "burn.admitted",
|
|
8351
|
+
message: `${added.length} ticket(s) minted during this run joined it: ${added.join(", ")}`,
|
|
8352
|
+
data: { seqs: added }
|
|
8353
|
+
});
|
|
8354
|
+
};
|
|
8355
|
+
const mirrorFinding = (t, progress, reason) => {
|
|
8356
|
+
if (!t.originFindingId)
|
|
8357
|
+
return;
|
|
8358
|
+
ctx.updateFinding?.(t.originFindingId, progress, reason);
|
|
8359
|
+
};
|
|
7569
8360
|
const readyState = (seq) => {
|
|
7570
8361
|
const t = bySeq.get(seq);
|
|
7571
8362
|
if (!t)
|
|
@@ -7581,7 +8372,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
7581
8372
|
return t.blockedBy.every((b) => satisfied(t, status.get(b))) ? "ready" : "wait";
|
|
7582
8373
|
};
|
|
7583
8374
|
const harvestDigest2 = (t, digest) => {
|
|
7584
|
-
const failed = isReviewTicket(t) ?
|
|
8375
|
+
const failed = isReviewTicket(t) ? scheduled.filter((x) => !isReviewTicket(x) && status.get(x.seq) === "failed").map((x) => x.seq) : [];
|
|
7585
8376
|
const body = [failed.length > 0 ? failedBlockerNote(failed) : undefined, digest].filter(Boolean).join(`
|
|
7586
8377
|
|
|
7587
8378
|
`);
|
|
@@ -7594,6 +8385,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
7594
8385
|
if (!t)
|
|
7595
8386
|
return;
|
|
7596
8387
|
ctx.updateTicket(t.id, { status: "failed", error, ...digest ? { digest } : {} });
|
|
8388
|
+
mirrorFinding(t, "failed", error);
|
|
7597
8389
|
harvestDigest2(t, digest);
|
|
7598
8390
|
if (extra)
|
|
7599
8391
|
ctx.emitEvent({ ...extra, ticketId: t.id });
|
|
@@ -7604,6 +8396,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
7604
8396
|
return;
|
|
7605
8397
|
status.set(seq, "burning");
|
|
7606
8398
|
ctx.updateTicket(t.id, { status: "burning" });
|
|
8399
|
+
mirrorFinding(t, "fixing");
|
|
7607
8400
|
ctx.emitEvent({
|
|
7608
8401
|
type: "ticket.burning",
|
|
7609
8402
|
message: `burning ticket ${t.seq}: ${t.title}`,
|
|
@@ -7613,6 +8406,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
7613
8406
|
if (outcome.status === "done") {
|
|
7614
8407
|
status.set(seq, "done");
|
|
7615
8408
|
ctx.updateTicket(t.id, { status: "done", commits: outcome.commits, digest: outcome.digest });
|
|
8409
|
+
mirrorFinding(t, "fixed");
|
|
7616
8410
|
ctx.emitEvent({
|
|
7617
8411
|
type: "ticket.done",
|
|
7618
8412
|
message: `ticket ${t.seq} done \u2014 ${outcome.commits.length} commit(s)`,
|
|
@@ -7636,6 +8430,8 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
7636
8430
|
data: { error: outcome.error }
|
|
7637
8431
|
});
|
|
7638
8432
|
}
|
|
8433
|
+
if (isReviewTicket(t))
|
|
8434
|
+
admitNewTickets();
|
|
7639
8435
|
};
|
|
7640
8436
|
while (pending.size > 0 || inFlight.size > 0) {
|
|
7641
8437
|
ctx.signal.throwIfAborted();
|
|
@@ -7692,7 +8488,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
7692
8488
|
for (const s of status.values())
|
|
7693
8489
|
if (s === "done")
|
|
7694
8490
|
done += 1;
|
|
7695
|
-
return { done, digests };
|
|
8491
|
+
return { done, admitted, digests };
|
|
7696
8492
|
}
|
|
7697
8493
|
function composeRunDigest(entries) {
|
|
7698
8494
|
if (entries.length === 0)
|
|
@@ -7703,15 +8499,44 @@ ${e.digest.trim()}`).join(`
|
|
|
7703
8499
|
|
|
7704
8500
|
`);
|
|
7705
8501
|
}
|
|
8502
|
+
function gateTicketAuth(deps) {
|
|
8503
|
+
const authMissing = deps.ticketAuthMissing;
|
|
8504
|
+
if (!authMissing || deps.config.sandbox === "noSandbox")
|
|
8505
|
+
return deps.executeTicketRun;
|
|
8506
|
+
return async (ctx, ticket, run3) => {
|
|
8507
|
+
const runtime = authMissing(ticket);
|
|
8508
|
+
if (!runtime)
|
|
8509
|
+
return deps.executeTicketRun(ctx, ticket, run3);
|
|
8510
|
+
const hint = `${runtime} is not authenticated \u2014 ${RUNTIME_AUTH_SETUP_HINT[runtime]}`;
|
|
8511
|
+
ctx.emitEvent({ type: AUTH_MISSING_EVENT, message: hint, ticketId: ticket.id });
|
|
8512
|
+
return { status: "failed", error: hint };
|
|
8513
|
+
};
|
|
8514
|
+
}
|
|
7706
8515
|
async function burnRun(ctx, deps) {
|
|
7707
8516
|
const tickets2 = ctx.tickets;
|
|
7708
8517
|
const burnable = tickets2.filter((t) => t.status !== "cancelled");
|
|
7709
8518
|
const cancelled = tickets2.length - burnable.length;
|
|
7710
|
-
const total = burnable.length;
|
|
7711
8519
|
if (deps.config.sandbox !== "noSandbox" && !deps.hasAuthToken) {
|
|
7712
8520
|
ctx.emitEvent({ type: AUTH_MISSING_EVENT, message: RUNTIME_AUTH_SETUP_HINT[deps.runtime] });
|
|
7713
8521
|
return { status: "failed", summary: "burn aborted: auth token missing" };
|
|
7714
8522
|
}
|
|
8523
|
+
if (deps.config.sandbox !== "noSandbox" && deps.exec) {
|
|
8524
|
+
const image = resolveSandboxImage(deps.config);
|
|
8525
|
+
const binary = RUNTIME_BINARY[deps.runtime];
|
|
8526
|
+
const probe = await deps.exec(deps.config.sandbox, [
|
|
8527
|
+
"run",
|
|
8528
|
+
"--rm",
|
|
8529
|
+
"--entrypoint",
|
|
8530
|
+
binary,
|
|
8531
|
+
image,
|
|
8532
|
+
"--version"
|
|
8533
|
+
]);
|
|
8534
|
+
if (!probe.ok || probe.code !== 0) {
|
|
8535
|
+
const message = missingImageRuntimeMessage(deps.runtime, image);
|
|
8536
|
+
ctx.emitEvent({ type: IMAGE_RUNTIME_MISSING_EVENT, message });
|
|
8537
|
+
return { status: "failed", summary: message };
|
|
8538
|
+
}
|
|
8539
|
+
}
|
|
7715
8540
|
const cycle = detectCycle(burnable);
|
|
7716
8541
|
if (cycle) {
|
|
7717
8542
|
const path = cycle.join(" \u2192 ");
|
|
@@ -7722,7 +8547,8 @@ async function burnRun(ctx, deps) {
|
|
|
7722
8547
|
});
|
|
7723
8548
|
return { status: "failed", summary: `dependency cycle: ${path}` };
|
|
7724
8549
|
}
|
|
7725
|
-
const { done, digests } = await burnTickets(ctx, tickets2, deps
|
|
8550
|
+
const { done, admitted, digests } = await burnTickets(ctx, tickets2, gateTicketAuth(deps), deps.concurrency);
|
|
8551
|
+
const total = burnable.length + admitted;
|
|
7726
8552
|
const summary = cancelled > 0 ? `${done}/${total} tickets done (${cancelled} cancelled)` : `${done}/${total} tickets done`;
|
|
7727
8553
|
ctx.emitEvent({ type: "burn.summary", message: summary, data: { done, total, cancelled } });
|
|
7728
8554
|
const digest = composeRunDigest(digests);
|
|
@@ -7735,7 +8561,7 @@ function resolverTemplatePath() {
|
|
|
7735
8561
|
return burnerAssetPath("resolve-conflict.md");
|
|
7736
8562
|
}
|
|
7737
8563
|
function burnerAssetPath(file) {
|
|
7738
|
-
const here = dirname6(
|
|
8564
|
+
const here = dirname6(fileURLToPath6(import.meta.url));
|
|
7739
8565
|
return join14(resolveSkillsRoot(here), "burner", file);
|
|
7740
8566
|
}
|
|
7741
8567
|
function listFeatureDocs(docsDir, prefix = "") {
|
|
@@ -7759,11 +8585,11 @@ function readDocsDigest(projectId, slug) {
|
|
|
7759
8585
|
...missing ? { missing } : {}
|
|
7760
8586
|
});
|
|
7761
8587
|
const worktree = worktreeDir(projectId, slug);
|
|
7762
|
-
if (!
|
|
8588
|
+
if (!existsSync16(worktree)) {
|
|
7763
8589
|
return done("_No talk worktree on disk \u2014 docs digest skipped._", "no-worktree");
|
|
7764
8590
|
}
|
|
7765
8591
|
const docsDir = join14(worktree, ...docsRel.split("/"));
|
|
7766
|
-
if (!
|
|
8592
|
+
if (!existsSync16(docsDir)) {
|
|
7767
8593
|
return done("_No docs/features dir in the talk worktree \u2014 docs digest skipped._", "no-docs-dir");
|
|
7768
8594
|
}
|
|
7769
8595
|
const names = listFeatureDocs(docsDir).sort((a, b) => agentDigestDocOrder(a) - agentDigestDocOrder(b) || a.localeCompare(b));
|
|
@@ -7826,7 +8652,7 @@ function readAgentFile(dirs, file) {
|
|
|
7826
8652
|
if (!dir)
|
|
7827
8653
|
continue;
|
|
7828
8654
|
const p = join14(dir, file);
|
|
7829
|
-
if (
|
|
8655
|
+
if (existsSync16(p)) {
|
|
7830
8656
|
try {
|
|
7831
8657
|
return readFileSync9(p, "utf8");
|
|
7832
8658
|
} catch {}
|
|
@@ -7896,8 +8722,8 @@ function buildAgentExtraArgs(runtime, options) {
|
|
|
7896
8722
|
function quoteArg(value) {
|
|
7897
8723
|
return `"${value}"`;
|
|
7898
8724
|
}
|
|
7899
|
-
function selectSandbox(config2, mounts = []) {
|
|
7900
|
-
const imageOpts = buildSandboxOptions(config2, mounts);
|
|
8725
|
+
function selectSandbox(config2, mounts = [], env = {}) {
|
|
8726
|
+
const imageOpts = buildSandboxOptions(config2, mounts, env);
|
|
7901
8727
|
switch (config2.sandbox) {
|
|
7902
8728
|
case "docker":
|
|
7903
8729
|
return docker(imageOpts);
|
|
@@ -7909,17 +8735,46 @@ function selectSandbox(config2, mounts = []) {
|
|
|
7909
8735
|
throw new Error(`no sandbox provider for sandbox "${config2.sandbox}" \u2014 refusing to run the agent unsandboxed`);
|
|
7910
8736
|
}
|
|
7911
8737
|
}
|
|
7912
|
-
function buildSandboxOptions(config2, mounts = []) {
|
|
8738
|
+
function buildSandboxOptions(config2, mounts = [], env = {}) {
|
|
7913
8739
|
return {
|
|
7914
8740
|
imageName: resolveSandboxImage(config2),
|
|
7915
8741
|
...mounts.length > 0 ? { mounts } : {},
|
|
7916
|
-
...config2.burnCpus !== undefined ? { cpus: config2.burnCpus } : {}
|
|
8742
|
+
...config2.burnCpus !== undefined ? { cpus: config2.burnCpus } : {},
|
|
8743
|
+
...Object.keys(env).length > 0 ? { env } : {}
|
|
7917
8744
|
};
|
|
7918
8745
|
}
|
|
7919
|
-
async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget, blocks, runCtx) {
|
|
8746
|
+
async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget, ensureCacheVolume, blocks, runCtx) {
|
|
8747
|
+
const allocator = resolveBurnCacheMode(config2) === "volume" ? getBurnSlotAllocator(config2.burnConcurrency) : undefined;
|
|
8748
|
+
return withBurnCacheSlot(allocator, async (slot) => {
|
|
8749
|
+
if (slot !== undefined)
|
|
8750
|
+
await ensureCacheVolume();
|
|
8751
|
+
return burnTicket(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget, blocks, runCtx, slot);
|
|
8752
|
+
});
|
|
8753
|
+
}
|
|
8754
|
+
async function withBurnCacheSlot(allocator, body) {
|
|
8755
|
+
if (!allocator)
|
|
8756
|
+
return body(undefined);
|
|
8757
|
+
let slot;
|
|
8758
|
+
try {
|
|
8759
|
+
slot = allocator.claim();
|
|
8760
|
+
} catch (err) {
|
|
8761
|
+
if (!(err instanceof BurnSlotsExhaustedError))
|
|
8762
|
+
throw err;
|
|
8763
|
+
}
|
|
8764
|
+
if (slot === undefined)
|
|
8765
|
+
return body(undefined);
|
|
8766
|
+
try {
|
|
8767
|
+
return await body(slot);
|
|
8768
|
+
} finally {
|
|
8769
|
+
allocator.release(slot);
|
|
8770
|
+
}
|
|
8771
|
+
}
|
|
8772
|
+
async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget, blocks, runCtx, slot) {
|
|
7920
8773
|
const { project, feature } = ctx;
|
|
7921
|
-
const
|
|
7922
|
-
|
|
8774
|
+
const startedAt = Date.now();
|
|
8775
|
+
const workspaceMode = resolveBurnWorkspaceMode(slot === undefined ? { ...config2, burnCache: "off" } : config2);
|
|
8776
|
+
const agentRepoPath = slot !== undefined ? slotRepoPath(slot) : ISOLATED_REPO_PATH;
|
|
8777
|
+
if (workspaceMode !== "mounted")
|
|
7923
8778
|
await ensureIsolatedPushTarget();
|
|
7924
8779
|
const attachments = attachmentSources(ticket.context);
|
|
7925
8780
|
const attachmentCommands = buildAttachmentCopyCommands(attachments);
|
|
@@ -7935,7 +8790,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
7935
8790
|
const ticketJson = buildTicketJson(ticket);
|
|
7936
8791
|
const featureBrief = buildFeatureBrief(feature);
|
|
7937
8792
|
const docsDigest = blocks.docsDigest;
|
|
7938
|
-
const workspaceNotes = buildWorkspaceNotes(workspaceMode);
|
|
8793
|
+
const workspaceNotes = buildWorkspaceNotes(workspaceMode, agentRepoPath);
|
|
7939
8794
|
const guardInstalled = config2.burnGuard && config2.sandbox !== "noSandbox";
|
|
7940
8795
|
const prepared = resolvePreparedSettings(config2, project);
|
|
7941
8796
|
const verifyNotes = buildVerifyNotes(prepared);
|
|
@@ -7948,30 +8803,56 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
7948
8803
|
DRIVE_NOTES: blocks.driveNotes,
|
|
7949
8804
|
GUARD_NOTES: buildGuardNotes(guardInstalled),
|
|
7950
8805
|
TICKET_JSON: ticketJson,
|
|
7951
|
-
BLOCKERS: buildBlockersBlock(ticket.blockedBy, runCtx.digests)
|
|
8806
|
+
BLOCKERS: buildBlockersBlock(ticket.blockedBy, runCtx.digests),
|
|
8807
|
+
FIX_NOTES: buildFixNotes(ticket)
|
|
7952
8808
|
});
|
|
7953
8809
|
const toolchain = readRepoToolchain(project.repoPath);
|
|
7954
8810
|
const pm = detectPackageManager(toolchain);
|
|
7955
8811
|
const setupCommand = resolveSetupCommand(toolchain, prepared.setupCommand);
|
|
7956
|
-
const
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
8812
|
+
const cache = buildBurnCacheMounts(slot, project.id, config2.sandbox, pm);
|
|
8813
|
+
const sandboxEnv = cache.env;
|
|
8814
|
+
const mounts = [...cache.mounts];
|
|
8815
|
+
for (const mount of cache.mounts) {
|
|
8816
|
+
if ("hostPath" in mount)
|
|
7960
8817
|
mkdirSync9(mount.hostPath, { recursive: true });
|
|
7961
|
-
mounts.push(mount);
|
|
7962
|
-
}
|
|
7963
8818
|
}
|
|
8819
|
+
const codexAuthMount = codexAuthMountFor(model.runtime, config2.sandbox);
|
|
8820
|
+
if (codexAuthMount)
|
|
8821
|
+
mounts.push(codexAuthMount);
|
|
7964
8822
|
const guardInstall = guardInstalled ? buildGuardInstallCommand(model.runtime) : undefined;
|
|
7965
|
-
const
|
|
8823
|
+
const withPrelude = (setup) => chainSetupCommands(codexAuthMount ? buildCodexAuthCopyCommand() : undefined, guardInstall, setup);
|
|
7966
8824
|
const agentOptions = guardInstalled ? { bypassHookTrust: true } : {};
|
|
8825
|
+
const slotStamp = buildSlotStamp(resolveSandboxImage(config2), toolchain.packageManagerField);
|
|
8826
|
+
const setupHookFor = (branch) => withPrelude(slot !== undefined ? buildSlotSetupCommand(slot, branch, setupCommand, pm, slotStamp) : workspaceMode === "isolated" ? buildIsolatedSetupCommand(branch, setupCommand, pm) : setupCommand);
|
|
7967
8827
|
mkdirSync9(logsDir(), { recursive: true });
|
|
7968
8828
|
const logFilePath = join14(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
|
|
7969
8829
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
7970
8830
|
beginTranscript(ticket.id);
|
|
7971
8831
|
const timer = createToolTimer();
|
|
8832
|
+
const setupRun = { worktree: undefined, iteration: null };
|
|
8833
|
+
const consumeSetupMarker = (iteration) => {
|
|
8834
|
+
if (slot === undefined || !setupRun.worktree || setupRun.iteration === iteration)
|
|
8835
|
+
return;
|
|
8836
|
+
setupRun.iteration = iteration;
|
|
8837
|
+
const marker = takeSetupMarker(setupRun.worktree);
|
|
8838
|
+
if (!marker)
|
|
8839
|
+
return;
|
|
8840
|
+
ctx.emitEvent({
|
|
8841
|
+
type: "burn.setup",
|
|
8842
|
+
message: `ticket ${ticket.seq}: cache slot ${slot} ${marker.cold ? "cold" : "warm"} \u2014 synced in ${fmtSeconds(marker.syncMs)}, installed in ${fmtSeconds(marker.installMs)}`,
|
|
8843
|
+
ticketId: ticket.id,
|
|
8844
|
+
data: { slot, cold: marker.cold, syncMs: marker.syncMs, installMs: marker.installMs }
|
|
8845
|
+
});
|
|
8846
|
+
};
|
|
8847
|
+
const beginSetupSpan = (branch) => {
|
|
8848
|
+
setupRun.worktree = burnWorktreePath(project.repoPath, branch);
|
|
8849
|
+
setupRun.iteration = null;
|
|
8850
|
+
timer.beginSetup();
|
|
8851
|
+
};
|
|
7972
8852
|
const onStreamEvent = (event) => {
|
|
7973
8853
|
throttle.onEvent(event);
|
|
7974
8854
|
timer.onEvent(event);
|
|
8855
|
+
consumeSetupMarker(event.iteration);
|
|
7975
8856
|
if (event.type === "text") {
|
|
7976
8857
|
appendTranscript(ticket.id, { kind: "text", text: event.message });
|
|
7977
8858
|
} else if (event.type === "toolCall") {
|
|
@@ -8007,6 +8888,12 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8007
8888
|
const preserveChain = (branch) => {
|
|
8008
8889
|
ctx.updateTicket(ticket.id, { attemptBranch: branch });
|
|
8009
8890
|
};
|
|
8891
|
+
const discards = new Map;
|
|
8892
|
+
const discardWorktree = (branch) => {
|
|
8893
|
+
const pending = discards.get(branch) ?? cleanupBurnWorktree(project.repoPath, branch);
|
|
8894
|
+
discards.set(branch, pending);
|
|
8895
|
+
return pending;
|
|
8896
|
+
};
|
|
8010
8897
|
const runResolver = async (input) => {
|
|
8011
8898
|
const resolveBranch = ticketBranchName(feature.slug, ticket.seq, newId("r").slice(2, 10));
|
|
8012
8899
|
const otherSide = await commitSummaries(project.repoPath, input.branch, feature.branch);
|
|
@@ -8022,7 +8909,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8022
8909
|
OTHER_SIDE: buildOtherSideBlock(otherSide),
|
|
8023
8910
|
MERGE_COMMAND: resolveMergeCommand(workspaceMode, feature.branch)
|
|
8024
8911
|
});
|
|
8025
|
-
const hookCommand =
|
|
8912
|
+
const hookCommand = setupHookFor(resolveBranch);
|
|
8026
8913
|
appendTranscript(ticket.id, {
|
|
8027
8914
|
kind: "text",
|
|
8028
8915
|
text: `
|
|
@@ -8035,9 +8922,10 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8035
8922
|
};
|
|
8036
8923
|
let result;
|
|
8037
8924
|
try {
|
|
8925
|
+
beginSetupSpan(resolveBranch);
|
|
8038
8926
|
result = await run2({
|
|
8039
8927
|
agent: buildBurnAgent(config2, token, model, agentOptions),
|
|
8040
|
-
sandbox: selectSandbox(config2, mounts),
|
|
8928
|
+
sandbox: selectSandbox(config2, mounts, sandboxEnv),
|
|
8041
8929
|
cwd: project.repoPath,
|
|
8042
8930
|
prompt,
|
|
8043
8931
|
branchStrategy: { type: "branch", branch: resolveBranch, baseBranch: input.branch },
|
|
@@ -8054,6 +8942,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8054
8942
|
logging: { type: "file", path: logFilePath, onAgentStreamEvent: onStreamEvent }
|
|
8055
8943
|
});
|
|
8056
8944
|
} catch (err) {
|
|
8945
|
+
await discardWorktree(resolveBranch);
|
|
8057
8946
|
if (ctx.signal.aborted)
|
|
8058
8947
|
throw err;
|
|
8059
8948
|
const tip = await bestTip();
|
|
@@ -8065,9 +8954,9 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
8065
8954
|
error: ticketAbort.signal.aborted ? "stopped by user during conflict resolution" : errorHeadline(err instanceof Error ? err.message : String(err))
|
|
8066
8955
|
};
|
|
8067
8956
|
}
|
|
8068
|
-
await cleanupBurnWorktree(project.repoPath, resolveBranch);
|
|
8069
8957
|
}
|
|
8070
8958
|
const blocked = readAgentFile([result?.preservedWorktreePath, project.repoPath], "BLOCKED.md");
|
|
8959
|
+
await discardWorktree(resolveBranch);
|
|
8071
8960
|
if (blocked !== undefined && blocked.trim().length > 0) {
|
|
8072
8961
|
return {
|
|
8073
8962
|
ok: false,
|
|
@@ -8157,17 +9046,18 @@ ${blocked.trim()}`
|
|
|
8157
9046
|
let tempBranch = "";
|
|
8158
9047
|
for (let attempt = 1;; attempt++) {
|
|
8159
9048
|
tempBranch = ticketBranchName(feature.slug, ticket.seq, newId("b").slice(2, 10));
|
|
8160
|
-
const hookCommand =
|
|
9049
|
+
const hookCommand = setupHookFor(tempBranch);
|
|
8161
9050
|
if (hookCommand && attempt === 1) {
|
|
8162
9051
|
ctx.emitEvent({
|
|
8163
9052
|
type: "burn.setup",
|
|
8164
|
-
message: workspaceMode === "isolated" ? `preparing isolated workspace (native-FS clone)${setupCommand ? ` + deps install: ${setupCommand}` : ""}` : `installing deps before agent start: ${setupCommand}`,
|
|
8165
|
-
ticketId: ticket.id
|
|
9053
|
+
message: slot !== undefined ? `preparing cache slot ${slot}${setupCommand ? ` + deps install: ${setupCommand}` : ""}` : workspaceMode === "isolated" ? `preparing isolated workspace (native-FS clone)${setupCommand ? ` + deps install: ${setupCommand}` : ""}` : `installing deps before agent start: ${setupCommand}`,
|
|
9054
|
+
ticketId: ticket.id,
|
|
9055
|
+
...slot !== undefined ? { data: { slot } } : {}
|
|
8166
9056
|
});
|
|
8167
9057
|
}
|
|
8168
9058
|
const runOptions = {
|
|
8169
9059
|
agent: buildBurnAgent(config2, token, model, agentOptions),
|
|
8170
|
-
sandbox: selectSandbox(config2, mounts),
|
|
9060
|
+
sandbox: selectSandbox(config2, mounts, sandboxEnv),
|
|
8171
9061
|
cwd: project.repoPath,
|
|
8172
9062
|
prompt: retryNotes ? `${basePrompt}
|
|
8173
9063
|
|
|
@@ -8192,14 +9082,17 @@ ${retryNotes}` : basePrompt,
|
|
|
8192
9082
|
};
|
|
8193
9083
|
try {
|
|
8194
9084
|
await excludeAttachments();
|
|
9085
|
+
beginSetupSpan(tempBranch);
|
|
8195
9086
|
result = await run2(runOptions);
|
|
8196
9087
|
await clearAttachmentsFor(tempBranch);
|
|
8197
9088
|
break;
|
|
8198
9089
|
} catch (err) {
|
|
8199
9090
|
await clearAttachmentsFor(tempBranch);
|
|
9091
|
+
await discardWorktree(tempBranch);
|
|
8200
9092
|
if (ctx.signal.aborted)
|
|
8201
9093
|
throw err;
|
|
8202
9094
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9095
|
+
const missingBinary = missingAgentBinaryMessage(err, model.runtime, resolveSandboxImage(config2));
|
|
8203
9096
|
const salvaged = await branchCommitsAhead(project.repoPath, feature.branch, tempBranch);
|
|
8204
9097
|
if (salvaged.length > 0)
|
|
8205
9098
|
baseBranch = tempBranch;
|
|
@@ -8216,7 +9109,7 @@ ${retryNotes}` : basePrompt,
|
|
|
8216
9109
|
};
|
|
8217
9110
|
}
|
|
8218
9111
|
if (isWorktreeTeardownError(err)) {
|
|
8219
|
-
const removed = await
|
|
9112
|
+
const removed = await discardWorktree(tempBranch);
|
|
8220
9113
|
if (salvaged.length > 0) {
|
|
8221
9114
|
ctx.emitEvent({
|
|
8222
9115
|
type: "burn.worktree.teardown-failed",
|
|
@@ -8241,6 +9134,11 @@ ${retryNotes}` : basePrompt,
|
|
|
8241
9134
|
}
|
|
8242
9135
|
};
|
|
8243
9136
|
}
|
|
9137
|
+
if (missingBinary) {
|
|
9138
|
+
if (salvaged.length > 0)
|
|
9139
|
+
preserveChain(tempBranch);
|
|
9140
|
+
return { status: "failed", error: missingBinary };
|
|
9141
|
+
}
|
|
8244
9142
|
if (classifyTicketRunError(err, model.runtime) === "retryable" && attempt < maxAttempts) {
|
|
8245
9143
|
const headline = errorHeadline(msg);
|
|
8246
9144
|
retryNotes = buildRetryNotes({ error: headline, commitCount: salvaged.length });
|
|
@@ -8282,6 +9180,7 @@ ${retryNotes}` : basePrompt,
|
|
|
8282
9180
|
const agentFileDirs = [result.preservedWorktreePath, project.repoPath];
|
|
8283
9181
|
const blocked = readAgentFile(agentFileDirs, "BLOCKED.md");
|
|
8284
9182
|
harvestedDigest = harvestDigest(agentFileDirs);
|
|
9183
|
+
await discardWorktree(tempBranch);
|
|
8285
9184
|
const chain = await branchCommitsAhead(project.repoPath, feature.branch, tempBranch);
|
|
8286
9185
|
let outcome;
|
|
8287
9186
|
if (result.commits.length === 0 && blocked !== undefined && blocked.trim().length > 0) {
|
|
@@ -8302,27 +9201,43 @@ ${blocked.trim()}` };
|
|
|
8302
9201
|
releaseTicketAbort(ticket.id);
|
|
8303
9202
|
throttle.flush();
|
|
8304
9203
|
endTranscript(ticket.id);
|
|
8305
|
-
|
|
8306
|
-
if (timing.calls > 0) {
|
|
8307
|
-
ctx.emitEvent({
|
|
8308
|
-
type: "ticket.timing",
|
|
8309
|
-
message: `ticket ${ticket.seq} spent ${formatTimingSummary(timing)}`,
|
|
8310
|
-
ticketId: ticket.id,
|
|
8311
|
-
data: timing
|
|
8312
|
-
});
|
|
8313
|
-
}
|
|
9204
|
+
emitTicketTiming(ctx, ticket, buildTicketTiming(timer.summary(), startedAt, Date.now()));
|
|
8314
9205
|
}
|
|
8315
9206
|
}
|
|
8316
9207
|
function resolveTicketModel(config2, project, runOverride, ticket) {
|
|
8317
|
-
|
|
9208
|
+
const step = isReviewTicket(ticket) ? "review" : "implement";
|
|
9209
|
+
return resolveModelEntry(step, config2, project, ticket.model ?? runOverride);
|
|
8318
9210
|
}
|
|
8319
9211
|
function resolveBurnDeps(ctx) {
|
|
8320
9212
|
const config2 = loadConfig();
|
|
8321
9213
|
const model = resolveModelEntry("implement", config2, ctx.project, ctx.modelOverride);
|
|
8322
9214
|
const token = readTokenFromEnvFile(envPath(), model.runtime);
|
|
9215
|
+
const exec = createSystemExec({ cwd: ctx.project.repoPath });
|
|
9216
|
+
const imageProbeCache = new Map;
|
|
9217
|
+
const cachedExec = (command, args) => {
|
|
9218
|
+
const key = `${resolveSandboxImage(config2)}\x00${model.runtime}`;
|
|
9219
|
+
let result = imageProbeCache.get(key);
|
|
9220
|
+
if (!result) {
|
|
9221
|
+
result = exec(command, args);
|
|
9222
|
+
imageProbeCache.set(key, result);
|
|
9223
|
+
}
|
|
9224
|
+
return result;
|
|
9225
|
+
};
|
|
8323
9226
|
const land = createSerialQueue();
|
|
8324
9227
|
let pushTargetReady;
|
|
8325
9228
|
const ensureIsolatedPushTarget = () => pushTargetReady ??= allowPushToCheckedOutBranches(ctx.project.repoPath);
|
|
9229
|
+
let cacheVolumeReady;
|
|
9230
|
+
const ensureCacheVolume = () => {
|
|
9231
|
+
if (resolveBurnCacheMode(config2) !== "volume")
|
|
9232
|
+
return Promise.resolve();
|
|
9233
|
+
const engine = config2.sandbox === "podman" ? "podman" : "docker";
|
|
9234
|
+
return cacheVolumeReady ??= ensureBurnCacheVolume({
|
|
9235
|
+
engine,
|
|
9236
|
+
imageName: resolveSandboxImage(config2),
|
|
9237
|
+
projectId: ctx.project.id,
|
|
9238
|
+
exec
|
|
9239
|
+
});
|
|
9240
|
+
};
|
|
8326
9241
|
const docs2 = readDocsDigest(ctx.project.id, ctx.feature.slug);
|
|
8327
9242
|
emitDocsDigestEvent(ctx, docs2);
|
|
8328
9243
|
const blocks = {
|
|
@@ -8336,28 +9251,39 @@ function resolveBurnDeps(ctx) {
|
|
|
8336
9251
|
dbResetCommand: ctx.project.dbResetCommand
|
|
8337
9252
|
})
|
|
8338
9253
|
};
|
|
9254
|
+
const ticketCredentials = (ticket) => {
|
|
9255
|
+
const ticketModel = resolveTicketModel(config2, ctx.project, ctx.modelOverride, ticket);
|
|
9256
|
+
return {
|
|
9257
|
+
model: ticketModel,
|
|
9258
|
+
token: ticketModel.runtime === model.runtime ? token : readTokenFromEnvFile(envPath(), ticketModel.runtime)
|
|
9259
|
+
};
|
|
9260
|
+
};
|
|
8339
9261
|
return {
|
|
8340
9262
|
config: config2,
|
|
8341
9263
|
runtime: model.runtime,
|
|
8342
|
-
hasAuthToken: token
|
|
9264
|
+
hasAuthToken: burnAuthReady(model.runtime, token),
|
|
9265
|
+
exec: cachedExec,
|
|
9266
|
+
ticketAuthMissing: (ticket) => {
|
|
9267
|
+
const { model: ticketModel, token: ticketToken } = ticketCredentials(ticket);
|
|
9268
|
+
return burnAuthReady(ticketModel.runtime, ticketToken) ? undefined : ticketModel.runtime;
|
|
9269
|
+
},
|
|
8343
9270
|
concurrency: config2.burnConcurrency,
|
|
8344
9271
|
executeTicketRun: (c, ticket, run3) => {
|
|
8345
|
-
const
|
|
8346
|
-
const ticketToken = ticketModel.runtime === model.runtime ? token : readTokenFromEnvFile(envPath(), ticketModel.runtime);
|
|
9272
|
+
const { model: ticketModel, token: ticketToken } = ticketCredentials(ticket);
|
|
8347
9273
|
return isReviewTicket(ticket) ? executeReviewTicket(c, ticket, {
|
|
8348
9274
|
config: config2,
|
|
8349
9275
|
token: ticketToken,
|
|
8350
9276
|
model: ticketModel,
|
|
8351
9277
|
docsDigest: docs2.text,
|
|
8352
9278
|
lapDigests: run3.digests
|
|
8353
|
-
}) : realExecuteTicketRun(c, ticket, config2, ticketToken, ticketModel, land, ensureIsolatedPushTarget, blocks, run3);
|
|
9279
|
+
}) : realExecuteTicketRun(c, ticket, config2, ticketToken, ticketModel, land, ensureIsolatedPushTarget, ensureCacheVolume, blocks, run3);
|
|
8354
9280
|
}
|
|
8355
9281
|
};
|
|
8356
9282
|
}
|
|
8357
9283
|
function readTokenFromEnvFile(path, runtime) {
|
|
8358
9284
|
const key = RUNTIME_AUTH_KEY[runtime];
|
|
8359
9285
|
let fromFile;
|
|
8360
|
-
if (
|
|
9286
|
+
if (existsSync16(path)) {
|
|
8361
9287
|
try {
|
|
8362
9288
|
fromFile = parseEnvFile2(readFileSync9(path, "utf8"))[key];
|
|
8363
9289
|
} catch {
|
|
@@ -8367,18 +9293,25 @@ function readTokenFromEnvFile(path, runtime) {
|
|
|
8367
9293
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env[key];
|
|
8368
9294
|
return token && token.length > 0 ? token : undefined;
|
|
8369
9295
|
}
|
|
8370
|
-
var AUTH_MISSING_EVENT = "auth.missing", DROPPED_MAP_SECTIONS, AGENT_CONVENTIONS_FILE = "CLAUDE.md", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RUNTIME_FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, RUNTIME_RETRYABLE_ERROR_PATTERNS, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
9296
|
+
var AUTH_MISSING_EVENT = "auth.missing", IMAGE_RUNTIME_MISSING_EVENT = "burn.image_runtime_missing", RUNTIME_BINARY, DROPPED_MAP_SECTIONS, AGENT_CONVENTIONS_FILE = "CLAUDE.md", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, CODEX_HOST_MOUNT_PATH = "/mnt/host-codex", SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", SETUP_MARKER_FILE = ".runcastle-setup", SETUP_MARKER = "RUNCASTLE_SETUP", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RUNTIME_FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, RUNTIME_RETRYABLE_ERROR_PATTERNS, AGENT_BINARY, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
8371
9297
|
var init_ticket_burner = __esm(() => {
|
|
8372
9298
|
init_src();
|
|
8373
9299
|
init_config_load();
|
|
8374
9300
|
init_paths();
|
|
8375
9301
|
init_skills_root();
|
|
9302
|
+
init_codex_auth();
|
|
9303
|
+
init_system_exec();
|
|
8376
9304
|
init_knowledge();
|
|
8377
9305
|
init_setup();
|
|
8378
9306
|
init_agent_stream();
|
|
8379
9307
|
init_git();
|
|
8380
9308
|
init_burn_guard();
|
|
9309
|
+
init_burn_cache();
|
|
8381
9310
|
init_review_ticket();
|
|
9311
|
+
RUNTIME_BINARY = {
|
|
9312
|
+
"claude-code": "claude",
|
|
9313
|
+
codex: "codex"
|
|
9314
|
+
};
|
|
8382
9315
|
DROPPED_MAP_SECTIONS = ["Not yet specified", "Out of scope"];
|
|
8383
9316
|
PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
8384
9317
|
PM_CACHE_SANDBOX_PATHS = {
|
|
@@ -8431,7 +9364,10 @@ var init_ticket_burner = __esm(() => {
|
|
|
8431
9364
|
/\b401\b|\b403\b/,
|
|
8432
9365
|
/insufficient_quota|exceeded your current quota/i,
|
|
8433
9366
|
/model_not_found|does not exist or you do not have access/i,
|
|
8434
|
-
/CODEX_API_KEY
|
|
9367
|
+
/CODEX_API_KEY/,
|
|
9368
|
+
/not logged in/i,
|
|
9369
|
+
/\bauth (failed|required)\b/i,
|
|
9370
|
+
/refresh token/i
|
|
8435
9371
|
]
|
|
8436
9372
|
};
|
|
8437
9373
|
RETRYABLE_ERROR_PATTERNS = [
|
|
@@ -8448,6 +9384,10 @@ var init_ticket_burner = __esm(() => {
|
|
|
8448
9384
|
"claude-code": [],
|
|
8449
9385
|
codex: [/rate_limit_exceeded/i, /\b429\b/, /server_error/i, /stream (disconnected|interrupted)/i]
|
|
8450
9386
|
};
|
|
9387
|
+
AGENT_BINARY = {
|
|
9388
|
+
"claude-code": "claude",
|
|
9389
|
+
codex: "codex"
|
|
9390
|
+
};
|
|
8451
9391
|
activeTicketAborts = new Map;
|
|
8452
9392
|
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
8453
9393
|
ticketBurner = {
|
|
@@ -8459,7 +9399,7 @@ var init_ticket_burner = __esm(() => {
|
|
|
8459
9399
|
// src/workflows/research.ts
|
|
8460
9400
|
import { mkdirSync as mkdirSync10, readFileSync as readFileSync10 } from "fs";
|
|
8461
9401
|
import { dirname as dirname7, join as join15 } from "path";
|
|
8462
|
-
import { fileURLToPath as
|
|
9402
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
8463
9403
|
import { run as run3 } from "@ai-hero/sandcastle";
|
|
8464
9404
|
function waypointSlug(waypoint) {
|
|
8465
9405
|
const base = waypoint.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -8527,7 +9467,7 @@ function interpretResearchResult(result, docRelPath) {
|
|
|
8527
9467
|
return { status: "failed", error: "agent produced no commits \u2014 no research summary landed" };
|
|
8528
9468
|
}
|
|
8529
9469
|
function researchTemplatePath() {
|
|
8530
|
-
const here = dirname7(
|
|
9470
|
+
const here = dirname7(fileURLToPath7(import.meta.url));
|
|
8531
9471
|
return join15(resolveSkillsRoot(here), "burner", "research-waypoint.md");
|
|
8532
9472
|
}
|
|
8533
9473
|
async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
@@ -8544,14 +9484,16 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
8544
9484
|
mkdirSync10(logsDir(), { recursive: true });
|
|
8545
9485
|
const logFilePath = join15(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
|
|
8546
9486
|
const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
|
|
9487
|
+
const codexAuthMount = codexAuthMountFor(model.runtime, config2.sandbox);
|
|
8547
9488
|
const runOptions = {
|
|
8548
9489
|
agent: buildBurnAgent(config2, token, model),
|
|
8549
|
-
sandbox: selectSandbox(config2),
|
|
9490
|
+
sandbox: selectSandbox(config2, codexAuthMount ? [codexAuthMount] : []),
|
|
8550
9491
|
cwd: project.repoPath,
|
|
8551
9492
|
prompt,
|
|
8552
9493
|
branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
|
|
8553
9494
|
signal: ctx.signal,
|
|
8554
9495
|
name: `research-${waypoint.seq}`,
|
|
9496
|
+
...codexAuthMount ? { hooks: { sandbox: { onSandboxReady: [{ command: buildCodexAuthCopyCommand() }] } } } : {},
|
|
8555
9497
|
logging: { type: "file", path: logFilePath, onAgentStreamEvent: throttle.onEvent }
|
|
8556
9498
|
};
|
|
8557
9499
|
let result;
|
|
@@ -8599,7 +9541,7 @@ function resolveResearchDeps(ctx) {
|
|
|
8599
9541
|
return {
|
|
8600
9542
|
config: config2,
|
|
8601
9543
|
runtime: model.runtime,
|
|
8602
|
-
hasAuthToken: token
|
|
9544
|
+
hasAuthToken: burnAuthReady(model.runtime, token),
|
|
8603
9545
|
executeResearchRun: (c, waypoint) => realExecuteResearchRun(c, waypoint, config2, token, model)
|
|
8604
9546
|
};
|
|
8605
9547
|
}
|
|
@@ -8634,7 +9576,7 @@ var init_registry2 = __esm(() => {
|
|
|
8634
9576
|
});
|
|
8635
9577
|
|
|
8636
9578
|
// src/workflows/runner.ts
|
|
8637
|
-
import { eq as
|
|
9579
|
+
import { eq as eq10 } from "drizzle-orm";
|
|
8638
9580
|
function workflowClaimsFeatureBranch(workflowId) {
|
|
8639
9581
|
return BRANCH_CLAIMING.has(workflowId);
|
|
8640
9582
|
}
|
|
@@ -8663,7 +9605,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
8663
9605
|
claim(ctx, opts.claimWaypointId, runId);
|
|
8664
9606
|
} catch (e) {
|
|
8665
9607
|
const summary = e instanceof Error ? e.message : "claim failed";
|
|
8666
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(
|
|
9608
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(eq10(runs.id, runId)).run();
|
|
8667
9609
|
throw e;
|
|
8668
9610
|
}
|
|
8669
9611
|
}
|
|
@@ -8694,6 +9636,10 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
8694
9636
|
updateTicket: (id, patch) => {
|
|
8695
9637
|
updateTicket(ctx, id, patch);
|
|
8696
9638
|
},
|
|
9639
|
+
listTickets: () => listByFeature2(ctx, featureId),
|
|
9640
|
+
updateFinding: (findingId, progress, reason) => {
|
|
9641
|
+
markFixProgress(ctx, findingId, progress, reason);
|
|
9642
|
+
},
|
|
8697
9643
|
input: opts.input,
|
|
8698
9644
|
modelOverride: opts.modelOverride,
|
|
8699
9645
|
resolveWaypoint: (id, disposition, summary) => {
|
|
@@ -8731,7 +9677,7 @@ async function executeRun(ctx, runId, featureId, workflow2, runPromise, controll
|
|
|
8731
9677
|
} finally {
|
|
8732
9678
|
controllers.delete(runId);
|
|
8733
9679
|
}
|
|
8734
|
-
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary, ...digest ? { digest } : {} }).where(
|
|
9680
|
+
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary, ...digest ? { digest } : {} }).where(eq10(runs.id, runId)).run();
|
|
8735
9681
|
releaseForSession(ctx, runId);
|
|
8736
9682
|
if (workflowClaimsFeatureBranch(workflow2)) {
|
|
8737
9683
|
sweepOrphanedBurning(ctx, featureId, `orphaned \u2014 the run ended (${status}) while it was burning`);
|
|
@@ -8771,6 +9717,7 @@ var init_runner = __esm(() => {
|
|
|
8771
9717
|
init_gates();
|
|
8772
9718
|
init_git();
|
|
8773
9719
|
init_repo();
|
|
9720
|
+
init_review_findings();
|
|
8774
9721
|
init_tickets();
|
|
8775
9722
|
init_waypoints();
|
|
8776
9723
|
init_registry2();
|
|
@@ -8779,14 +9726,13 @@ var init_runner = __esm(() => {
|
|
|
8779
9726
|
});
|
|
8780
9727
|
|
|
8781
9728
|
// src/services/features.ts
|
|
8782
|
-
import { desc as desc5, eq as
|
|
9729
|
+
import { desc as desc5, eq as eq11 } from "drizzle-orm";
|
|
8783
9730
|
import { rmSync as rmSync4 } from "fs";
|
|
8784
9731
|
async function createFeature(ctx, input) {
|
|
8785
9732
|
const project = requireProjectById(ctx, input.projectId);
|
|
8786
9733
|
const slug = uniqueSlug(ctx, project.id, input.title);
|
|
8787
9734
|
const branch = `feature/${slug}`;
|
|
8788
|
-
const
|
|
8789
|
-
const cut = input.draft ? null : await ensureFeatureBranch(project, slug, requestedBase);
|
|
9735
|
+
const cut = input.draft ? null : await ensureFeatureBranch(project, slug, await requestedBase(project, input.baseBranch));
|
|
8790
9736
|
const row = {
|
|
8791
9737
|
id: newId("feat"),
|
|
8792
9738
|
projectId: project.id,
|
|
@@ -8817,12 +9763,7 @@ async function createFeature(ctx, input) {
|
|
|
8817
9763
|
});
|
|
8818
9764
|
if (!cut)
|
|
8819
9765
|
return feature;
|
|
8820
|
-
|
|
8821
|
-
if (cut.branchReady) {
|
|
8822
|
-
try {
|
|
8823
|
-
await commitDocs(project.repoPath, `runcastle: scaffold ${slug} docs`);
|
|
8824
|
-
} catch {}
|
|
8825
|
-
}
|
|
9766
|
+
await scaffoldDocsOnFeatureBranch(ctx, project, feature, { brief: input.brief });
|
|
8826
9767
|
return feature;
|
|
8827
9768
|
}
|
|
8828
9769
|
function requireNotDraft(feature) {
|
|
@@ -8836,21 +9777,16 @@ async function startDraft(ctx, featureId, opts = {}) {
|
|
|
8836
9777
|
throw new GateError(`feature ${feature.slug} is not a draft \u2014 it has already been started`);
|
|
8837
9778
|
}
|
|
8838
9779
|
const project = projectForFeature(ctx, feature);
|
|
8839
|
-
const
|
|
8840
|
-
const { branchReady, baseBranch } = await ensureFeatureBranch(project, feature.slug,
|
|
8841
|
-
ctx.db.update(features).set({ status: "active", baseBranch }).where(
|
|
9780
|
+
const base = await requestedBase(project, opts.baseBranch);
|
|
9781
|
+
const { branchReady, baseBranch } = await ensureFeatureBranch(project, feature.slug, base);
|
|
9782
|
+
ctx.db.update(features).set({ status: "active", baseBranch }).where(eq11(features.id, featureId)).run();
|
|
8842
9783
|
const started = { ...feature, status: "active", baseBranch };
|
|
8843
9784
|
emit(ctx, featureId, {
|
|
8844
9785
|
type: "feature.started",
|
|
8845
9786
|
message: branchReady ? `feature ${feature.slug} started (${feature.branch} \u2190 ${baseBranch})` : `feature ${feature.slug} started (branch pending)`,
|
|
8846
9787
|
data: { branch: feature.branch, baseBranch, branchReady }
|
|
8847
9788
|
});
|
|
8848
|
-
|
|
8849
|
-
if (branchReady) {
|
|
8850
|
-
try {
|
|
8851
|
-
await commitDocs(project.repoPath, `runcastle: scaffold ${feature.slug} docs`);
|
|
8852
|
-
} catch {}
|
|
8853
|
-
}
|
|
9789
|
+
await scaffoldDocsOnFeatureBranch(ctx, project, started, { brief: feature.brief });
|
|
8854
9790
|
return started;
|
|
8855
9791
|
}
|
|
8856
9792
|
function quickTicketTitle(prose) {
|
|
@@ -8876,10 +9812,10 @@ ${body}
|
|
|
8876
9812
|
function quickReviewTicket(proses) {
|
|
8877
9813
|
return {
|
|
8878
9814
|
title: "Review the integrated change",
|
|
8879
|
-
goal: "Review the integrated feature branch and report what you find
|
|
8880
|
-
context: "This feature came through the quick-change door, so there was no grill session and there " + "is no spec.md or decisions.md to review it against: the whole statement of intent is the " + `${proses.length === 1 ? "sentence" : `${proses.length} sentences`} the human typed, which ` + "are the other tickets in this batch and are reproduced verbatim in brief.md. Nobody " + "prescribed a walkthrough for you either \u2014 judge from the diff whether there is anything " + "drivable, and if there is not,
|
|
9815
|
+
goal: "Review the integrated feature branch and report what you find, in exactly one mode: " + "drive the app in a browser against the criteria below when this change touched something " + "a human can operate and a drive is available, and otherwise run the verify gates and " + "code-review the branch's diff against its base. Write one note per finding; finding bugs " + "is a successful review, and the notes are the deliverable.",
|
|
9816
|
+
context: "This feature came through the quick-change door, so there was no grill session and there " + "is no spec.md or decisions.md to review it against: the whole statement of intent is the " + `${proses.length === 1 ? "sentence" : `${proses.length} sentences`} the human typed, which ` + "are the other tickets in this batch and are reproduced verbatim in brief.md. Nobody " + "prescribed a walkthrough for you either \u2014 judge from the diff whether there is anything " + "drivable, and if there is not, take the gates-and-diff mode. Your digest is " + "the lap's prose summary of what landed, and its first line names the mode you ran; the " + "review page leads with it.",
|
|
8881
9817
|
acceptanceCriteria: [
|
|
8882
|
-
"
|
|
9818
|
+
"Reviewed in one mode \u2014 either walked in a browser, or put through the verify gates and " + "code-reviewed on both axes (the repo's own standards, and the change against what was " + "asked for).",
|
|
8883
9819
|
...proses.map((prose) => `Landed and does what it says: ${prose}`)
|
|
8884
9820
|
],
|
|
8885
9821
|
seams: [],
|
|
@@ -8897,8 +9833,8 @@ async function quickChange(ctx, input) {
|
|
|
8897
9833
|
throw new InvalidInputError("a quick change needs a sentence describing the change");
|
|
8898
9834
|
const slug = uniqueSlug(ctx, project.id, title);
|
|
8899
9835
|
const branch = `feature/${slug}`;
|
|
8900
|
-
const
|
|
8901
|
-
const { branchReady, baseBranch } = await ensureFeatureBranch(project, slug,
|
|
9836
|
+
const base = await requestedBase(project, input.baseBranch);
|
|
9837
|
+
const { branchReady, baseBranch } = await ensureFeatureBranch(project, slug, base);
|
|
8902
9838
|
const inserted = ctx.db.insert(features).values({
|
|
8903
9839
|
id: newId("feat"),
|
|
8904
9840
|
projectId: project.id,
|
|
@@ -8920,7 +9856,7 @@ async function quickChange(ctx, input) {
|
|
|
8920
9856
|
message: branchReady ? `feature.created (${branch} \u2190 ${baseBranch})` : "feature.created (branch pending)",
|
|
8921
9857
|
data: { slug, branch, baseBranch, branchReady }
|
|
8922
9858
|
});
|
|
8923
|
-
|
|
9859
|
+
await scaffoldDocsOnFeatureBranch(ctx, project, feature, { brief: quickBrief(title, proses) });
|
|
8924
9860
|
const stored = storeTickets(ctx, feature.id, [
|
|
8925
9861
|
...proses.map((prose) => ({
|
|
8926
9862
|
title: quickTicketTitle(prose),
|
|
@@ -8940,24 +9876,42 @@ async function quickChange(ctx, input) {
|
|
|
8940
9876
|
message: `quick change \u2014 born at implementation on lap 1 with ${tally} (${typed.map((t) => `#${t.seq}`).join(", ")}) plus a review ticket (#${review.seq}); no grill session, no spec.md`,
|
|
8941
9877
|
data: { slug, ticketSeqs: stored.map((t) => t.seq), phase: "implementation" }
|
|
8942
9878
|
});
|
|
8943
|
-
if (branchReady) {
|
|
8944
|
-
try {
|
|
8945
|
-
await commitDocs(project.repoPath, `runcastle: scaffold ${slug} docs`);
|
|
8946
|
-
} catch {}
|
|
8947
|
-
}
|
|
8948
9879
|
return feature;
|
|
8949
9880
|
}
|
|
8950
|
-
async function
|
|
9881
|
+
async function requestedBase(project, picked) {
|
|
9882
|
+
return picked?.trim() || await currentCheckoutBranch(project);
|
|
9883
|
+
}
|
|
9884
|
+
async function ensureFeatureBranch(project, slug, base) {
|
|
8951
9885
|
try {
|
|
8952
|
-
const baseBranch = await resolveBaseBranch(project,
|
|
9886
|
+
const baseBranch = await resolveBaseBranch(project, base);
|
|
8953
9887
|
await createFeatureBranch(project, slug, baseBranch);
|
|
8954
9888
|
return { branchReady: true, baseBranch };
|
|
8955
9889
|
} catch (e) {
|
|
8956
9890
|
if (isNotImplemented(e))
|
|
8957
|
-
return { branchReady: false, baseBranch:
|
|
9891
|
+
return { branchReady: false, baseBranch: base };
|
|
8958
9892
|
throw e;
|
|
8959
9893
|
}
|
|
8960
9894
|
}
|
|
9895
|
+
async function scaffoldDocsOnFeatureBranch(ctx, project, feature, opts) {
|
|
9896
|
+
let worktreePath;
|
|
9897
|
+
try {
|
|
9898
|
+
worktreePath = await ensureTalkWorktree(project, feature);
|
|
9899
|
+
} catch (e) {
|
|
9900
|
+
if (isNotImplemented(e)) {
|
|
9901
|
+
scaffoldDocs(ctx, feature, opts);
|
|
9902
|
+
return;
|
|
9903
|
+
}
|
|
9904
|
+
emit(ctx, feature.id, {
|
|
9905
|
+
type: "docs.scaffold_failed",
|
|
9906
|
+
message: `docs not scaffolded onto ${feature.branch}: ${e instanceof Error ? e.message : String(e)}`
|
|
9907
|
+
});
|
|
9908
|
+
return;
|
|
9909
|
+
}
|
|
9910
|
+
scaffoldDocs(ctx, feature, opts);
|
|
9911
|
+
try {
|
|
9912
|
+
await commitDocs(worktreePath, `runcastle: scaffold ${feature.slug} docs`);
|
|
9913
|
+
} catch {}
|
|
9914
|
+
}
|
|
8961
9915
|
function getFeatureFull(ctx, id) {
|
|
8962
9916
|
const feature = getFeatureRow(ctx, id);
|
|
8963
9917
|
const waypoints2 = feature.mapped ? listByFeature(ctx, id) : [];
|
|
@@ -8974,7 +9928,7 @@ function getFeatureFull(ctx, id) {
|
|
|
8974
9928
|
};
|
|
8975
9929
|
}
|
|
8976
9930
|
function list(ctx, projectId) {
|
|
8977
|
-
const rows = ctx.db.select().from(features).where(
|
|
9931
|
+
const rows = ctx.db.select().from(features).where(eq11(features.projectId, projectId)).orderBy(desc5(features.createdAt)).all();
|
|
8978
9932
|
const lastActivity = latestTsByFeature(ctx, projectId);
|
|
8979
9933
|
return rows.map((row) => {
|
|
8980
9934
|
const feature = rowToFeature(row);
|
|
@@ -9089,7 +10043,7 @@ function rethink(ctx, featureId) {
|
|
|
9089
10043
|
throw new GateError(`${feature.slug} is being test-driven \u2014 stop the test drive first, it holds the feature branch`);
|
|
9090
10044
|
}
|
|
9091
10045
|
const lap = feature.lap + 1;
|
|
9092
|
-
ctx.db.update(features).set({ lap }).where(
|
|
10046
|
+
ctx.db.update(features).set({ lap }).where(eq11(features.id, featureId)).run();
|
|
9093
10047
|
return setPhase(ctx, featureId, RETHINK_LOOP_BACK.to, "lap.started", `rethink \u2014 lap ${lap}`);
|
|
9094
10048
|
}
|
|
9095
10049
|
async function rethinkAndLaunch(ctx, featureId, launch) {
|
|
@@ -9098,7 +10052,7 @@ async function rethinkAndLaunch(ctx, featureId, launch) {
|
|
|
9098
10052
|
try {
|
|
9099
10053
|
return await launch(feature);
|
|
9100
10054
|
} catch (e) {
|
|
9101
|
-
ctx.db.update(features).set({ lap: before.lap }).where(
|
|
10055
|
+
ctx.db.update(features).set({ lap: before.lap }).where(eq11(features.id, featureId)).run();
|
|
9102
10056
|
setPhase(ctx, featureId, before.phase, "lap.aborted", `lap ${feature.lap} aborted \u2014 its terminal could not be opened (${errMsg2(e)}); back at ${before.phase} on lap ${before.lap}`);
|
|
9103
10057
|
throw e;
|
|
9104
10058
|
}
|
|
@@ -9177,7 +10131,7 @@ function escalateToMap(ctx, featureId, input) {
|
|
|
9177
10131
|
return { ok: true, warning: `feature ${feature.slug} is already mapped \u2014 no changes made` };
|
|
9178
10132
|
}
|
|
9179
10133
|
const project = projectForFeature(ctx, feature);
|
|
9180
|
-
ctx.db.update(features).set({ mapped: true }).where(
|
|
10134
|
+
ctx.db.update(features).set({ mapped: true }).where(eq11(features.id, featureId)).run();
|
|
9181
10135
|
scaffoldMapDoc(project, { ...feature, mapped: true }, input);
|
|
9182
10136
|
emit(ctx, featureId, {
|
|
9183
10137
|
type: "feature.escalated",
|
|
@@ -9195,7 +10149,7 @@ function archiveFeature(ctx, featureId) {
|
|
|
9195
10149
|
const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
|
|
9196
10150
|
if (live)
|
|
9197
10151
|
endSession(ctx, live.id);
|
|
9198
|
-
ctx.db.update(features).set({ status: "archived" }).where(
|
|
10152
|
+
ctx.db.update(features).set({ status: "archived" }).where(eq11(features.id, featureId)).run();
|
|
9199
10153
|
emit(ctx, featureId, {
|
|
9200
10154
|
type: "feature.archived",
|
|
9201
10155
|
message: `feature ${feature.slug} archived`,
|
|
@@ -9209,7 +10163,7 @@ function unarchiveFeature(ctx, featureId) {
|
|
|
9209
10163
|
throw new GateError(`feature ${feature.slug} is not archived`);
|
|
9210
10164
|
}
|
|
9211
10165
|
const restored = feature.phase === "shipped" ? "shipped" : "active";
|
|
9212
|
-
ctx.db.update(features).set({ status: restored }).where(
|
|
10166
|
+
ctx.db.update(features).set({ status: restored }).where(eq11(features.id, featureId)).run();
|
|
9213
10167
|
emit(ctx, featureId, {
|
|
9214
10168
|
type: "feature.unarchived",
|
|
9215
10169
|
message: `feature ${feature.slug} unarchived (${restored})`,
|
|
@@ -9249,13 +10203,13 @@ async function deleteFeature(ctx, featureId) {
|
|
|
9249
10203
|
return { ok: true, slug: feature.slug };
|
|
9250
10204
|
}
|
|
9251
10205
|
function deleteFeatureRows(ctx, featureId) {
|
|
9252
|
-
ctx.db.delete(tickets).where(
|
|
9253
|
-
ctx.db.delete(sessions).where(
|
|
9254
|
-
ctx.db.delete(runs).where(
|
|
9255
|
-
ctx.db.delete(events).where(
|
|
9256
|
-
ctx.db.delete(gateOverrides).where(
|
|
9257
|
-
ctx.db.delete(waypoints).where(
|
|
9258
|
-
ctx.db.delete(features).where(
|
|
10206
|
+
ctx.db.delete(tickets).where(eq11(tickets.featureId, featureId)).run();
|
|
10207
|
+
ctx.db.delete(sessions).where(eq11(sessions.featureId, featureId)).run();
|
|
10208
|
+
ctx.db.delete(runs).where(eq11(runs.featureId, featureId)).run();
|
|
10209
|
+
ctx.db.delete(events).where(eq11(events.featureId, featureId)).run();
|
|
10210
|
+
ctx.db.delete(gateOverrides).where(eq11(gateOverrides.featureId, featureId)).run();
|
|
10211
|
+
ctx.db.delete(waypoints).where(eq11(waypoints.featureId, featureId)).run();
|
|
10212
|
+
ctx.db.delete(features).where(eq11(features.id, featureId)).run();
|
|
9259
10213
|
}
|
|
9260
10214
|
function gateState(ctx, feature) {
|
|
9261
10215
|
const gate = nextGate(feature);
|
|
@@ -9266,7 +10220,7 @@ function gateState(ctx, feature) {
|
|
|
9266
10220
|
}
|
|
9267
10221
|
function uniqueSlug(ctx, projectId, title) {
|
|
9268
10222
|
const base = slugify(title);
|
|
9269
|
-
const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(
|
|
10223
|
+
const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(eq11(features.projectId, projectId)).all().map((r) => r.slug));
|
|
9270
10224
|
if (!existing.has(base))
|
|
9271
10225
|
return base;
|
|
9272
10226
|
let n = 2;
|
|
@@ -9297,9 +10251,9 @@ var init_features = __esm(() => {
|
|
|
9297
10251
|
});
|
|
9298
10252
|
|
|
9299
10253
|
// src/services/test-notes.ts
|
|
9300
|
-
import { existsSync as
|
|
10254
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync11, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
9301
10255
|
import { dirname as dirname8 } from "path";
|
|
9302
|
-
import { asc as
|
|
10256
|
+
import { asc as asc5, eq as eq12 } from "drizzle-orm";
|
|
9303
10257
|
function rowToNote(row) {
|
|
9304
10258
|
return TestNote.parse({
|
|
9305
10259
|
id: row.id,
|
|
@@ -9310,13 +10264,13 @@ function rowToNote(row) {
|
|
|
9310
10264
|
author: row.author,
|
|
9311
10265
|
ticketId: row.ticketId ?? undefined,
|
|
9312
10266
|
videoTimestamp: row.videoTimestamp ?? undefined,
|
|
9313
|
-
screenshotUrl:
|
|
10267
|
+
screenshotUrl: existsSync18(annotationPath(row.id)) ? noteScreenshotUrl(row.id) : undefined,
|
|
9314
10268
|
createdAt: row.createdAt,
|
|
9315
10269
|
updatedAt: row.updatedAt
|
|
9316
10270
|
});
|
|
9317
10271
|
}
|
|
9318
10272
|
function getNote(ctx, id) {
|
|
9319
|
-
const row = ctx.db.select().from(testNotes).where(
|
|
10273
|
+
const row = ctx.db.select().from(testNotes).where(eq12(testNotes.id, id)).get();
|
|
9320
10274
|
if (!row)
|
|
9321
10275
|
throw new NotFoundError(`test note ${id} not found`);
|
|
9322
10276
|
return rowToNote(row);
|
|
@@ -9332,8 +10286,8 @@ function assertOpen(note, action) {
|
|
|
9332
10286
|
throw new InvalidInputError(`cannot ${action} note ${note.id} \u2014 it is ${note.status}; only open notes can be changed`);
|
|
9333
10287
|
}
|
|
9334
10288
|
}
|
|
9335
|
-
function
|
|
9336
|
-
return ctx.db.select().from(testNotes).where(
|
|
10289
|
+
function listByFeature4(ctx, featureId) {
|
|
10290
|
+
return ctx.db.select().from(testNotes).where(eq12(testNotes.featureId, featureId)).orderBy(asc5(testNotes.createdAt), asc5(testNotes.id)).all().map(rowToNote);
|
|
9337
10291
|
}
|
|
9338
10292
|
function addNote(ctx, featureId, text2, author = "human", videoTimestamp) {
|
|
9339
10293
|
const body = cleanText(text2);
|
|
@@ -9364,7 +10318,7 @@ function editNote(ctx, noteId, text2) {
|
|
|
9364
10318
|
const current2 = getNote(ctx, noteId);
|
|
9365
10319
|
assertOpen(current2, "edit");
|
|
9366
10320
|
const body = cleanText(text2);
|
|
9367
|
-
ctx.db.update(testNotes).set({ text: body, updatedAt: Date.now() }).where(
|
|
10321
|
+
ctx.db.update(testNotes).set({ text: body, updatedAt: Date.now() }).where(eq12(testNotes.id, noteId)).run();
|
|
9368
10322
|
emit(ctx, current2.featureId, {
|
|
9369
10323
|
type: "note.edited",
|
|
9370
10324
|
message: "note edited",
|
|
@@ -9376,7 +10330,7 @@ function editNote(ctx, noteId, text2) {
|
|
|
9376
10330
|
function deleteNote(ctx, noteId) {
|
|
9377
10331
|
const current2 = getNote(ctx, noteId);
|
|
9378
10332
|
assertOpen(current2, "delete");
|
|
9379
|
-
ctx.db.delete(testNotes).where(
|
|
10333
|
+
ctx.db.delete(testNotes).where(eq12(testNotes.id, noteId)).run();
|
|
9380
10334
|
rmSync5(annotationPath(noteId), { force: true });
|
|
9381
10335
|
emit(ctx, current2.featureId, {
|
|
9382
10336
|
type: "note.deleted",
|
|
@@ -9403,7 +10357,7 @@ function toggleNote(ctx, noteId) {
|
|
|
9403
10357
|
throw new InvalidInputError(`cannot toggle note ${noteId} \u2014 it is promoted, and promoted notes are frozen`);
|
|
9404
10358
|
}
|
|
9405
10359
|
const status = current2.status === "open" ? "done" : "open";
|
|
9406
|
-
ctx.db.update(testNotes).set({ status, updatedAt: Date.now() }).where(
|
|
10360
|
+
ctx.db.update(testNotes).set({ status, updatedAt: Date.now() }).where(eq12(testNotes.id, noteId)).run();
|
|
9407
10361
|
emit(ctx, current2.featureId, {
|
|
9408
10362
|
type: "note.toggled",
|
|
9409
10363
|
message: `note ${current2.status} \u2192 ${status}`,
|
|
@@ -9453,7 +10407,7 @@ function freezeAsTickets(ctx, noteIds) {
|
|
|
9453
10407
|
const tickets2 = storeTickets(ctx, featureId, selected.map((note) => promotionTicket(feature, note)));
|
|
9454
10408
|
const now = Date.now();
|
|
9455
10409
|
selected.forEach((note, i) => {
|
|
9456
|
-
ctx.db.update(testNotes).set({ status: "promoted", ticketId: tickets2[i].id, updatedAt: now }).where(
|
|
10410
|
+
ctx.db.update(testNotes).set({ status: "promoted", ticketId: tickets2[i].id, updatedAt: now }).where(eq12(testNotes.id, note.id)).run();
|
|
9457
10411
|
});
|
|
9458
10412
|
return { feature, notes: selected.map((n) => getNote(ctx, n.id)), tickets: tickets2 };
|
|
9459
10413
|
}
|
|
@@ -9491,7 +10445,7 @@ function noteLine(ctx, note) {
|
|
|
9491
10445
|
return `- [x] ${note.text}${ticket}${shot}`;
|
|
9492
10446
|
}
|
|
9493
10447
|
function renderTestNotes(ctx, feature) {
|
|
9494
|
-
const notes =
|
|
10448
|
+
const notes = listByFeature4(ctx, feature.id);
|
|
9495
10449
|
const laps = [...new Set(notes.map((n) => n.lap))].sort((a, b) => a - b);
|
|
9496
10450
|
const lines = ["# Test notes"];
|
|
9497
10451
|
for (const lap of laps) {
|
|
@@ -9518,7 +10472,7 @@ var init_test_notes = __esm(() => {
|
|
|
9518
10472
|
});
|
|
9519
10473
|
|
|
9520
10474
|
// src/mcp/server.ts
|
|
9521
|
-
import { existsSync as
|
|
10475
|
+
import { existsSync as existsSync19, readFileSync as readFileSync11 } from "fs";
|
|
9522
10476
|
import { join as join16, relative as relative3, resolve as resolve9, sep as sep2 } from "path";
|
|
9523
10477
|
import { StreamableHTTPTransport } from "@hono/mcp";
|
|
9524
10478
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -9817,6 +10771,23 @@ function toolAddTestNote(ctx, caller, input) {
|
|
|
9817
10771
|
const identity = requireRunIdentity(ctx, caller, "add_test_note");
|
|
9818
10772
|
return addNote(ctx, identity.featureId, input.text, "agent");
|
|
9819
10773
|
}
|
|
10774
|
+
function toolReportFinding(ctx, caller, input) {
|
|
10775
|
+
const identity = requireRunIdentity(ctx, caller, "report_finding");
|
|
10776
|
+
const reviewTicket = listByFeature2(ctx, identity.featureId).find((ticket) => ticket.kind === "review" && ticket.status === "burning");
|
|
10777
|
+
if (!reviewTicket) {
|
|
10778
|
+
throw new GateError("report_finding requires a burning review ticket on this run\u2019s feature");
|
|
10779
|
+
}
|
|
10780
|
+
const result = reportFinding(ctx, {
|
|
10781
|
+
featureId: identity.featureId,
|
|
10782
|
+
reviewTicket,
|
|
10783
|
+
input
|
|
10784
|
+
});
|
|
10785
|
+
return {
|
|
10786
|
+
findingId: result.finding.id,
|
|
10787
|
+
fixTicketSeq: result.fixTicket?.seq ?? null,
|
|
10788
|
+
overCap: result.overCap
|
|
10789
|
+
};
|
|
10790
|
+
}
|
|
9820
10791
|
async function toolRetryDrive(ctx, session) {
|
|
9821
10792
|
if (session.kind !== "drive-fix") {
|
|
9822
10793
|
throw new GateError(`retrying a drive belongs to a drive-fix session, and this is a ${session.kind} session. ` + "It stops and restarts a test drive on the human's machine \u2014 only the session opened on " + "that failed drive may do that; everyone else asks the human to drive from the review panel.");
|
|
@@ -9872,7 +10843,7 @@ async function toolCreateFeature(ctx, session, input) {
|
|
|
9872
10843
|
phase: feature.phase
|
|
9873
10844
|
};
|
|
9874
10845
|
}
|
|
9875
|
-
function toolGetProjectContext(ctx, session) {
|
|
10846
|
+
async function toolGetProjectContext(ctx, session) {
|
|
9876
10847
|
const project = requireProject(ctx, session);
|
|
9877
10848
|
const charter = readCharter(session.worktreePath);
|
|
9878
10849
|
return {
|
|
@@ -9880,7 +10851,18 @@ function toolGetProjectContext(ctx, session) {
|
|
|
9880
10851
|
...charter !== undefined ? { charter } : {},
|
|
9881
10852
|
adrs: listLiveAdrs(session.worktreePath).map(adrRef),
|
|
9882
10853
|
adrsNote: ADRS_NOTE,
|
|
9883
|
-
featureIndex: list(ctx, project.id).map(featureIndexLine)
|
|
10854
|
+
featureIndex: list(ctx, project.id).map(featureIndexLine),
|
|
10855
|
+
baseBranches: await readBaseBranches(project)
|
|
10856
|
+
};
|
|
10857
|
+
}
|
|
10858
|
+
async function readBaseBranches(project) {
|
|
10859
|
+
const { current: current2, branches, remoteBranches } = await listBranches(project);
|
|
10860
|
+
const selectable = [...branches, ...remoteBranches];
|
|
10861
|
+
return {
|
|
10862
|
+
current: current2,
|
|
10863
|
+
currentIsSelectable: selectable.includes(current2),
|
|
10864
|
+
selectable,
|
|
10865
|
+
detectedMain: await detectMainBranch(project.repoPath)
|
|
9884
10866
|
};
|
|
9885
10867
|
}
|
|
9886
10868
|
function adrRef(adr) {
|
|
@@ -9902,7 +10884,7 @@ function toolReadAdr(ctx, session, input) {
|
|
|
9902
10884
|
if (rel === "" || rel.startsWith("..") || rel.split(sep2).includes("..")) {
|
|
9903
10885
|
throw new InvalidInputError(`adr path escapes ${ADR_DIR_REL}: ${input.relPath}`);
|
|
9904
10886
|
}
|
|
9905
|
-
if (!
|
|
10887
|
+
if (!existsSync19(target))
|
|
9906
10888
|
throw new NotFoundError(`adr not found: ${input.relPath}`);
|
|
9907
10889
|
return { relPath: `${ADR_DIR_REL}/${name}`, content: readFileSync11(target, "utf8") };
|
|
9908
10890
|
}
|
|
@@ -10064,6 +11046,16 @@ function buildMcpServer(audience) {
|
|
|
10064
11046
|
return ok(toolAddTestNote(rc.ctx, rc.caller, args));
|
|
10065
11047
|
});
|
|
10066
11048
|
}
|
|
11049
|
+
if (wants("report_finding")) {
|
|
11050
|
+
server.registerTool("report_finding", {
|
|
11051
|
+
title: "Report a structured review finding",
|
|
11052
|
+
description: "Report one typed finding for the feature under review. Actionable defects mint fix " + "tickets automatically up to the review cap; observations are retained for the digest.",
|
|
11053
|
+
inputSchema: ReviewFindingInput.shape
|
|
11054
|
+
}, async (args, extra) => {
|
|
11055
|
+
const rc = await resolveRunCaller(extra);
|
|
11056
|
+
return ok(toolReportFinding(rc.ctx, rc.caller, ReviewFindingInput.parse(args)));
|
|
11057
|
+
});
|
|
11058
|
+
}
|
|
10067
11059
|
if (wants("retry_drive")) {
|
|
10068
11060
|
server.registerTool("retry_drive", {
|
|
10069
11061
|
title: "Retry this feature\u2019s test drive",
|
|
@@ -10099,7 +11091,7 @@ function buildMcpServer(audience) {
|
|
|
10099
11091
|
inputSchema: {
|
|
10100
11092
|
title: z4.string().min(1).describe("The feature\u2019s name; its slug and branch derive from this."),
|
|
10101
11093
|
oneLiner: z4.string().optional().describe("One line saying what this feature is. Required for every shape EXCEPT `tickets`, " + "where the first ticket\u2019s prose becomes the one-liner and anything sent here is " + "ignored."),
|
|
10102
|
-
baseBranch: z4.string().optional().describe("Branch to cut from
|
|
11094
|
+
baseBranch: z4.string().optional().describe("Branch to cut from \u2014 always pass it explicitly, naming the base you told the " + "human you would use (see `get_project_context`\u2019s `baseBranches`). Omitted, it " + "falls back to whatever the project checkout happens to be on. Drafts pass none: " + "a parked feature picks its base at Start."),
|
|
10103
11095
|
brief: z4.string().optional().describe("The reasoning you just worked out with the human \u2014 why this feature exists and " + "what it must NOT swallow. Becomes brief.md verbatim, and without it that " + "reasoning is lost. Unused by the `tickets` shape, whose brief IS the proses."),
|
|
10104
11096
|
draft: z4.boolean().optional().describe("PARK it instead of starting it: a row and its brief, no branch and no files, " + "until the human clicks Start. Ask them per feature whether to start or park."),
|
|
10105
11097
|
tickets: z4.array(z4.string().min(1)).min(1).optional().describe("The quick-change shape: work too small to deserve a grill. Creates ONE feature at " + "the implementation phase carrying a ticket per prose (plus the review ticket " + "every batch closes with). Send every sentence of one quick change in a SINGLE " + "call \u2014 calling this once per ticket makes one feature per ticket.")
|
|
@@ -10114,13 +11106,13 @@ function buildMcpServer(audience) {
|
|
|
10114
11106
|
if (wants("get_project_context")) {
|
|
10115
11107
|
server.registerTool("get_project_context", {
|
|
10116
11108
|
title: "Get project context",
|
|
10117
|
-
description: "The project as it stands: the project row, the charter (CONTEXT.md) in full, an INDEX " + "of every live ADR (superseded ones omitted) and a one-line index of every feature. " + "ADR bodies are not inlined \u2014 read the ones your work touches with `read_adr`. Shipped " + "features carry their one-liner and docs path (readable on disk); in-flight ones carry " + "slug, phase, lap and ticket counts but no docs path, because their docs live on an " + "unmerged branch. Use the slug with `get_work_record` to see what one actually did.",
|
|
11109
|
+
description: "The project as it stands: the project row, the charter (CONTEXT.md) in full, an INDEX " + "of every live ADR (superseded ones omitted) and a one-line index of every feature. " + "ADR bodies are not inlined \u2014 read the ones your work touches with `read_adr`. Shipped " + "features carry their one-liner and docs path (readable on disk); in-flight ones carry " + "slug, phase, lap and ticket counts but no docs path, because their docs live on an " + "unmerged branch. Use the slug with `get_work_record` to see what one actually did. " + "Plus `baseBranches` \u2014 the branch a new feature would cut from: the checkout\u2019s `current` " + "branch, `currentIsSelectable` (false mid test drive or on a detached HEAD, the one case " + "with no default), every `selectable` base, and the `detectedMain` line to suggest then.",
|
|
10118
11110
|
inputSchema: {}
|
|
10119
11111
|
}, async (_args, extra) => {
|
|
10120
11112
|
const rs = await resolveCtxSession(extra);
|
|
10121
11113
|
if (!rs)
|
|
10122
11114
|
return noSession();
|
|
10123
|
-
return ok(toolGetProjectContext(rs.ctx, rs.session));
|
|
11115
|
+
return ok(await toolGetProjectContext(rs.ctx, rs.session));
|
|
10124
11116
|
});
|
|
10125
11117
|
}
|
|
10126
11118
|
if (wants("read_adr")) {
|
|
@@ -10359,6 +11351,7 @@ var init_server = __esm(() => {
|
|
|
10359
11351
|
init_features();
|
|
10360
11352
|
init_events();
|
|
10361
11353
|
init_findings();
|
|
11354
|
+
init_review_findings();
|
|
10362
11355
|
init_git();
|
|
10363
11356
|
init_knowledge();
|
|
10364
11357
|
init_repo();
|
|
@@ -10398,7 +11391,8 @@ var init_server = __esm(() => {
|
|
|
10398
11391
|
dry_run_drive: ["prepare"],
|
|
10399
11392
|
retry_drive: ["drive-fix"],
|
|
10400
11393
|
review_drive: ["run"],
|
|
10401
|
-
|
|
11394
|
+
report_finding: ["run"],
|
|
11395
|
+
add_test_note: [],
|
|
10402
11396
|
record_event: [...FEATURE_KINDS, ...PROJECT_KINDS]
|
|
10403
11397
|
};
|
|
10404
11398
|
CompletablePhase = Phase.exclude(["shipped"]);
|
|
@@ -10488,7 +11482,7 @@ var init_ws = __esm(() => {
|
|
|
10488
11482
|
});
|
|
10489
11483
|
|
|
10490
11484
|
// src/services/prep.ts
|
|
10491
|
-
import { and as
|
|
11485
|
+
import { and as and9, desc as desc6, eq as eq13, inArray as inArray4 } from "drizzle-orm";
|
|
10492
11486
|
function keysToPrepare(ctx, project) {
|
|
10493
11487
|
return unsetPreparedKeys(project).filter((key) => isOverwritable(ctx, project.id, key));
|
|
10494
11488
|
}
|
|
@@ -10498,10 +11492,10 @@ function isPrepared(ctx, project) {
|
|
|
10498
11492
|
return hasCompletedProjectSession(ctx, project.id, "prepare");
|
|
10499
11493
|
}
|
|
10500
11494
|
function preparedAt(ctx, projectId) {
|
|
10501
|
-
const prepareSessions = new Set(ctx.db.select({ id: sessions.id }).from(sessions).where(
|
|
11495
|
+
const prepareSessions = new Set(ctx.db.select({ id: sessions.id }).from(sessions).where(and9(eq13(sessions.projectId, projectId), eq13(sessions.kind, "prepare"))).all().map((r) => r.id));
|
|
10502
11496
|
if (prepareSessions.size === 0)
|
|
10503
11497
|
return null;
|
|
10504
|
-
const ends = ctx.db.select({ ts: events.ts, data: events.data }).from(events).where(
|
|
11498
|
+
const ends = ctx.db.select({ ts: events.ts, data: events.data }).from(events).where(and9(eq13(events.projectId, projectId), inArray4(events.type, ["session.ended", "session.reconciled"]))).orderBy(desc6(events.ts)).all();
|
|
10505
11499
|
for (const end of ends) {
|
|
10506
11500
|
const sessionId = end.data?.sessionId;
|
|
10507
11501
|
if (sessionId && prepareSessions.has(sessionId))
|
|
@@ -10756,7 +11750,7 @@ var init_hooks = __esm(() => {
|
|
|
10756
11750
|
});
|
|
10757
11751
|
|
|
10758
11752
|
// src/routes/reviews.ts
|
|
10759
|
-
import { createReadStream, statSync as
|
|
11753
|
+
import { createReadStream, statSync as statSync4 } from "fs";
|
|
10760
11754
|
import { Readable } from "stream";
|
|
10761
11755
|
import { Hono as Hono3 } from "hono";
|
|
10762
11756
|
function walkthroughUrl(ticketId) {
|
|
@@ -10764,7 +11758,7 @@ function walkthroughUrl(ticketId) {
|
|
|
10764
11758
|
}
|
|
10765
11759
|
function fileSize(path) {
|
|
10766
11760
|
try {
|
|
10767
|
-
const stat =
|
|
11761
|
+
const stat = statSync4(path);
|
|
10768
11762
|
return stat.isFile() ? stat.size : undefined;
|
|
10769
11763
|
} catch {
|
|
10770
11764
|
return;
|
|
@@ -10942,7 +11936,7 @@ var init_stream = __esm(() => {
|
|
|
10942
11936
|
});
|
|
10943
11937
|
|
|
10944
11938
|
// src/routes/web.ts
|
|
10945
|
-
import { existsSync as
|
|
11939
|
+
import { existsSync as existsSync20, statSync as statSync5 } from "fs";
|
|
10946
11940
|
import { readFile } from "fs/promises";
|
|
10947
11941
|
import { extname, join as join17, normalize, resolve as resolve10, sep as sep3 } from "path";
|
|
10948
11942
|
function contentType(path) {
|
|
@@ -10950,7 +11944,7 @@ function contentType(path) {
|
|
|
10950
11944
|
}
|
|
10951
11945
|
function isFile(path) {
|
|
10952
11946
|
try {
|
|
10953
|
-
return
|
|
11947
|
+
return statSync5(path).isFile();
|
|
10954
11948
|
} catch {
|
|
10955
11949
|
return false;
|
|
10956
11950
|
}
|
|
@@ -10995,7 +11989,7 @@ function mountWebApp(app, distDir) {
|
|
|
10995
11989
|
}
|
|
10996
11990
|
function mountWebAppIfBuilt(app) {
|
|
10997
11991
|
const dist = resolveWebDist();
|
|
10998
|
-
if (
|
|
11992
|
+
if (existsSync20(join17(dist, "index.html")))
|
|
10999
11993
|
mountWebApp(app, dist);
|
|
11000
11994
|
}
|
|
11001
11995
|
var MIME;
|
|
@@ -11024,11 +12018,11 @@ var init_web = __esm(() => {
|
|
|
11024
12018
|
// src/version.ts
|
|
11025
12019
|
import { readFileSync as readFileSync12 } from "fs";
|
|
11026
12020
|
import { dirname as dirname9, join as join18 } from "path";
|
|
11027
|
-
import { fileURLToPath as
|
|
12021
|
+
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
11028
12022
|
function runcastleVersion() {
|
|
11029
12023
|
if (cached2)
|
|
11030
12024
|
return cached2;
|
|
11031
|
-
const here = dirname9(
|
|
12025
|
+
const here = dirname9(fileURLToPath8(import.meta.url));
|
|
11032
12026
|
for (const pkgPath of [join18(here, "..", "package.json"), join18(here, "package.json")]) {
|
|
11033
12027
|
try {
|
|
11034
12028
|
const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
|
|
@@ -11046,12 +12040,12 @@ var init_version = () => {};
|
|
|
11046
12040
|
|
|
11047
12041
|
// src/services/install-id.ts
|
|
11048
12042
|
import { randomUUID } from "crypto";
|
|
11049
|
-
import { existsSync as
|
|
12043
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
|
|
11050
12044
|
import { dirname as dirname10 } from "path";
|
|
11051
12045
|
function getInstallId() {
|
|
11052
12046
|
const file = installIdPath();
|
|
11053
12047
|
try {
|
|
11054
|
-
if (
|
|
12048
|
+
if (existsSync21(file)) {
|
|
11055
12049
|
const existing = readFileSync13(file, "utf8").trim();
|
|
11056
12050
|
if (UUID_RE.test(existing))
|
|
11057
12051
|
return existing;
|
|
@@ -11251,9 +12245,9 @@ var init_events2 = __esm(() => {
|
|
|
11251
12245
|
});
|
|
11252
12246
|
|
|
11253
12247
|
// src/launcher/launcher.ts
|
|
11254
|
-
import { existsSync as
|
|
12248
|
+
import { existsSync as existsSync22 } from "fs";
|
|
11255
12249
|
import { join as join19 } from "path";
|
|
11256
|
-
import { and as
|
|
12250
|
+
import { and as and10, eq as eq14 } from "drizzle-orm";
|
|
11257
12251
|
function ptyExitMessage(exitCode) {
|
|
11258
12252
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
11259
12253
|
return `terminal exited (code ${label})`;
|
|
@@ -11273,7 +12267,7 @@ function renderCommand(runtime, spec) {
|
|
|
11273
12267
|
return [...env, runtime.binary, ...spec.argv].join(" ");
|
|
11274
12268
|
}
|
|
11275
12269
|
function activeRunFor(ctx, featureId) {
|
|
11276
|
-
const row = ctx.db.select().from(runs).where(
|
|
12270
|
+
const row = ctx.db.select().from(runs).where(and10(eq14(runs.featureId, featureId), eq14(runs.status, "running"))).limit(1).get();
|
|
11277
12271
|
return row ? rowToRun(row) : null;
|
|
11278
12272
|
}
|
|
11279
12273
|
function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
@@ -11637,7 +12631,7 @@ async function launchProjectSession(ctx, input, opts = {}) {
|
|
|
11637
12631
|
const model = resolveModelEntry("project", ctx.config, project);
|
|
11638
12632
|
const runtime = runtimeAdapterFor(model.runtime);
|
|
11639
12633
|
assertRuntimeReady(runtime, opts);
|
|
11640
|
-
const worktreePath = await ensureProjectWorktree(project, (res) => reportProjectLanding(ctx, project, res, { retried: true }));
|
|
12634
|
+
const { worktreePath, base } = await ensureProjectWorktree(project, (res) => reportProjectLanding(ctx, project, res, { retried: true }));
|
|
11641
12635
|
const session = createSessionRow(ctx, {
|
|
11642
12636
|
projectId: project.id,
|
|
11643
12637
|
kind: "project",
|
|
@@ -11690,7 +12684,7 @@ async function launchProjectSession(ctx, input, opts = {}) {
|
|
|
11690
12684
|
session,
|
|
11691
12685
|
project,
|
|
11692
12686
|
config: ctx.config,
|
|
11693
|
-
projectBrief: { project, branch: PROJECT_BRANCH, worktreePath },
|
|
12687
|
+
projectBrief: { project, branch: PROJECT_BRANCH, worktreePath, base },
|
|
11694
12688
|
worktreePath,
|
|
11695
12689
|
serverUrl: serverUrlFor(ctx.config),
|
|
11696
12690
|
model: model.id,
|
|
@@ -11724,7 +12718,7 @@ async function buildPrepareBrief(ctx, project) {
|
|
|
11724
12718
|
};
|
|
11725
12719
|
}
|
|
11726
12720
|
function probePrepareHost(repoPath) {
|
|
11727
|
-
const has = (rel) =>
|
|
12721
|
+
const has = (rel) => existsSync22(join19(repoPath, rel));
|
|
11728
12722
|
const lock = LOCKFILES.find(([file]) => has(file));
|
|
11729
12723
|
return {
|
|
11730
12724
|
platform: process.platform,
|
|
@@ -12074,10 +13068,10 @@ var init_feature = __esm(() => {
|
|
|
12074
13068
|
});
|
|
12075
13069
|
|
|
12076
13070
|
// src/services/fsbrowse.ts
|
|
12077
|
-
import { existsSync as
|
|
12078
|
-
import { homedir as
|
|
13071
|
+
import { existsSync as existsSync23, readdirSync as readdirSync6, statSync as statSync6 } from "fs";
|
|
13072
|
+
import { homedir as homedir3 } from "os";
|
|
12079
13073
|
import { basename as basename3, dirname as dirname11, isAbsolute as isAbsolute2, join as join21, resolve as resolve11, sep as sep4 } from "path";
|
|
12080
|
-
function expandPath(input, home =
|
|
13074
|
+
function expandPath(input, home = homedir3()) {
|
|
12081
13075
|
const trimmed = input.trim();
|
|
12082
13076
|
if (trimmed === "")
|
|
12083
13077
|
return trimmed;
|
|
@@ -12090,31 +13084,31 @@ function expandPath(input, home = homedir4()) {
|
|
|
12090
13084
|
}
|
|
12091
13085
|
function looksLikeRepo(dir) {
|
|
12092
13086
|
try {
|
|
12093
|
-
return
|
|
13087
|
+
return existsSync23(join21(dir, ".git"));
|
|
12094
13088
|
} catch {
|
|
12095
13089
|
return false;
|
|
12096
13090
|
}
|
|
12097
13091
|
}
|
|
12098
|
-
function listRoots(home =
|
|
13092
|
+
function listRoots(home = homedir3()) {
|
|
12099
13093
|
const roots = [];
|
|
12100
13094
|
if (process.platform === "win32") {
|
|
12101
13095
|
for (let code = 67;code <= 90; code++) {
|
|
12102
13096
|
const drive = `${String.fromCharCode(code)}:${sep4}`;
|
|
12103
13097
|
try {
|
|
12104
|
-
if (
|
|
13098
|
+
if (existsSync23(drive))
|
|
12105
13099
|
roots.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
|
|
12106
13100
|
} catch {}
|
|
12107
13101
|
}
|
|
12108
13102
|
} else {
|
|
12109
13103
|
roots.push({ label: "/", path: "/", kind: "drive" });
|
|
12110
13104
|
}
|
|
12111
|
-
if (
|
|
13105
|
+
if (existsSync23(home))
|
|
12112
13106
|
roots.push({ label: "Home", path: home, kind: "home" });
|
|
12113
13107
|
const seen = new Set(roots.map(pathKey));
|
|
12114
13108
|
for (const name of COMMON_DIRS) {
|
|
12115
13109
|
const candidate = join21(home, name);
|
|
12116
13110
|
const key = pathKey({ path: candidate });
|
|
12117
|
-
if (seen.has(key) || !
|
|
13111
|
+
if (seen.has(key) || !existsSync23(candidate))
|
|
12118
13112
|
continue;
|
|
12119
13113
|
seen.add(key);
|
|
12120
13114
|
roots.push({ label: name, path: candidate, kind: "common" });
|
|
@@ -12140,17 +13134,17 @@ function parentOf(dir) {
|
|
|
12140
13134
|
const parent = dirname11(dir);
|
|
12141
13135
|
return parent === dir ? null : parent;
|
|
12142
13136
|
}
|
|
12143
|
-
function browseDir(input, showHidden = false, home =
|
|
13137
|
+
function browseDir(input, showHidden = false, home = homedir3()) {
|
|
12144
13138
|
const target = input && input.trim() !== "" ? expandPath(input, home) : home;
|
|
12145
13139
|
if (!isAbsolute2(target)) {
|
|
12146
13140
|
throw new InvalidInputError(`path is not absolute: ${target}`);
|
|
12147
13141
|
}
|
|
12148
|
-
if (!
|
|
13142
|
+
if (!existsSync23(target)) {
|
|
12149
13143
|
throw new InvalidInputError(`path does not exist: ${target}`);
|
|
12150
13144
|
}
|
|
12151
13145
|
let dir = target;
|
|
12152
13146
|
try {
|
|
12153
|
-
if (!
|
|
13147
|
+
if (!statSync6(target).isDirectory())
|
|
12154
13148
|
dir = dirname11(target);
|
|
12155
13149
|
} catch (e) {
|
|
12156
13150
|
throw new InvalidInputError(`cannot read path: ${target} (${errMsg4(e)})`);
|
|
@@ -12180,7 +13174,7 @@ function browseDir(input, showHidden = false, home = homedir4()) {
|
|
|
12180
13174
|
let isDirectory = dirent.isDirectory();
|
|
12181
13175
|
if (isSymlink) {
|
|
12182
13176
|
try {
|
|
12183
|
-
isDirectory =
|
|
13177
|
+
isDirectory = statSync6(path).isDirectory();
|
|
12184
13178
|
} catch {
|
|
12185
13179
|
continue;
|
|
12186
13180
|
}
|
|
@@ -12370,9 +13364,9 @@ var init_conversations = __esm(() => {
|
|
|
12370
13364
|
});
|
|
12371
13365
|
|
|
12372
13366
|
// src/services/projects.ts
|
|
12373
|
-
import { existsSync as
|
|
13367
|
+
import { existsSync as existsSync24 } from "fs";
|
|
12374
13368
|
import { basename as basename4, join as join22 } from "path";
|
|
12375
|
-
import { eq as
|
|
13369
|
+
import { eq as eq15, isNull } from "drizzle-orm";
|
|
12376
13370
|
function listProjects(ctx) {
|
|
12377
13371
|
return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
|
|
12378
13372
|
}
|
|
@@ -12390,15 +13384,14 @@ function warnIfTranslatedMount(ctx, projectId, repoPath) {
|
|
|
12390
13384
|
async function openProject(ctx, rawPath) {
|
|
12391
13385
|
const repoPath = expandPath(rawPath);
|
|
12392
13386
|
await assertRepoTolerant(ctx, repoPath);
|
|
12393
|
-
const mainBranch = await detectMainBranchTolerant(ctx, repoPath);
|
|
12394
13387
|
const name = basename4(repoPath) || repoPath;
|
|
12395
13388
|
const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
|
|
12396
13389
|
if (existing) {
|
|
12397
|
-
ctx.db.update(projects).set({
|
|
12398
|
-
const reopened = { ...existing,
|
|
13390
|
+
ctx.db.update(projects).set({ repoPath, closedAt: null }).where(eq15(projects.id, existing.id)).run();
|
|
13391
|
+
const reopened = { ...existing, repoPath, closedAt: undefined };
|
|
12399
13392
|
emitProject(ctx, existing.id, {
|
|
12400
13393
|
type: "project.opened",
|
|
12401
|
-
message: `project ${existing.name} re-opened at ${repoPath}
|
|
13394
|
+
message: `project ${existing.name} re-opened at ${repoPath}`
|
|
12402
13395
|
});
|
|
12403
13396
|
warnIfTranslatedMount(ctx, existing.id, repoPath);
|
|
12404
13397
|
return reopened;
|
|
@@ -12407,7 +13400,6 @@ async function openProject(ctx, rawPath) {
|
|
|
12407
13400
|
id: newId("proj"),
|
|
12408
13401
|
name,
|
|
12409
13402
|
repoPath,
|
|
12410
|
-
mainBranch,
|
|
12411
13403
|
devCommand: null,
|
|
12412
13404
|
closedAt: null
|
|
12413
13405
|
};
|
|
@@ -12415,7 +13407,7 @@ async function openProject(ctx, rawPath) {
|
|
|
12415
13407
|
const project = rowToProject(inserted);
|
|
12416
13408
|
emitProject(ctx, project.id, {
|
|
12417
13409
|
type: "project.opened",
|
|
12418
|
-
message: `project ${name} at ${repoPath}
|
|
13410
|
+
message: `project ${name} at ${repoPath}`
|
|
12419
13411
|
});
|
|
12420
13412
|
warnIfTranslatedMount(ctx, project.id, repoPath);
|
|
12421
13413
|
return project;
|
|
@@ -12425,7 +13417,7 @@ function closeProject(ctx, projectId) {
|
|
|
12425
13417
|
if (projectHasActiveRun(ctx, projectId)) {
|
|
12426
13418
|
throw new InvalidInputError(`cannot close ${project.name}: a run is in flight \u2014 wait for it to finish or cancel it first`);
|
|
12427
13419
|
}
|
|
12428
|
-
ctx.db.update(projects).set({ closedAt: Date.now() }).where(
|
|
13420
|
+
ctx.db.update(projects).set({ closedAt: Date.now() }).where(eq15(projects.id, projectId)).run();
|
|
12429
13421
|
emitProject(ctx, projectId, {
|
|
12430
13422
|
type: "project.closed",
|
|
12431
13423
|
message: `project ${project.name} closed`
|
|
@@ -12434,7 +13426,7 @@ function closeProject(ctx, projectId) {
|
|
|
12434
13426
|
}
|
|
12435
13427
|
function renameProject(ctx, projectId, name) {
|
|
12436
13428
|
const project = requireProjectById(ctx, projectId);
|
|
12437
|
-
ctx.db.update(projects).set({ name }).where(
|
|
13429
|
+
ctx.db.update(projects).set({ name }).where(eq15(projects.id, projectId)).run();
|
|
12438
13430
|
emitProject(ctx, projectId, {
|
|
12439
13431
|
type: "project.renamed",
|
|
12440
13432
|
message: `project renamed ${project.name} \u2192 ${name}`,
|
|
@@ -12452,20 +13444,11 @@ async function assertRepoTolerant(ctx, repoPath) {
|
|
|
12452
13444
|
} catch (e) {
|
|
12453
13445
|
if (!isNotImplemented(e))
|
|
12454
13446
|
throw e;
|
|
12455
|
-
if (!
|
|
13447
|
+
if (!existsSync24(join22(repoPath, ".git"))) {
|
|
12456
13448
|
throw new InvalidInputError(`not a git repository: ${repoPath}`);
|
|
12457
13449
|
}
|
|
12458
13450
|
}
|
|
12459
13451
|
}
|
|
12460
|
-
async function detectMainBranchTolerant(ctx, repoPath) {
|
|
12461
|
-
try {
|
|
12462
|
-
return await detectMainBranch(repoPath);
|
|
12463
|
-
} catch (e) {
|
|
12464
|
-
if (!isNotImplemented(e))
|
|
12465
|
-
throw e;
|
|
12466
|
-
return ctx.config.mainBranch;
|
|
12467
|
-
}
|
|
12468
|
-
}
|
|
12469
13452
|
var init_projects = __esm(() => {
|
|
12470
13453
|
init_src();
|
|
12471
13454
|
init_schema();
|
|
@@ -12495,6 +13478,7 @@ var init_project = __esm(() => {
|
|
|
12495
13478
|
roots: publicProcedure.query(() => listRoots()),
|
|
12496
13479
|
browse: publicProcedure.input(z8.object({ dir: z8.string().optional(), showHidden: z8.boolean().optional() }).optional()).query(({ input }) => browseDir(input?.dir, input?.showHidden ?? false)),
|
|
12497
13480
|
branches: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => listBranches(requireProjectById(ctx, input.projectId))),
|
|
13481
|
+
sessionBranch: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => sessionBranchView(requireProjectById(ctx, input.projectId))),
|
|
12498
13482
|
open: publicProcedure.input(z8.object({ repoPath: z8.string().min(1) })).mutation(({ ctx, input }) => openProject(ctx, input.repoPath)),
|
|
12499
13483
|
close: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ ctx, input }) => closeProject(ctx, input.projectId)),
|
|
12500
13484
|
rename: publicProcedure.input(z8.object({ projectId: z8.string(), name: ProjectName })).mutation(({ ctx, input }) => renameProject(ctx, input.projectId, input.name)),
|
|
@@ -12513,8 +13497,26 @@ var init_project = __esm(() => {
|
|
|
12513
13497
|
});
|
|
12514
13498
|
});
|
|
12515
13499
|
|
|
12516
|
-
// src/trpc/routers/
|
|
13500
|
+
// src/trpc/routers/review-findings.ts
|
|
12517
13501
|
import * as z9 from "zod";
|
|
13502
|
+
var reviewFindingsRouter;
|
|
13503
|
+
var init_review_findings2 = __esm(() => {
|
|
13504
|
+
init_features();
|
|
13505
|
+
init_review_findings();
|
|
13506
|
+
init_context();
|
|
13507
|
+
reviewFindingsRouter = router({
|
|
13508
|
+
listByFeature: publicProcedure.input(z9.object({ featureId: z9.string() })).query(({ ctx, input }) => viewByFeature(ctx, input.featureId)),
|
|
13509
|
+
dismiss: publicProcedure.input(z9.object({ findingId: z9.string() })).mutation(({ ctx, input }) => dismiss(ctx, input.findingId)),
|
|
13510
|
+
fixOpenDefects: publicProcedure.input(z9.object({ featureId: z9.string() })).mutation(async ({ ctx, input }) => {
|
|
13511
|
+
const promoted = promoteOpenDefects(ctx, input.featureId);
|
|
13512
|
+
const { runId } = await burn(ctx, input.featureId);
|
|
13513
|
+
return { ...promoted, runId };
|
|
13514
|
+
})
|
|
13515
|
+
});
|
|
13516
|
+
});
|
|
13517
|
+
|
|
13518
|
+
// src/trpc/routers/run.ts
|
|
13519
|
+
import * as z10 from "zod";
|
|
12518
13520
|
var runRouter;
|
|
12519
13521
|
var init_run = __esm(() => {
|
|
12520
13522
|
init_agent_stream();
|
|
@@ -12522,9 +13524,9 @@ var init_run = __esm(() => {
|
|
|
12522
13524
|
init_runner();
|
|
12523
13525
|
init_context();
|
|
12524
13526
|
runRouter = router({
|
|
12525
|
-
get: publicProcedure.input(
|
|
12526
|
-
agentTranscript: publicProcedure.input(
|
|
12527
|
-
cancel: publicProcedure.input(
|
|
13527
|
+
get: publicProcedure.input(z10.object({ runId: z10.string() })).query(({ ctx, input }) => getRunRow(ctx, input.runId)),
|
|
13528
|
+
agentTranscript: publicProcedure.input(z10.object({ ticketId: z10.string(), after: z10.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
|
|
13529
|
+
cancel: publicProcedure.input(z10.object({ runId: z10.string() })).mutation(({ ctx, input }) => {
|
|
12528
13530
|
getRunRow(ctx, input.runId);
|
|
12529
13531
|
cancelRun(input.runId);
|
|
12530
13532
|
return { ok: true };
|
|
@@ -12533,47 +13535,21 @@ var init_run = __esm(() => {
|
|
|
12533
13535
|
});
|
|
12534
13536
|
|
|
12535
13537
|
// src/trpc/routers/settings.ts
|
|
12536
|
-
import * as
|
|
13538
|
+
import * as z11 from "zod";
|
|
12537
13539
|
var settingsRouter;
|
|
12538
13540
|
var init_settings2 = __esm(() => {
|
|
12539
13541
|
init_src();
|
|
12540
13542
|
init_settings();
|
|
12541
13543
|
init_context();
|
|
12542
13544
|
settingsRouter = router({
|
|
12543
|
-
get: publicProcedure.input(
|
|
13545
|
+
get: publicProcedure.input(z11.object({ projectId: z11.string().optional() }).optional()).query(({ ctx, input }) => getSettings(ctx, input?.projectId)),
|
|
12544
13546
|
update: publicProcedure.input(SettingsUpdateInput).mutation(({ ctx, input }) => updateSettings(ctx, input))
|
|
12545
13547
|
});
|
|
12546
13548
|
});
|
|
12547
13549
|
|
|
12548
|
-
// src/doctor/system-exec.ts
|
|
12549
|
-
import { spawn as spawn4 } from "child_process";
|
|
12550
|
-
function createSystemExec(opts = {}) {
|
|
12551
|
-
return (command, args) => new Promise((resolve12) => {
|
|
12552
|
-
const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
|
|
12553
|
-
let stdout = "";
|
|
12554
|
-
let stderr = "";
|
|
12555
|
-
const child = spawn4(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
|
|
12556
|
-
child.stdout?.on("data", (d) => {
|
|
12557
|
-
stdout += d.toString();
|
|
12558
|
-
});
|
|
12559
|
-
child.stderr?.on("data", (d) => {
|
|
12560
|
-
stderr += d.toString();
|
|
12561
|
-
});
|
|
12562
|
-
child.on("error", (err) => {
|
|
12563
|
-
resolve12({ ok: false, code: null, stdout, stderr: stderr || String(err) });
|
|
12564
|
-
});
|
|
12565
|
-
child.on("close", (code) => {
|
|
12566
|
-
resolve12({ ok: true, code: code ?? 0, stdout, stderr });
|
|
12567
|
-
});
|
|
12568
|
-
});
|
|
12569
|
-
}
|
|
12570
|
-
var init_system_exec = __esm(() => {
|
|
12571
|
-
init_resolve_executable();
|
|
12572
|
-
});
|
|
12573
|
-
|
|
12574
13550
|
// src/trpc/routers/setup.ts
|
|
12575
|
-
import { isNotNull as
|
|
12576
|
-
import * as
|
|
13551
|
+
import { isNotNull as isNotNull3 } from "drizzle-orm";
|
|
13552
|
+
import * as z12 from "zod";
|
|
12577
13553
|
var setupRouter;
|
|
12578
13554
|
var init_setup2 = __esm(() => {
|
|
12579
13555
|
init_src();
|
|
@@ -12586,7 +13562,7 @@ var init_setup2 = __esm(() => {
|
|
|
12586
13562
|
init_context();
|
|
12587
13563
|
setupRouter = router({
|
|
12588
13564
|
doctor: publicProcedure.query(({ ctx }) => {
|
|
12589
|
-
const projectModels = ctx.db.select({ model: projects.model }).from(projects).where(
|
|
13565
|
+
const projectModels = ctx.db.select({ model: projects.model }).from(projects).where(isNotNull3(projects.model)).all().map((p) => p.model);
|
|
12590
13566
|
return runDoctor({
|
|
12591
13567
|
exec: createSystemExec(),
|
|
12592
13568
|
runtimes: configuredRuntimes(ctx.config, projectModels),
|
|
@@ -12594,10 +13570,10 @@ var init_setup2 = __esm(() => {
|
|
|
12594
13570
|
});
|
|
12595
13571
|
}),
|
|
12596
13572
|
runtimeGuide: publicProcedure.query(() => runtimeInstallGuide(process.platform)),
|
|
12597
|
-
gitIdentity: publicProcedure.input(
|
|
12598
|
-
afkToken: publicProcedure.input(
|
|
12599
|
-
seedModelDefaults: publicProcedure.input(
|
|
12600
|
-
startTerminal: publicProcedure.input(
|
|
13573
|
+
gitIdentity: publicProcedure.input(z12.object({ name: z12.string(), email: z12.string() })).mutation(({ input }) => writeGitIdentity(createSystemExec(), input)),
|
|
13574
|
+
afkToken: publicProcedure.input(z12.object({ token: z12.string(), runtime: AgentRuntime.default("claude-code") })).mutation(({ input }) => saveAfkCredential(fileAfkTokenIo(createSystemExec(), input.runtime), input.token, input.runtime)),
|
|
13575
|
+
seedModelDefaults: publicProcedure.input(z12.object({ runtimes: z12.array(AgentRuntime) })).mutation(({ ctx, input }) => seedModelDefaults(ctx, input.runtimes)),
|
|
13576
|
+
startTerminal: publicProcedure.input(z12.object({ kind: z12.enum(["setup-token", "build-image", "claude-login", "codex-login"]) })).mutation(async ({ ctx, input }) => {
|
|
12601
13577
|
const exec = createSystemExec();
|
|
12602
13578
|
const preferred = ctx.config.sandbox === "podman" ? "podman" : "docker";
|
|
12603
13579
|
const runtime = await resolveRuntime(exec, preferred);
|
|
@@ -12619,40 +13595,80 @@ var init_setup2 = __esm(() => {
|
|
|
12619
13595
|
});
|
|
12620
13596
|
|
|
12621
13597
|
// src/trpc/routers/system.ts
|
|
12622
|
-
|
|
13598
|
+
import * as z13 from "zod";
|
|
13599
|
+
function burnCacheEngine(sandbox) {
|
|
13600
|
+
return sandbox === "docker" || sandbox === "podman" ? sandbox : null;
|
|
13601
|
+
}
|
|
13602
|
+
var projectInput, systemRouter;
|
|
12623
13603
|
var init_system = __esm(() => {
|
|
13604
|
+
init_src();
|
|
13605
|
+
init_system_exec();
|
|
13606
|
+
init_errors();
|
|
13607
|
+
init_events();
|
|
12624
13608
|
init_update_check();
|
|
13609
|
+
init_burn_cache();
|
|
12625
13610
|
init_version();
|
|
12626
13611
|
init_context();
|
|
13612
|
+
projectInput = z13.object({ projectId: z13.string() });
|
|
12627
13613
|
systemRouter = router({
|
|
12628
13614
|
version: publicProcedure.query(() => ({ version: runcastleVersion() })),
|
|
12629
|
-
checkUpdate: publicProcedure.query(() => getUpdateInfo(runcastleVersion()))
|
|
13615
|
+
checkUpdate: publicProcedure.query(() => getUpdateInfo(runcastleVersion())),
|
|
13616
|
+
burnCache: router({
|
|
13617
|
+
status: publicProcedure.input(projectInput).query(async ({ ctx, input }) => {
|
|
13618
|
+
const mode = resolveBurnCacheMode(ctx.config);
|
|
13619
|
+
const engine = burnCacheEngine(ctx.config.sandbox);
|
|
13620
|
+
const sizeBytes = mode === "volume" && engine ? await burnCacheVolumeSize({
|
|
13621
|
+
engine,
|
|
13622
|
+
projectId: input.projectId,
|
|
13623
|
+
exec: createSystemExec()
|
|
13624
|
+
}) : null;
|
|
13625
|
+
return { mode, engine, volumeName: burnCacheVolumeName(input.projectId), sizeBytes };
|
|
13626
|
+
}),
|
|
13627
|
+
clear: publicProcedure.input(projectInput).mutation(async ({ ctx, input }) => {
|
|
13628
|
+
const engine = burnCacheEngine(ctx.config.sandbox);
|
|
13629
|
+
if (!engine) {
|
|
13630
|
+
throw new InvalidInputError(`the burn cache needs a docker or podman sandbox; this one is "${ctx.config.sandbox}"`);
|
|
13631
|
+
}
|
|
13632
|
+
const volumeName = burnCacheVolumeName(input.projectId);
|
|
13633
|
+
await removeBurnCacheVolume({
|
|
13634
|
+
engine,
|
|
13635
|
+
projectId: input.projectId,
|
|
13636
|
+
exec: createSystemExec(),
|
|
13637
|
+
slots: getBurnSlotAllocator(ctx.config.burnConcurrency).held()
|
|
13638
|
+
});
|
|
13639
|
+
emitProject(ctx, input.projectId, {
|
|
13640
|
+
type: "burn-cache.cleared",
|
|
13641
|
+
message: `cleared the burn cache volume ${volumeName}`
|
|
13642
|
+
});
|
|
13643
|
+
return { volumeName };
|
|
13644
|
+
})
|
|
13645
|
+
})
|
|
12630
13646
|
});
|
|
12631
13647
|
});
|
|
12632
13648
|
|
|
12633
13649
|
// src/trpc/routers/test-notes.ts
|
|
12634
|
-
import * as
|
|
13650
|
+
import * as z14 from "zod";
|
|
12635
13651
|
var testNotesRouter;
|
|
12636
13652
|
var init_test_notes2 = __esm(() => {
|
|
12637
13653
|
init_test_notes();
|
|
12638
13654
|
init_context();
|
|
12639
13655
|
testNotesRouter = router({
|
|
12640
|
-
list: publicProcedure.input(
|
|
12641
|
-
add: publicProcedure.input(
|
|
12642
|
-
featureId:
|
|
12643
|
-
text:
|
|
12644
|
-
videoTimestamp:
|
|
13656
|
+
list: publicProcedure.input(z14.object({ featureId: z14.string() })).query(({ ctx, input }) => listByFeature4(ctx, input.featureId)),
|
|
13657
|
+
add: publicProcedure.input(z14.object({
|
|
13658
|
+
featureId: z14.string(),
|
|
13659
|
+
text: z14.string().min(1),
|
|
13660
|
+
videoTimestamp: z14.number().nonnegative().optional()
|
|
12645
13661
|
})).mutation(({ ctx, input }) => addNote(ctx, input.featureId, input.text, "human", input.videoTimestamp)),
|
|
12646
|
-
edit: publicProcedure.input(
|
|
12647
|
-
remove: publicProcedure.input(
|
|
12648
|
-
toggle: publicProcedure.input(
|
|
12649
|
-
promote: publicProcedure.input(
|
|
12650
|
-
promoteMany: publicProcedure.input(
|
|
13662
|
+
edit: publicProcedure.input(z14.object({ noteId: z14.string(), text: z14.string().min(1) })).mutation(({ ctx, input }) => editNote(ctx, input.noteId, input.text)),
|
|
13663
|
+
remove: publicProcedure.input(z14.object({ noteId: z14.string() })).mutation(({ ctx, input }) => deleteNote(ctx, input.noteId)),
|
|
13664
|
+
toggle: publicProcedure.input(z14.object({ noteId: z14.string() })).mutation(({ ctx, input }) => toggleNote(ctx, input.noteId)),
|
|
13665
|
+
promote: publicProcedure.input(z14.object({ noteId: z14.string() })).mutation(({ ctx, input }) => promoteNote(ctx, input.noteId)),
|
|
13666
|
+
promoteMany: publicProcedure.input(z14.object({ noteIds: z14.array(z14.string()).min(1) })).mutation(({ ctx, input }) => promoteMany(ctx, input.noteIds))
|
|
12651
13667
|
});
|
|
12652
13668
|
});
|
|
12653
13669
|
|
|
12654
13670
|
// src/trpc/routers/ticket.ts
|
|
12655
|
-
import * as
|
|
13671
|
+
import * as z15 from "zod";
|
|
12656
13672
|
var ticketRouter;
|
|
12657
13673
|
var init_ticket = __esm(() => {
|
|
12658
13674
|
init_features();
|
|
@@ -12661,8 +13677,8 @@ var init_ticket = __esm(() => {
|
|
|
12661
13677
|
init_ticket_burner();
|
|
12662
13678
|
init_context();
|
|
12663
13679
|
ticketRouter = router({
|
|
12664
|
-
retry: publicProcedure.input(
|
|
12665
|
-
stop: publicProcedure.input(
|
|
13680
|
+
retry: publicProcedure.input(z15.object({ ticketId: z15.string(), fresh: z15.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
|
|
13681
|
+
stop: publicProcedure.input(z15.object({ ticketId: z15.string() })).mutation(({ ctx, input }) => {
|
|
12666
13682
|
if (stopTicketRun(input.ticketId))
|
|
12667
13683
|
return { stopped: true, swept: false };
|
|
12668
13684
|
const ticket = getTicket(ctx, input.ticketId);
|
|
@@ -12672,14 +13688,14 @@ var init_ticket = __esm(() => {
|
|
|
12672
13688
|
}
|
|
12673
13689
|
return { stopped: false, swept: false };
|
|
12674
13690
|
}),
|
|
12675
|
-
cancel: publicProcedure.input(
|
|
12676
|
-
edit: publicProcedure.input(
|
|
12677
|
-
ticketId:
|
|
12678
|
-
title:
|
|
12679
|
-
goal:
|
|
12680
|
-
context:
|
|
12681
|
-
acceptanceCriteria:
|
|
12682
|
-
model:
|
|
13691
|
+
cancel: publicProcedure.input(z15.object({ ticketId: z15.string(), reason: z15.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason)),
|
|
13692
|
+
edit: publicProcedure.input(z15.object({
|
|
13693
|
+
ticketId: z15.string(),
|
|
13694
|
+
title: z15.string().min(1).optional(),
|
|
13695
|
+
goal: z15.string().min(1).optional(),
|
|
13696
|
+
context: z15.string().optional(),
|
|
13697
|
+
acceptanceCriteria: z15.array(z15.string().min(1)).optional(),
|
|
13698
|
+
model: z15.string().optional()
|
|
12683
13699
|
})).mutation(({ ctx, input }) => {
|
|
12684
13700
|
const { ticketId, ...patch } = input;
|
|
12685
13701
|
return editTicket(ctx, ticketId, patch);
|
|
@@ -12695,6 +13711,7 @@ var init_router = __esm(() => {
|
|
|
12695
13711
|
init_events2();
|
|
12696
13712
|
init_feature();
|
|
12697
13713
|
init_project();
|
|
13714
|
+
init_review_findings2();
|
|
12698
13715
|
init_run();
|
|
12699
13716
|
init_settings2();
|
|
12700
13717
|
init_setup2();
|
|
@@ -12707,6 +13724,7 @@ var init_router = __esm(() => {
|
|
|
12707
13724
|
run: runRouter,
|
|
12708
13725
|
ticket: ticketRouter,
|
|
12709
13726
|
notes: testNotesRouter,
|
|
13727
|
+
findings: reviewFindingsRouter,
|
|
12710
13728
|
events: eventsRouter,
|
|
12711
13729
|
docs: docsRouter,
|
|
12712
13730
|
settings: settingsRouter,
|
|
@@ -12716,14 +13734,14 @@ var init_router = __esm(() => {
|
|
|
12716
13734
|
});
|
|
12717
13735
|
|
|
12718
13736
|
// src/workflows/reconcile-runs.ts
|
|
12719
|
-
import { eq as
|
|
13737
|
+
import { eq as eq16 } from "drizzle-orm";
|
|
12720
13738
|
async function reconcileStaleRuns(ctx) {
|
|
12721
|
-
const stale = ctx.db.select().from(runs).where(
|
|
13739
|
+
const stale = ctx.db.select().from(runs).where(eq16(runs.status, "running")).all().map(rowToRun);
|
|
12722
13740
|
const reconciled = [];
|
|
12723
13741
|
for (const run4 of stale) {
|
|
12724
13742
|
if (isRunActive(run4.id))
|
|
12725
13743
|
continue;
|
|
12726
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(
|
|
13744
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq16(runs.id, run4.id)).run();
|
|
12727
13745
|
const released = releaseForSession(ctx, run4.id);
|
|
12728
13746
|
const swept = workflowClaimsFeatureBranch(run4.workflow) ? sweepOrphanedBurning(ctx, run4.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
|
|
12729
13747
|
if (workflowClaimsFeatureBranch(run4.workflow)) {
|