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