runcastle 1.3.4 → 1.3.5
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 +1074 -411
- package/drizzle/0037_finding_carry_and_provenance.sql +5 -0
- package/drizzle/0037_project_sandbox_image.sql +1 -0
- package/index.js +1070 -409
- package/package.json +1 -1
- package/skills/burner/review-ticket.md +11 -5
- package/skills/packs/runcastle/skills/prepare/SKILL.md +35 -1
- package/skills/packs/runcastle/skills/revisit/SKILL.md +10 -2
- package/skills/packs/runcastle/skills/tickets/SKILL.md +1 -1
- package/web/assets/index-Vbh17gX3.js +186 -0
- package/web/index.html +1 -1
- package/web/assets/index-CgCA-f62.js +0 -186
package/index.js
CHANGED
|
@@ -52,8 +52,8 @@ var init_docs = __esm(() => {
|
|
|
52
52
|
|
|
53
53
|
// ../core/src/config.ts
|
|
54
54
|
import * as z from "zod";
|
|
55
|
-
function resolveSandboxImage(config) {
|
|
56
|
-
return config.sandboxImage
|
|
55
|
+
function resolveSandboxImage(config, project) {
|
|
56
|
+
return project?.sandboxImage?.trim() || config.sandboxImage?.trim() || DEFAULT_SANDBOX_IMAGE;
|
|
57
57
|
}
|
|
58
58
|
function resolveBurnCacheMode(config) {
|
|
59
59
|
return BURN_CACHE_SANDBOXES.includes(config.sandbox) ? config.burnCache : "off";
|
|
@@ -181,6 +181,14 @@ import * as z2 from "zod";
|
|
|
181
181
|
function isProjectSessionKind(kind) {
|
|
182
182
|
return PROJECT_SESSION_KINDS.includes(kind);
|
|
183
183
|
}
|
|
184
|
+
function driveDenialOf(result) {
|
|
185
|
+
return {
|
|
186
|
+
...result.deniedReason ? { deniedReason: result.deniedReason } : {},
|
|
187
|
+
...result.deniedCode ? { deniedCode: result.deniedCode } : {},
|
|
188
|
+
...result.retriable !== undefined ? { retriable: result.retriable } : {},
|
|
189
|
+
...result.dirtyFiles ? { dirtyFiles: result.dirtyFiles } : {}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
184
192
|
function unresolvedMergeConflict(events) {
|
|
185
193
|
let conflict = null;
|
|
186
194
|
for (const event of events) {
|
|
@@ -197,7 +205,7 @@ function unresolvedMergeConflict(events) {
|
|
|
197
205
|
}
|
|
198
206
|
return conflict;
|
|
199
207
|
}
|
|
200
|
-
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, DriveState;
|
|
208
|
+
var Phase, TicketStatus, SessionKind, PROJECT_SESSION_KINDS, SessionPurpose, MergeBranchPair, RunStatus, FeatureStatus, SessionStatus, TicketKind, FindingKind, FindingSeverity, FindingStatus, FindingOpenReason, FindingResolvedBy, ReviewFindingInput, ReviewFinding, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNoteAuthor, TestNote, PREPARED_KEYS, PreparedKey, PROVENANCE_KEYS, ProvenanceKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow, DriveState;
|
|
201
209
|
var init_schemas = __esm(() => {
|
|
202
210
|
init_config();
|
|
203
211
|
Phase = z2.enum([
|
|
@@ -231,8 +239,9 @@ var init_schemas = __esm(() => {
|
|
|
231
239
|
TicketKind = z2.enum(["implementation", "review"]);
|
|
232
240
|
FindingKind = z2.enum(["defect", "observation"]);
|
|
233
241
|
FindingSeverity = z2.enum(["high", "medium", "low"]);
|
|
234
|
-
FindingStatus = z2.enum(["open", "fixing", "fixed", "failed", "dismissed"]);
|
|
242
|
+
FindingStatus = z2.enum(["open", "fixing", "fixed", "failed", "dismissed", "carried"]);
|
|
235
243
|
FindingOpenReason = z2.enum(["over-cap", "fix-failed", "verification"]);
|
|
244
|
+
FindingResolvedBy = z2.enum(["fix-ticket", "session"]);
|
|
236
245
|
ReviewFindingInput = z2.object({
|
|
237
246
|
kind: FindingKind,
|
|
238
247
|
severity: FindingSeverity,
|
|
@@ -256,6 +265,9 @@ var init_schemas = __esm(() => {
|
|
|
256
265
|
openReason: FindingOpenReason.nullable(),
|
|
257
266
|
failureReason: z2.string().nullable(),
|
|
258
267
|
fixTicketId: z2.string().nullable(),
|
|
268
|
+
carriedLap: z2.number().nullable(),
|
|
269
|
+
resolvedBy: FindingResolvedBy.nullable(),
|
|
270
|
+
resolutionNote: z2.string().nullable(),
|
|
259
271
|
createdAt: z2.number()
|
|
260
272
|
});
|
|
261
273
|
TicketInput = z2.object({
|
|
@@ -332,12 +344,14 @@ var init_schemas = __esm(() => {
|
|
|
332
344
|
"driveInstructions"
|
|
333
345
|
];
|
|
334
346
|
PreparedKey = z2.enum(PREPARED_KEYS);
|
|
347
|
+
PROVENANCE_KEYS = [...PREPARED_KEYS, "sandboxImage"];
|
|
348
|
+
ProvenanceKey = z2.enum(PROVENANCE_KEYS);
|
|
335
349
|
DRIVE_LOOP_KEYS = [
|
|
336
350
|
"devCommand",
|
|
337
351
|
"driveSetupCommand",
|
|
338
352
|
"driveStopCommand"
|
|
339
353
|
];
|
|
340
|
-
FindingSource = z2.enum(["prep", "human", "session"]);
|
|
354
|
+
FindingSource = z2.enum(["prep", "human", "session", "build"]);
|
|
341
355
|
ProjectName = z2.string().min(1, "a project needs a name").max(PROJECT_NAME_MAX, `a project name can be at most ${PROJECT_NAME_MAX} characters`);
|
|
342
356
|
Project = z2.object({
|
|
343
357
|
id: z2.string(),
|
|
@@ -346,6 +360,7 @@ var init_schemas = __esm(() => {
|
|
|
346
360
|
sessionBranch: z2.string().optional(),
|
|
347
361
|
devCommand: z2.string().optional(),
|
|
348
362
|
model: z2.string().optional(),
|
|
363
|
+
sandboxImage: z2.string().optional(),
|
|
349
364
|
setupCommand: z2.string().optional(),
|
|
350
365
|
verifyCommands: z2.string().optional(),
|
|
351
366
|
knownFailures: z2.string().optional(),
|
|
@@ -356,7 +371,7 @@ var init_schemas = __esm(() => {
|
|
|
356
371
|
closedAt: z2.number().optional()
|
|
357
372
|
});
|
|
358
373
|
ProjectFinding = z2.object({
|
|
359
|
-
key:
|
|
374
|
+
key: ProvenanceKey,
|
|
360
375
|
source: FindingSource,
|
|
361
376
|
evidence: z2.string().optional(),
|
|
362
377
|
establishedAt: z2.number(),
|
|
@@ -644,6 +659,7 @@ var init_db_schema = __esm(() => {
|
|
|
644
659
|
devCommand: text("dev_command"),
|
|
645
660
|
model: text("model"),
|
|
646
661
|
sandbox: text("sandbox"),
|
|
662
|
+
sandboxImage: text("sandbox_image"),
|
|
647
663
|
closedAt: integer("closed_at"),
|
|
648
664
|
setupCommand: text("setup_command"),
|
|
649
665
|
verifyCommands: text("verify_commands"),
|
|
@@ -738,6 +754,9 @@ var init_db_schema = __esm(() => {
|
|
|
738
754
|
openReason: text("open_reason").$type(),
|
|
739
755
|
failureReason: text("failure_reason"),
|
|
740
756
|
fixTicketId: text("fix_ticket_id"),
|
|
757
|
+
carriedLap: integer("carried_lap"),
|
|
758
|
+
resolvedBy: text("resolved_by").$type(),
|
|
759
|
+
resolutionNote: text("resolution_note"),
|
|
741
760
|
createdAt: integer("created_at").notNull()
|
|
742
761
|
});
|
|
743
762
|
testNotes = sqliteTable("test_notes", {
|
|
@@ -2129,6 +2148,7 @@ function rowToProject(row) {
|
|
|
2129
2148
|
sessionBranch: row.sessionBranch ?? undefined,
|
|
2130
2149
|
devCommand: row.devCommand ?? undefined,
|
|
2131
2150
|
model: row.model ?? undefined,
|
|
2151
|
+
sandboxImage: row.sandboxImage ?? undefined,
|
|
2132
2152
|
setupCommand: row.setupCommand ?? undefined,
|
|
2133
2153
|
verifyCommands: row.verifyCommands ?? undefined,
|
|
2134
2154
|
knownFailures: row.knownFailures ?? undefined,
|
|
@@ -2797,8 +2817,8 @@ var init_drive_env = () => {};
|
|
|
2797
2817
|
|
|
2798
2818
|
// src/services/findings.ts
|
|
2799
2819
|
import { and as and4, eq as eq4 } from "drizzle-orm";
|
|
2800
|
-
function
|
|
2801
|
-
return
|
|
2820
|
+
function isProvenanceKey(key) {
|
|
2821
|
+
return PROVENANCE_SET.has(key);
|
|
2802
2822
|
}
|
|
2803
2823
|
function findingRow(ctx, projectId, key) {
|
|
2804
2824
|
return ctx.db.select({
|
|
@@ -2840,7 +2860,7 @@ function recordFinding(ctx, projectId, input) {
|
|
|
2840
2860
|
});
|
|
2841
2861
|
}
|
|
2842
2862
|
function recordHuman(ctx, projectId, key, value) {
|
|
2843
|
-
if (!
|
|
2863
|
+
if (!isProvenanceKey(key))
|
|
2844
2864
|
return;
|
|
2845
2865
|
const existing = findingRow(ctx, projectId, key);
|
|
2846
2866
|
if (value === null) {
|
|
@@ -2884,8 +2904,8 @@ async function listFindings(ctx, project) {
|
|
|
2884
2904
|
distances.set(sha, await commitsSince(project.repoPath, sha, mainLine));
|
|
2885
2905
|
}
|
|
2886
2906
|
}
|
|
2887
|
-
const order = new Map(
|
|
2888
|
-
return rows.filter((row) =>
|
|
2907
|
+
const order = new Map(PROVENANCE_KEYS.map((k, i) => [k, i]));
|
|
2908
|
+
return rows.filter((row) => isProvenanceKey(row.key)).sort((a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0)).map((row) => {
|
|
2889
2909
|
const staleCommits = row.establishedSha ? distances.get(row.establishedSha) : undefined;
|
|
2890
2910
|
return {
|
|
2891
2911
|
key: row.key,
|
|
@@ -2905,7 +2925,7 @@ function unsetPreparedKeys(project) {
|
|
|
2905
2925
|
return value === undefined || value.trim() === "";
|
|
2906
2926
|
});
|
|
2907
2927
|
}
|
|
2908
|
-
var VALUE_COLUMN, COLUMN_NAME, PREPARED_SET, UNVERIFIED;
|
|
2928
|
+
var VALUE_COLUMN, COLUMN_NAME, PREPARED_SET, PROVENANCE_SET, UNVERIFIED;
|
|
2909
2929
|
var init_findings = __esm(() => {
|
|
2910
2930
|
init_src();
|
|
2911
2931
|
init_schema();
|
|
@@ -2918,7 +2938,8 @@ var init_findings = __esm(() => {
|
|
|
2918
2938
|
driveSetupCommand: projects.driveSetupCommand,
|
|
2919
2939
|
driveStopCommand: projects.driveStopCommand,
|
|
2920
2940
|
dbResetCommand: projects.dbResetCommand,
|
|
2921
|
-
driveInstructions: projects.driveInstructions
|
|
2941
|
+
driveInstructions: projects.driveInstructions,
|
|
2942
|
+
sandboxImage: projects.sandboxImage
|
|
2922
2943
|
};
|
|
2923
2944
|
COLUMN_NAME = {
|
|
2924
2945
|
setupCommand: "setupCommand",
|
|
@@ -2928,9 +2949,11 @@ var init_findings = __esm(() => {
|
|
|
2928
2949
|
driveSetupCommand: "driveSetupCommand",
|
|
2929
2950
|
driveStopCommand: "driveStopCommand",
|
|
2930
2951
|
dbResetCommand: "dbResetCommand",
|
|
2931
|
-
driveInstructions: "driveInstructions"
|
|
2952
|
+
driveInstructions: "driveInstructions",
|
|
2953
|
+
sandboxImage: "sandboxImage"
|
|
2932
2954
|
};
|
|
2933
2955
|
PREPARED_SET = new Set(PREPARED_KEYS);
|
|
2956
|
+
PROVENANCE_SET = new Set(PROVENANCE_KEYS);
|
|
2934
2957
|
UNVERIFIED = { verifiedAt: null, verifiedSha: null };
|
|
2935
2958
|
});
|
|
2936
2959
|
|
|
@@ -2963,6 +2986,18 @@ function deriveDriveState(info) {
|
|
|
2963
2986
|
function migrationPaths(paths) {
|
|
2964
2987
|
return paths.filter((p) => MIGRATION_DIR_RE.test(p) || DRIZZLE_SQL_RE.test(p));
|
|
2965
2988
|
}
|
|
2989
|
+
function deniedStart(deniedReason, deniedCode, dirtyFiles) {
|
|
2990
|
+
return {
|
|
2991
|
+
ok: false,
|
|
2992
|
+
deniedReason,
|
|
2993
|
+
deniedCode,
|
|
2994
|
+
retriable: deniedCode === "slot_held",
|
|
2995
|
+
...dirtyFiles ? { dirtyFiles } : {}
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
function truncatedFileList(files) {
|
|
2999
|
+
return `${files.slice(0, 5).join(", ")}${files.length > 5 ? ", \u2026" : ""}`;
|
|
3000
|
+
}
|
|
2966
3001
|
function featureBranch(slug) {
|
|
2967
3002
|
return `feature/${slug}`;
|
|
2968
3003
|
}
|
|
@@ -3692,6 +3727,10 @@ async function commitPipelineDocs(repoPath) {
|
|
|
3692
3727
|
await commitDocs(repoPath, "runcastle: docs the pipeline left uncommitted");
|
|
3693
3728
|
} catch {}
|
|
3694
3729
|
}
|
|
3730
|
+
async function driveBlockingPaths(repoPath) {
|
|
3731
|
+
await commitPipelineDocs(repoPath);
|
|
3732
|
+
return porcelainPaths((await git(repoPath).raw(["status", "--porcelain"])).trim());
|
|
3733
|
+
}
|
|
3695
3734
|
function activeTestDriveFeatureId() {
|
|
3696
3735
|
return testDriveState?.kind === "feature" ? testDriveState.featureId : undefined;
|
|
3697
3736
|
}
|
|
@@ -3779,7 +3818,7 @@ async function testDrive(ctx, project, feature, action, purpose = "human") {
|
|
|
3779
3818
|
if (carriedChanges.length > 0) {
|
|
3780
3819
|
emit(ctx, feature.id, {
|
|
3781
3820
|
type: "testdrive.carried_changes",
|
|
3782
|
-
message: `${carriedChanges.length} uncommitted file(s) came back with you onto ${previousBranch2}: ${carriedChanges
|
|
3821
|
+
message: `${carriedChanges.length} uncommitted file(s) came back with you onto ${previousBranch2}: ${truncatedFileList(carriedChanges)}`,
|
|
3783
3822
|
data: { branch: previousBranch2, files: carriedChanges }
|
|
3784
3823
|
});
|
|
3785
3824
|
}
|
|
@@ -3792,18 +3831,22 @@ async function testDrive(ctx, project, feature, action, purpose = "human") {
|
|
|
3792
3831
|
...teardown?.failure ? { hookFailure: teardown.failure } : {}
|
|
3793
3832
|
};
|
|
3794
3833
|
}
|
|
3795
|
-
await
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3834
|
+
const dirtyFiles = await driveBlockingPaths(project.repoPath);
|
|
3835
|
+
if (dirtyFiles.length > 0) {
|
|
3836
|
+
if (purpose === "review") {
|
|
3837
|
+
emit(ctx, feature.id, {
|
|
3838
|
+
type: "reviewdrive.denied",
|
|
3839
|
+
message: `review drive denied \u2014 ${dirtyFiles.length} uncommitted file(s) in the working tree: ${truncatedFileList(dirtyFiles)}`,
|
|
3840
|
+
data: { code: "dirty", dirtyFiles }
|
|
3841
|
+
});
|
|
3842
|
+
}
|
|
3843
|
+
return deniedStart(DENY_DIRTY, "dirty", dirtyFiles);
|
|
3844
|
+
}
|
|
3799
3845
|
if (testDriveState) {
|
|
3800
|
-
return
|
|
3801
|
-
ok: false,
|
|
3802
|
-
deniedReason: testDriveState.kind === "dryRun" ? DENY_DRY_RUN_ACTIVE : DENY_ACTIVE
|
|
3803
|
-
};
|
|
3846
|
+
return deniedStart(testDriveState.kind === "dryRun" ? DENY_DRY_RUN_ACTIVE : DENY_ACTIVE, "slot_held");
|
|
3804
3847
|
}
|
|
3805
3848
|
if (purpose === "human" && hasActiveRun(ctx, feature.id)) {
|
|
3806
|
-
return
|
|
3849
|
+
return deniedStart(DENY_ACTIVE_RUN, "active_run");
|
|
3807
3850
|
}
|
|
3808
3851
|
const talkWorktree = worktreeDir(project.id, feature.slug);
|
|
3809
3852
|
let detachedWorktree;
|
|
@@ -3863,7 +3906,7 @@ async function reviewDrive(ctx, project, feature, action) {
|
|
|
3863
3906
|
return {
|
|
3864
3907
|
ok: stop.ok,
|
|
3865
3908
|
action: "stop",
|
|
3866
|
-
...stop
|
|
3909
|
+
...driveDenialOf(stop),
|
|
3867
3910
|
drive: activeDriveInfo(),
|
|
3868
3911
|
...stop.hookFailure ? { hookFailure: stop.hookFailure } : {}
|
|
3869
3912
|
};
|
|
@@ -3892,7 +3935,7 @@ async function startReviewDrive(ctx, project, feature) {
|
|
|
3892
3935
|
return {
|
|
3893
3936
|
ok: start.ok,
|
|
3894
3937
|
action: "start",
|
|
3895
|
-
...start
|
|
3938
|
+
...driveDenialOf(start),
|
|
3896
3939
|
drive: activeDriveInfo(),
|
|
3897
3940
|
...start.hookFailure ? { hookFailure: start.hookFailure } : {}
|
|
3898
3941
|
};
|
|
@@ -4108,15 +4151,17 @@ ${result.output}` : ""}`,
|
|
|
4108
4151
|
}
|
|
4109
4152
|
async function dirtyPaths(g) {
|
|
4110
4153
|
try {
|
|
4111
|
-
|
|
4112
|
-
if (!out)
|
|
4113
|
-
return [];
|
|
4114
|
-
return out.split(`
|
|
4115
|
-
`).map((line) => line.slice(3).trim()).map((p) => p.includes(" -> ") ? p.split(" -> ").at(-1) ?? p : p).map((p) => p.replace(/^"|"$/g, "")).filter(Boolean);
|
|
4154
|
+
return porcelainPaths((await g.raw(["status", "--porcelain"])).trim());
|
|
4116
4155
|
} catch {
|
|
4117
4156
|
return [];
|
|
4118
4157
|
}
|
|
4119
4158
|
}
|
|
4159
|
+
function porcelainPaths(out) {
|
|
4160
|
+
if (!out)
|
|
4161
|
+
return [];
|
|
4162
|
+
return out.split(`
|
|
4163
|
+
`).map((line) => line.slice(3).trim()).map((p) => p.includes(" -> ") ? p.split(" -> ").at(-1) ?? p : p).map((p) => p.replace(/^"|"$/g, "")).filter(Boolean);
|
|
4164
|
+
}
|
|
4120
4165
|
async function detectDbDrift(ctx, project, feature, previousBranch, driveBranch) {
|
|
4121
4166
|
let files;
|
|
4122
4167
|
try {
|
|
@@ -4524,7 +4569,7 @@ function reportFinding(ctx, args) {
|
|
|
4524
4569
|
}
|
|
4525
4570
|
function updateStatus(ctx, findingId, patch) {
|
|
4526
4571
|
const current = getFinding(ctx, findingId);
|
|
4527
|
-
ctx.db.update(reviewFindings).set(patch).where(eq6(reviewFindings.id, findingId)).run();
|
|
4572
|
+
ctx.db.update(reviewFindings).set({ carriedLap: null, resolvedBy: null, resolutionNote: null, ...patch }).where(eq6(reviewFindings.id, findingId)).run();
|
|
4528
4573
|
emit(ctx, current.featureId, {
|
|
4529
4574
|
type: "finding.updated",
|
|
4530
4575
|
message: `finding ${findingId} marked ${patch.status}`,
|
|
@@ -4543,7 +4588,8 @@ function markFixed(ctx, findingId) {
|
|
|
4543
4588
|
return updateStatus(ctx, findingId, {
|
|
4544
4589
|
status: "fixed",
|
|
4545
4590
|
openReason: null,
|
|
4546
|
-
failureReason: null
|
|
4591
|
+
failureReason: null,
|
|
4592
|
+
resolvedBy: "fix-ticket"
|
|
4547
4593
|
});
|
|
4548
4594
|
}
|
|
4549
4595
|
function markFailed(ctx, findingId, reason) {
|
|
@@ -4570,6 +4616,8 @@ function dismiss(ctx, findingId) {
|
|
|
4570
4616
|
function defectState(finding, fixTicket) {
|
|
4571
4617
|
if (finding.status === "dismissed")
|
|
4572
4618
|
return "dismissed";
|
|
4619
|
+
if (finding.status === "carried")
|
|
4620
|
+
return "carried";
|
|
4573
4621
|
if (finding.status === "fixed" || fixTicket?.status === "done")
|
|
4574
4622
|
return "fixed";
|
|
4575
4623
|
if (fixTicket && fixTicket.status !== "failed" && fixTicket.status !== "cancelled") {
|
|
@@ -4578,11 +4626,15 @@ function defectState(finding, fixTicket) {
|
|
|
4578
4626
|
return finding.status === "open" || finding.status === "failed" || fixTicket?.status === "failed" || fixTicket?.status === "cancelled" ? "open" : "fixing";
|
|
4579
4627
|
}
|
|
4580
4628
|
function viewByFeature(ctx, featureId) {
|
|
4629
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
4581
4630
|
const findings = listByFeature3(ctx, featureId);
|
|
4582
4631
|
const tickets2 = listByFeature2(ctx, featureId);
|
|
4583
4632
|
const summary = { found: 0, fixed: 0, open: 0, observations: 0 };
|
|
4584
4633
|
const openDefects = [];
|
|
4634
|
+
const carriedFindings = findings.filter(isCarried);
|
|
4585
4635
|
for (const finding of findings) {
|
|
4636
|
+
if (finding.lap !== feature.lap)
|
|
4637
|
+
continue;
|
|
4586
4638
|
if (finding.kind === "observation") {
|
|
4587
4639
|
summary.observations += 1;
|
|
4588
4640
|
continue;
|
|
@@ -4596,11 +4648,75 @@ function viewByFeature(ctx, featureId) {
|
|
|
4596
4648
|
openDefects.push(finding);
|
|
4597
4649
|
}
|
|
4598
4650
|
}
|
|
4599
|
-
return { findings, openDefects, summary };
|
|
4651
|
+
return { findings, openDefects, carriedFindings, summary };
|
|
4652
|
+
}
|
|
4653
|
+
function openDefectsAcrossLaps(ctx, featureId) {
|
|
4654
|
+
const tickets2 = listByFeature2(ctx, featureId);
|
|
4655
|
+
return listByFeature3(ctx, featureId).filter((finding) => finding.kind === "defect" && defectState(finding, fixTicketOf(finding, tickets2)) === "open");
|
|
4656
|
+
}
|
|
4657
|
+
function isCarried(finding) {
|
|
4658
|
+
return finding.status === "carried";
|
|
4659
|
+
}
|
|
4660
|
+
function carriedDefectsAcrossLaps(ctx, featureId) {
|
|
4661
|
+
return listByFeature3(ctx, featureId).filter(isCarried);
|
|
4662
|
+
}
|
|
4663
|
+
function undispositionedDefects(ctx, featureId) {
|
|
4664
|
+
const { lap } = getFeatureRow(ctx, featureId);
|
|
4665
|
+
return openDefectsAcrossLaps(ctx, featureId).filter((finding) => finding.lap < lap);
|
|
4600
4666
|
}
|
|
4601
4667
|
function fixTicketOf(finding, tickets2) {
|
|
4602
4668
|
return finding.fixTicketId ? tickets2.find((t) => t.id === finding.fixTicketId) : undefined;
|
|
4603
4669
|
}
|
|
4670
|
+
function requireDispositionable(ctx, featureId, findingId) {
|
|
4671
|
+
const finding = getFinding(ctx, findingId);
|
|
4672
|
+
const eligible = finding.featureId === featureId && (finding.status === "carried" || openDefectsAcrossLaps(ctx, featureId).some((defect) => defect.id === findingId));
|
|
4673
|
+
if (!eligible) {
|
|
4674
|
+
throw new InvalidInputError(`finding ${findingId} is not an open or carried defect of this feature`);
|
|
4675
|
+
}
|
|
4676
|
+
return finding;
|
|
4677
|
+
}
|
|
4678
|
+
function carryFinding(ctx, featureId, findingId, note) {
|
|
4679
|
+
requireDispositionable(ctx, featureId, findingId);
|
|
4680
|
+
return updateStatus(ctx, findingId, {
|
|
4681
|
+
status: "carried",
|
|
4682
|
+
openReason: null,
|
|
4683
|
+
failureReason: null,
|
|
4684
|
+
carriedLap: getFeatureRow(ctx, featureId).lap,
|
|
4685
|
+
resolutionNote: note?.trim() || null
|
|
4686
|
+
});
|
|
4687
|
+
}
|
|
4688
|
+
function closeAsAddressed(ctx, featureId, findingId, note) {
|
|
4689
|
+
const attestation = note.trim();
|
|
4690
|
+
if (!attestation) {
|
|
4691
|
+
throw new InvalidInputError("closing a defect as addressed requires a note saying what addressed it");
|
|
4692
|
+
}
|
|
4693
|
+
requireDispositionable(ctx, featureId, findingId);
|
|
4694
|
+
return updateStatus(ctx, findingId, {
|
|
4695
|
+
status: "fixed",
|
|
4696
|
+
openReason: null,
|
|
4697
|
+
failureReason: null,
|
|
4698
|
+
resolvedBy: "session",
|
|
4699
|
+
resolutionNote: attestation
|
|
4700
|
+
});
|
|
4701
|
+
}
|
|
4702
|
+
function requireLinkableFindings(ctx, featureId, findingIds) {
|
|
4703
|
+
if (new Set(findingIds).size !== findingIds.length) {
|
|
4704
|
+
throw new InvalidInputError("two tickets in this batch link to the same finding");
|
|
4705
|
+
}
|
|
4706
|
+
for (const findingId of findingIds)
|
|
4707
|
+
requireDispositionable(ctx, featureId, findingId);
|
|
4708
|
+
}
|
|
4709
|
+
function linkFixTicket(ctx, featureId, findingId, fixTicketId) {
|
|
4710
|
+
requireDispositionable(ctx, featureId, findingId);
|
|
4711
|
+
ctx.db.update(reviewFindings).set({ fixTicketId }).where(eq6(reviewFindings.id, findingId)).run();
|
|
4712
|
+
return markFixing(ctx, findingId);
|
|
4713
|
+
}
|
|
4714
|
+
function reopenFinding(ctx, findingId) {
|
|
4715
|
+
const finding = getFinding(ctx, findingId);
|
|
4716
|
+
if (finding.status !== "carried")
|
|
4717
|
+
throw new InvalidInputError("only a carried finding can be reopened");
|
|
4718
|
+
return updateStatus(ctx, findingId, { status: "open", openReason: null, failureReason: null });
|
|
4719
|
+
}
|
|
4604
4720
|
function promoteOpenDefects(ctx, featureId, findingIds) {
|
|
4605
4721
|
const open = viewByFeature(ctx, featureId).openDefects;
|
|
4606
4722
|
const defects = findingIds ? findingIds.map((id) => {
|
|
@@ -4646,12 +4762,17 @@ function carriedWork(ctx, featureId) {
|
|
|
4646
4762
|
const carried = ctx.db.select({ id: testNotes.id }).from(testNotes).where(and7(eq7(testNotes.featureId, featureId), eq7(testNotes.status, "carried"))).all();
|
|
4647
4763
|
return {
|
|
4648
4764
|
carriedNotes: carried.length,
|
|
4649
|
-
openDefects:
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4765
|
+
openDefects: openDefectsAcrossLaps(ctx, featureId).map(toCarriedDefect),
|
|
4766
|
+
carriedDefects: carriedDefectsAcrossLaps(ctx, featureId).map(toCarriedDefect)
|
|
4767
|
+
};
|
|
4768
|
+
}
|
|
4769
|
+
function toCarriedDefect(defect) {
|
|
4770
|
+
return {
|
|
4771
|
+
id: defect.id,
|
|
4772
|
+
title: defect.title,
|
|
4773
|
+
location: defect.location,
|
|
4774
|
+
detail: defect.detail,
|
|
4775
|
+
reproStep: defect.reproStep
|
|
4655
4776
|
};
|
|
4656
4777
|
}
|
|
4657
4778
|
function hasCarriedWork(carried) {
|
|
@@ -4956,8 +5077,12 @@ function renderRevisitPrompt(feature, lap, purpose, runtime = DEFAULT_RUNTIME, c
|
|
|
4956
5077
|
`**${carriedSummary} \u2014 address them.** They are this lap's opening agenda:`,
|
|
4957
5078
|
`- \`${docs2}/test-notes.md\`, section \`## Carried, still open\` \u2014 every note carried`,
|
|
4958
5079
|
" and not yet done, whatever lap captured it. It exists; read it.",
|
|
4959
|
-
"- `get_feature_context` \u2192 `openDefects` \u2014 each open defect with its title,",
|
|
4960
|
-
" location, detail and repro step
|
|
5080
|
+
"- `get_feature_context` \u2192 `openDefects` \u2014 each open defect with its `id`, title,",
|
|
5081
|
+
" location, detail and repro step; and `carriedDefects`, the ones an earlier lap",
|
|
5082
|
+
" parked (agenda, never an obligation to re-carry). Every open defect from an",
|
|
5083
|
+
' EARLIER lap needs one of three answers before `complete_phase("tickets")` will',
|
|
5084
|
+
" pass: **link** it (emit this lap's ticket for it with `originFindingId` set),",
|
|
5085
|
+
" **carry** it, or **close it as addressed** \u2014 the last two are `resolve_finding`.",
|
|
4961
5086
|
`- \`${docs2}/spec.md\`, section \`## Later laps\` \u2014 scope parked by earlier laps,`,
|
|
4962
5087
|
" OPTIONAL; a missing one is normal, not an error."
|
|
4963
5088
|
] : [
|
|
@@ -5537,8 +5662,152 @@ function codexLoggedIn(env = process.env, fileExists = existsSync8) {
|
|
|
5537
5662
|
}
|
|
5538
5663
|
var init_codex_auth = () => {};
|
|
5539
5664
|
|
|
5665
|
+
// src/services/sandbox-image.ts
|
|
5666
|
+
import { createHash as createHash2 } from "crypto";
|
|
5667
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
|
|
5668
|
+
import { join as join10 } from "path";
|
|
5669
|
+
function projectSandboxDir(repoPath) {
|
|
5670
|
+
return join10(repoPath, ".runcastle", "sandbox");
|
|
5671
|
+
}
|
|
5672
|
+
function projectDockerfilePath(repoPath) {
|
|
5673
|
+
return join10(projectSandboxDir(repoPath), "Dockerfile");
|
|
5674
|
+
}
|
|
5675
|
+
function projectImageTag(projectId) {
|
|
5676
|
+
return `${DEFAULT_SANDBOX_IMAGE}-${projectId}`;
|
|
5677
|
+
}
|
|
5678
|
+
function hashDockerfileContents(contents) {
|
|
5679
|
+
return createHash2("sha256").update(contents).digest("hex");
|
|
5680
|
+
}
|
|
5681
|
+
function hashDockerfile(path) {
|
|
5682
|
+
try {
|
|
5683
|
+
return hashDockerfileContents(readFileSync4(path));
|
|
5684
|
+
} catch {
|
|
5685
|
+
return null;
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
async function inspectBuiltImage(exec, runtime, tag) {
|
|
5689
|
+
const out = await exec(runtime, [
|
|
5690
|
+
"image",
|
|
5691
|
+
"inspect",
|
|
5692
|
+
"--format",
|
|
5693
|
+
`{{index .Config.Labels "${DOCKERFILE_HASH_LABEL}"}}`,
|
|
5694
|
+
tag
|
|
5695
|
+
]);
|
|
5696
|
+
if (!(out.ok && out.code === 0))
|
|
5697
|
+
return { present: false, hash: null };
|
|
5698
|
+
const value = out.stdout.trim();
|
|
5699
|
+
return { present: true, hash: value === "" || value === "<no value>" ? null : value };
|
|
5700
|
+
}
|
|
5701
|
+
function unmanagedImageReason(imageName, projectDockerfilePresent = false) {
|
|
5702
|
+
const route = projectDockerfilePresent ? "clear the sandbox image setting to let runcastle build the .runcastle/sandbox/Dockerfile this repo already ships" : `clear the sandbox image setting to go back to ${DEFAULT_SANDBOX_IMAGE}, or commit a .runcastle/sandbox/Dockerfile for runcastle to build`;
|
|
5703
|
+
return `${imageName} is a custom image managed outside runcastle \u2014 ${route}.`;
|
|
5704
|
+
}
|
|
5705
|
+
function unmanagedImage(project) {
|
|
5706
|
+
const { id, overwritable } = project;
|
|
5707
|
+
const stored = project.stored?.trim() ?? "";
|
|
5708
|
+
if (stored === "" || overwritable)
|
|
5709
|
+
return null;
|
|
5710
|
+
if (stored === DEFAULT_SANDBOX_IMAGE || stored === projectImageTag(id))
|
|
5711
|
+
return null;
|
|
5712
|
+
return stored;
|
|
5713
|
+
}
|
|
5714
|
+
function stockBuildArgs(runtime, ids2 = {
|
|
5715
|
+
uid: process.getuid?.(),
|
|
5716
|
+
gid: process.getgid?.()
|
|
5717
|
+
}) {
|
|
5718
|
+
if (runtime === "podman")
|
|
5719
|
+
return {};
|
|
5720
|
+
return {
|
|
5721
|
+
...ids2.uid === undefined ? {} : { AGENT_UID: String(ids2.uid) },
|
|
5722
|
+
...ids2.gid === undefined ? {} : { AGENT_GID: String(ids2.gid) }
|
|
5723
|
+
};
|
|
5724
|
+
}
|
|
5725
|
+
function shippedDockerfile(project) {
|
|
5726
|
+
if (!project)
|
|
5727
|
+
return null;
|
|
5728
|
+
const path = projectDockerfilePath(project.repoPath);
|
|
5729
|
+
return existsSync9(path) ? path : null;
|
|
5730
|
+
}
|
|
5731
|
+
function planImageBuild(input) {
|
|
5732
|
+
const { config: config2, project, stockContext, stockFresh, buildArgs } = input;
|
|
5733
|
+
const stockHash = hashDockerfile(join10(stockContext, "Dockerfile")) ?? "";
|
|
5734
|
+
const stockStep = {
|
|
5735
|
+
tag: DEFAULT_SANDBOX_IMAGE,
|
|
5736
|
+
context: stockContext,
|
|
5737
|
+
dockerfileHash: stockHash,
|
|
5738
|
+
buildArgs
|
|
5739
|
+
};
|
|
5740
|
+
const projectDockerfile = shippedDockerfile(project);
|
|
5741
|
+
const handTyped = project ? unmanagedImage({
|
|
5742
|
+
id: project.id,
|
|
5743
|
+
stored: project.sandboxImage ?? null,
|
|
5744
|
+
overwritable: project.sandboxImageOverwritable
|
|
5745
|
+
}) : null;
|
|
5746
|
+
if (handTyped !== null) {
|
|
5747
|
+
return {
|
|
5748
|
+
kind: "refused",
|
|
5749
|
+
imageName: handTyped,
|
|
5750
|
+
reason: unmanagedImageReason(handTyped, projectDockerfile !== null)
|
|
5751
|
+
};
|
|
5752
|
+
}
|
|
5753
|
+
if (project && projectDockerfile) {
|
|
5754
|
+
const projectTag = projectImageTag(project.id);
|
|
5755
|
+
return {
|
|
5756
|
+
kind: "chain",
|
|
5757
|
+
projectTag,
|
|
5758
|
+
steps: [
|
|
5759
|
+
...stockFresh ? [] : [stockStep],
|
|
5760
|
+
{
|
|
5761
|
+
tag: projectTag,
|
|
5762
|
+
context: projectSandboxDir(project.repoPath),
|
|
5763
|
+
dockerfileHash: hashDockerfile(projectDockerfile) ?? "",
|
|
5764
|
+
buildArgs: {}
|
|
5765
|
+
}
|
|
5766
|
+
]
|
|
5767
|
+
};
|
|
5768
|
+
}
|
|
5769
|
+
const imageName = resolveSandboxImage(config2, project);
|
|
5770
|
+
const managed = imageName === DEFAULT_SANDBOX_IMAGE || project !== null && imageName === projectImageTag(project.id);
|
|
5771
|
+
if (!managed)
|
|
5772
|
+
return { kind: "refused", imageName, reason: unmanagedImageReason(imageName) };
|
|
5773
|
+
return { kind: "stock", steps: [stockStep] };
|
|
5774
|
+
}
|
|
5775
|
+
function buildStepArgs(step) {
|
|
5776
|
+
return [
|
|
5777
|
+
"build",
|
|
5778
|
+
"-t",
|
|
5779
|
+
step.tag,
|
|
5780
|
+
"--label",
|
|
5781
|
+
`${DOCKERFILE_HASH_LABEL}=${step.dockerfileHash}`,
|
|
5782
|
+
...Object.entries(step.buildArgs).flatMap(([key, value]) => ["--build-arg", `${key}=${value}`]),
|
|
5783
|
+
step.context
|
|
5784
|
+
];
|
|
5785
|
+
}
|
|
5786
|
+
function shellQuote(arg, platform) {
|
|
5787
|
+
if (/^[\w@%+=:,./\\-]+$/.test(arg))
|
|
5788
|
+
return arg;
|
|
5789
|
+
if (platform === "win32")
|
|
5790
|
+
return `"${arg}"`;
|
|
5791
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
5792
|
+
}
|
|
5793
|
+
function imageBuildTerminal(runtime, plan, platform = process.platform) {
|
|
5794
|
+
const steps = plan.steps;
|
|
5795
|
+
const last = steps[steps.length - 1];
|
|
5796
|
+
if (!last)
|
|
5797
|
+
throw new Error("image build plan has no steps");
|
|
5798
|
+
const cwd = last.context;
|
|
5799
|
+
if (steps.length === 1)
|
|
5800
|
+
return { cmd: runtime, args: buildStepArgs(last), cwd };
|
|
5801
|
+
const line = steps.map((step) => [runtime, ...buildStepArgs(step)].map((a) => shellQuote(a, platform)).join(" ")).join(" && ");
|
|
5802
|
+
return platform === "win32" ? { cmd: process.env.ComSpec ?? "cmd.exe", args: ["/d", "/s", "/c", line], cwd } : { cmd: "/bin/sh", args: ["-c", line], cwd };
|
|
5803
|
+
}
|
|
5804
|
+
var DOCKERFILE_HASH_LABEL = "runcastle.dockerfile-hash";
|
|
5805
|
+
var init_sandbox_image = __esm(() => {
|
|
5806
|
+
init_src();
|
|
5807
|
+
});
|
|
5808
|
+
|
|
5540
5809
|
// src/doctor/doctor.ts
|
|
5541
|
-
import { existsSync as
|
|
5810
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5542
5811
|
async function versionProbe(exec, id, label, bin, fix) {
|
|
5543
5812
|
const out = await exec(bin, ["--version"]);
|
|
5544
5813
|
if (out.ok && out.code === 0) {
|
|
@@ -5651,7 +5920,7 @@ function runtimeAfkKeyProbe(spec, env, severity = "error") {
|
|
|
5651
5920
|
};
|
|
5652
5921
|
}
|
|
5653
5922
|
async function runtimeProbes(spec, deps) {
|
|
5654
|
-
const fileExists = deps.fileExists ??
|
|
5923
|
+
const fileExists = deps.fileExists ?? existsSync10;
|
|
5655
5924
|
const afkKeyProbe = runtimeAfkKeyProbe(spec, deps.env, deps.severity);
|
|
5656
5925
|
return [
|
|
5657
5926
|
await runtimeBinaryProbe(spec, deps.exec, deps.severity),
|
|
@@ -5745,49 +6014,98 @@ async function containerRuntimeProbe(exec) {
|
|
|
5745
6014
|
fix: "Install Docker Desktop or Podman (see docs/research/PREREQS-NOTES.md \xA74). Not needed for interactive-only use."
|
|
5746
6015
|
};
|
|
5747
6016
|
}
|
|
5748
|
-
async function sandcastleImageProbe(
|
|
5749
|
-
const
|
|
5750
|
-
const
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
6017
|
+
async function sandcastleImageProbe(input) {
|
|
6018
|
+
const { exec, burnerDockerfile, dockerfileHash, project } = input;
|
|
6019
|
+
const runtime = await presentRuntime(exec);
|
|
6020
|
+
if (!runtime) {
|
|
6021
|
+
return {
|
|
6022
|
+
...IMAGE_ROW,
|
|
6023
|
+
status: "missing",
|
|
6024
|
+
severity: "error",
|
|
6025
|
+
detail: "no container runtime available to inspect the image",
|
|
6026
|
+
fix: "Install a container runtime (Docker or Podman) first, then build the image from the Enable AFK burns card in the app."
|
|
6027
|
+
};
|
|
6028
|
+
}
|
|
6029
|
+
const projectHash = project ? dockerfileHash(projectDockerfilePath(project.repoPath)) : null;
|
|
6030
|
+
const handTyped = project ? unmanagedImage(project) : null;
|
|
6031
|
+
let stored = project?.stored ?? null;
|
|
6032
|
+
if (project && stored !== null && projectHash === null && project.overwritable) {
|
|
6033
|
+
project.clearStored();
|
|
6034
|
+
stored = null;
|
|
6035
|
+
}
|
|
6036
|
+
const stockHash = dockerfileHash(burnerDockerfile);
|
|
6037
|
+
const stock = await inspectBuiltImage(exec, runtime, DEFAULT_SANDBOX_IMAGE);
|
|
6038
|
+
const stockFresh = stock.present && (stockHash === null || stock.hash === stockHash);
|
|
6039
|
+
if (project && handTyped === null && projectHash !== null) {
|
|
6040
|
+
const tag = projectImageTag(project.id);
|
|
6041
|
+
const image = await inspectBuiltImage(exec, runtime, tag);
|
|
6042
|
+
if (!image.present || project.overwritable && stored !== tag) {
|
|
6043
|
+
return {
|
|
6044
|
+
...IMAGE_ROW,
|
|
6045
|
+
status: "not-built-yet",
|
|
6046
|
+
severity: "error",
|
|
6047
|
+
detail: image.present ? `${tag} is built but is not this project's image yet` : `.runcastle/sandbox/Dockerfile is not built \u2014 ${tag} does not exist`,
|
|
6048
|
+
fix: "Open Settings \u2192 Burns (Build image) \u2014 runcastle builds .runcastle/sandbox/Dockerfile for you."
|
|
6049
|
+
};
|
|
6050
|
+
}
|
|
6051
|
+
if (image.hash !== projectHash) {
|
|
6052
|
+
return staleImage(`${tag} no longer matches .runcastle/sandbox/Dockerfile`);
|
|
6053
|
+
}
|
|
6054
|
+
if (!stockFresh) {
|
|
6055
|
+
const base = stock.present ? "no longer matches the burner Dockerfile" : "is not built";
|
|
6056
|
+
return staleImage(`${tag} is built on ${DEFAULT_SANDBOX_IMAGE}, which ${base}`);
|
|
5772
6057
|
}
|
|
5773
6058
|
return {
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
6059
|
+
...IMAGE_ROW,
|
|
6060
|
+
status: "ok",
|
|
6061
|
+
severity: "error",
|
|
6062
|
+
detail: `${tag} built from .runcastle/sandbox/Dockerfile`
|
|
6063
|
+
};
|
|
6064
|
+
}
|
|
6065
|
+
const imageName = resolveSandboxImage({ sandboxImage: input.imageName }, { sandboxImage: stored });
|
|
6066
|
+
if (imageName !== DEFAULT_SANDBOX_IMAGE) {
|
|
6067
|
+
const custom = await inspectBuiltImage(exec, runtime, imageName);
|
|
6068
|
+
const clauses = [
|
|
6069
|
+
`${imageName} is a custom image, managed outside runcastle`,
|
|
6070
|
+
...projectHash === null ? [] : [`and outranks this repo's .runcastle/sandbox/Dockerfile`],
|
|
6071
|
+
...custom.present ? [] : ["and is not built locally"]
|
|
6072
|
+
];
|
|
6073
|
+
return {
|
|
6074
|
+
...IMAGE_ROW,
|
|
6075
|
+
status: "custom",
|
|
6076
|
+
severity: custom.present ? "info" : "error",
|
|
6077
|
+
detail: clauses.join(" \u2014 "),
|
|
6078
|
+
fix: unmanagedImageReason(imageName, projectHash !== null)
|
|
6079
|
+
};
|
|
6080
|
+
}
|
|
6081
|
+
if (!stock.present) {
|
|
6082
|
+
return {
|
|
6083
|
+
...IMAGE_ROW,
|
|
5777
6084
|
status: "missing",
|
|
5778
6085
|
severity: "error",
|
|
5779
6086
|
detail: `image ${imageName} not found locally`,
|
|
5780
6087
|
fix: 'Start runcastle and click "Build image" on the Enable AFK burns card \u2014 it builds this for you (one click). Only needed for AFK/sandboxed burns.'
|
|
5781
6088
|
};
|
|
5782
6089
|
}
|
|
6090
|
+
if (!stockFresh)
|
|
6091
|
+
return staleImage(`${imageName} no longer matches the burner Dockerfile`);
|
|
6092
|
+
return { ...IMAGE_ROW, status: "ok", severity: "error", detail: `${imageName} present` };
|
|
6093
|
+
}
|
|
6094
|
+
async function presentRuntime(exec) {
|
|
6095
|
+
for (const runtime of ["docker", "podman"]) {
|
|
6096
|
+
const present = await exec(runtime, ["--version"]);
|
|
6097
|
+
if (present.ok && present.code === 0)
|
|
6098
|
+
return runtime;
|
|
6099
|
+
}
|
|
6100
|
+
return null;
|
|
6101
|
+
}
|
|
6102
|
+
function staleImage(detail) {
|
|
5783
6103
|
return {
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
tier: 2,
|
|
5787
|
-
status: "missing",
|
|
6104
|
+
...IMAGE_ROW,
|
|
6105
|
+
status: "stale",
|
|
5788
6106
|
severity: "error",
|
|
5789
|
-
detail:
|
|
5790
|
-
fix: "
|
|
6107
|
+
detail: `${detail} \u2014 rebuild`,
|
|
6108
|
+
fix: "Open Settings \u2192 Burns (Rebuild image)."
|
|
5791
6109
|
};
|
|
5792
6110
|
}
|
|
5793
6111
|
async function runDoctor(env) {
|
|
@@ -5795,7 +6113,7 @@ async function runDoctor(env) {
|
|
|
5795
6113
|
const processEnv = env.env ?? process.env;
|
|
5796
6114
|
const imageName = env.imageName ?? DEFAULT_SANDBOX_IMAGE;
|
|
5797
6115
|
const burnerDockerfile = env.burnerDockerfile ?? burnerDockerfilePath();
|
|
5798
|
-
const
|
|
6116
|
+
const dockerfileHash = env.dockerfileHash ?? hashDockerfile;
|
|
5799
6117
|
const required = new Set(env.runtimes ?? [DEFAULT_RUNTIME]);
|
|
5800
6118
|
const results = [
|
|
5801
6119
|
await bunProbe(exec),
|
|
@@ -5810,7 +6128,13 @@ async function runDoctor(env) {
|
|
|
5810
6128
|
severity: required.has(runtime) ? "error" : "info"
|
|
5811
6129
|
}));
|
|
5812
6130
|
}
|
|
5813
|
-
results.push(await gitIdentityProbe(exec, env.cwd), await containerRuntimeProbe(exec), await sandcastleImageProbe(
|
|
6131
|
+
results.push(await gitIdentityProbe(exec, env.cwd), await containerRuntimeProbe(exec), await sandcastleImageProbe({
|
|
6132
|
+
exec,
|
|
6133
|
+
imageName,
|
|
6134
|
+
burnerDockerfile,
|
|
6135
|
+
dockerfileHash,
|
|
6136
|
+
...env.projectImage ? { project: env.projectImage } : {}
|
|
6137
|
+
}));
|
|
5814
6138
|
const counts = (r) => r.severity === "error";
|
|
5815
6139
|
return {
|
|
5816
6140
|
results,
|
|
@@ -5823,11 +6147,12 @@ function exitCodeFor(report, mode) {
|
|
|
5823
6147
|
return report.tier1Ok ? 0 : 1;
|
|
5824
6148
|
return report.ok ? 0 : 1;
|
|
5825
6149
|
}
|
|
5826
|
-
var RUNTIME_SPECS;
|
|
6150
|
+
var RUNTIME_SPECS, IMAGE_ROW;
|
|
5827
6151
|
var init_doctor = __esm(() => {
|
|
5828
6152
|
init_src();
|
|
5829
6153
|
init_asset_paths();
|
|
5830
6154
|
init_codex_auth();
|
|
6155
|
+
init_sandbox_image();
|
|
5831
6156
|
RUNTIME_SPECS = {
|
|
5832
6157
|
"claude-code": {
|
|
5833
6158
|
runtime: "claude-code",
|
|
@@ -5859,21 +6184,22 @@ var init_doctor = __esm(() => {
|
|
|
5859
6184
|
authFile: codexAuthFile
|
|
5860
6185
|
}
|
|
5861
6186
|
};
|
|
6187
|
+
IMAGE_ROW = { id: "sandcastle-image", label: "Sandcastle container image", tier: 2 };
|
|
5862
6188
|
});
|
|
5863
6189
|
|
|
5864
6190
|
// src/launcher/runtimes/codex.ts
|
|
5865
6191
|
import {
|
|
5866
6192
|
copyFileSync,
|
|
5867
|
-
existsSync as
|
|
6193
|
+
existsSync as existsSync11,
|
|
5868
6194
|
mkdirSync as mkdirSync4,
|
|
5869
|
-
readFileSync as
|
|
6195
|
+
readFileSync as readFileSync5,
|
|
5870
6196
|
readdirSync as readdirSync2,
|
|
5871
|
-
statSync
|
|
6197
|
+
statSync,
|
|
5872
6198
|
writeFileSync as writeFileSync3
|
|
5873
6199
|
} from "fs";
|
|
5874
|
-
import { basename, dirname as dirname4, join as
|
|
6200
|
+
import { basename, dirname as dirname4, join as join11, resolve as resolve7 } from "path";
|
|
5875
6201
|
function codexHomeDir2(sessionId) {
|
|
5876
|
-
return
|
|
6202
|
+
return join11(sessionDir(sessionId), "codex-home");
|
|
5877
6203
|
}
|
|
5878
6204
|
function realCodexAuthFile(env = process.env) {
|
|
5879
6205
|
return codexAuthFile(env);
|
|
@@ -5921,11 +6247,11 @@ function mcpServerTables(userConfigToml) {
|
|
|
5921
6247
|
`).trim();
|
|
5922
6248
|
}
|
|
5923
6249
|
function readInheritedMcpServers(env = process.env) {
|
|
5924
|
-
const path =
|
|
5925
|
-
if (!
|
|
6250
|
+
const path = join11(dirname4(realCodexAuthFile(env)), "config.toml");
|
|
6251
|
+
if (!existsSync11(path))
|
|
5926
6252
|
return "";
|
|
5927
6253
|
try {
|
|
5928
|
-
return mcpServerTables(
|
|
6254
|
+
return mcpServerTables(readFileSync5(path, "utf8"));
|
|
5929
6255
|
} catch {
|
|
5930
6256
|
return "";
|
|
5931
6257
|
}
|
|
@@ -5946,19 +6272,19 @@ function renderCodexHooks(hookClient, kind, platform = process.platform) {
|
|
|
5946
6272
|
};
|
|
5947
6273
|
}
|
|
5948
6274
|
function writeCodexSkills(worktreePath, packDir) {
|
|
5949
|
-
if (!
|
|
6275
|
+
if (!existsSync11(worktreePath))
|
|
5950
6276
|
return [];
|
|
5951
|
-
const source =
|
|
5952
|
-
if (!
|
|
6277
|
+
const source = join11(packDir, "skills");
|
|
6278
|
+
if (!existsSync11(source))
|
|
5953
6279
|
return [];
|
|
5954
6280
|
const written = [];
|
|
5955
6281
|
for (const name of readdirSync2(source)) {
|
|
5956
|
-
const skill =
|
|
5957
|
-
if (!
|
|
6282
|
+
const skill = join11(source, name, "SKILL.md");
|
|
6283
|
+
if (!existsSync11(skill))
|
|
5958
6284
|
continue;
|
|
5959
|
-
const target =
|
|
6285
|
+
const target = join11(worktreePath, CODEX_SKILLS_REL, name);
|
|
5960
6286
|
mkdirSync4(target, { recursive: true });
|
|
5961
|
-
const path =
|
|
6287
|
+
const path = join11(target, "SKILL.md");
|
|
5962
6288
|
copyFileSync(skill, path);
|
|
5963
6289
|
written.push(path);
|
|
5964
6290
|
}
|
|
@@ -5972,8 +6298,8 @@ function excludeFromGit(worktreePath, entry) {
|
|
|
5972
6298
|
if (!info)
|
|
5973
6299
|
return;
|
|
5974
6300
|
mkdirSync4(info, { recursive: true });
|
|
5975
|
-
const path =
|
|
5976
|
-
const existing =
|
|
6301
|
+
const path = join11(info, "exclude");
|
|
6302
|
+
const existing = existsSync11(path) ? readFileSync5(path, "utf8") : "";
|
|
5977
6303
|
if (existing.split(/\r?\n/).some((line) => line.trim() === entry))
|
|
5978
6304
|
return;
|
|
5979
6305
|
const head = existing.length > 0 && !existing.endsWith(`
|
|
@@ -5984,17 +6310,17 @@ function excludeFromGit(worktreePath, entry) {
|
|
|
5984
6310
|
} catch {}
|
|
5985
6311
|
}
|
|
5986
6312
|
function gitInfoDir(worktreePath) {
|
|
5987
|
-
const dotGit =
|
|
5988
|
-
if (!
|
|
6313
|
+
const dotGit = join11(worktreePath, ".git");
|
|
6314
|
+
if (!existsSync11(dotGit))
|
|
5989
6315
|
return null;
|
|
5990
|
-
if (
|
|
5991
|
-
return
|
|
5992
|
-
const named = /^gitdir:\s*(.+)$/m.exec(
|
|
6316
|
+
if (statSync(dotGit).isDirectory())
|
|
6317
|
+
return join11(dotGit, "info");
|
|
6318
|
+
const named = /^gitdir:\s*(.+)$/m.exec(readFileSync5(dotGit, "utf8"));
|
|
5993
6319
|
if (!named)
|
|
5994
6320
|
return null;
|
|
5995
6321
|
const gitDir = resolve7(worktreePath, named[1].trim());
|
|
5996
6322
|
const parent = dirname4(gitDir);
|
|
5997
|
-
return basename(parent) === "worktrees" ?
|
|
6323
|
+
return basename(parent) === "worktrees" ? join11(dirname4(parent), "info") : join11(gitDir, "info");
|
|
5998
6324
|
}
|
|
5999
6325
|
function buildCodexArgs(input) {
|
|
6000
6326
|
return [
|
|
@@ -6013,7 +6339,7 @@ var init_codex = __esm(() => {
|
|
|
6013
6339
|
init_skills_root();
|
|
6014
6340
|
init_skills();
|
|
6015
6341
|
CODEX = RUNTIME_SPECS.codex;
|
|
6016
|
-
CODEX_SKILLS_REL =
|
|
6342
|
+
CODEX_SKILLS_REL = join11(".agents", "skills");
|
|
6017
6343
|
KICKOFF_LINES2 = kickoffLinesFor("codex");
|
|
6018
6344
|
codexRuntime = {
|
|
6019
6345
|
id: "codex",
|
|
@@ -6029,7 +6355,7 @@ var init_codex = __esm(() => {
|
|
|
6029
6355
|
doctorHint: 'Run `runcastle doctor` and fix its "Codex CLI" probe, then restart runcastle from a ' + "terminal where `codex --version` works."
|
|
6030
6356
|
};
|
|
6031
6357
|
}
|
|
6032
|
-
if (!
|
|
6358
|
+
if (!existsSync11(realCodexAuthFile())) {
|
|
6033
6359
|
return {
|
|
6034
6360
|
ok: false,
|
|
6035
6361
|
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()}`,
|
|
@@ -6041,9 +6367,9 @@ var init_codex = __esm(() => {
|
|
|
6041
6367
|
async writeArtifacts(input) {
|
|
6042
6368
|
const home = codexHomeDir2(input.session.id);
|
|
6043
6369
|
mkdirSync4(home, { recursive: true });
|
|
6044
|
-
const configPath2 =
|
|
6045
|
-
const hooksPath =
|
|
6046
|
-
const agentsPath =
|
|
6370
|
+
const configPath2 = join11(home, "config.toml");
|
|
6371
|
+
const hooksPath = join11(home, "hooks.json");
|
|
6372
|
+
const agentsPath = join11(home, "AGENTS.md");
|
|
6047
6373
|
writeFileSync3(configPath2, renderCodexConfig({
|
|
6048
6374
|
session: input.session,
|
|
6049
6375
|
config: input.config,
|
|
@@ -6056,8 +6382,8 @@ var init_codex = __esm(() => {
|
|
|
6056
6382
|
writeFileSync3(agentsPath, renderSessionPrompt({ ...input, runtime: this.id }), "utf8");
|
|
6057
6383
|
const files = [configPath2, hooksPath, agentsPath];
|
|
6058
6384
|
const auth = realCodexAuthFile();
|
|
6059
|
-
if (
|
|
6060
|
-
const copied =
|
|
6385
|
+
if (existsSync11(auth)) {
|
|
6386
|
+
const copied = join11(home, "auth.json");
|
|
6061
6387
|
copyFileSync(auth, copied);
|
|
6062
6388
|
files.push(copied);
|
|
6063
6389
|
}
|
|
@@ -6099,7 +6425,7 @@ var init_runtimes = __esm(() => {
|
|
|
6099
6425
|
});
|
|
6100
6426
|
|
|
6101
6427
|
// src/launcher/sessions.ts
|
|
6102
|
-
import { statSync as
|
|
6428
|
+
import { statSync as statSync2 } from "fs";
|
|
6103
6429
|
import { and as and8, desc as desc3, eq as eq8, inArray as inArray2, isNotNull as isNotNull2, sql as sql2 } from "drizzle-orm";
|
|
6104
6430
|
function createSessionRow(ctx, input) {
|
|
6105
6431
|
const row = ctx.db.insert(sessions).values({
|
|
@@ -6454,7 +6780,7 @@ function transcriptBytes(session) {
|
|
|
6454
6780
|
if (!session?.transcriptPath)
|
|
6455
6781
|
return;
|
|
6456
6782
|
try {
|
|
6457
|
-
return
|
|
6783
|
+
return statSync2(session.transcriptPath).size;
|
|
6458
6784
|
} catch {
|
|
6459
6785
|
return;
|
|
6460
6786
|
}
|
|
@@ -6550,7 +6876,7 @@ async function getRuntimeCtx() {
|
|
|
6550
6876
|
var current = null;
|
|
6551
6877
|
|
|
6552
6878
|
// src/services/gates.ts
|
|
6553
|
-
import { existsSync as
|
|
6879
|
+
import { existsSync as existsSync12 } from "fs";
|
|
6554
6880
|
import { and as and9, desc as desc4, eq as eq9, inArray as inArray4 } from "drizzle-orm";
|
|
6555
6881
|
function checkGate(ctx, check, feature) {
|
|
6556
6882
|
switch (check) {
|
|
@@ -6586,6 +6912,10 @@ function checkGate(ctx, check, feature) {
|
|
|
6586
6912
|
reason: 'no review ticket on this lap \u2014 every batch closes with one (kind: "review"); emit it, ' + "fix the kind on a review-shaped ticket, or override this gate with a reason"
|
|
6587
6913
|
};
|
|
6588
6914
|
}
|
|
6915
|
+
const undispositioned = undispositionedDefects(ctx, feature.id);
|
|
6916
|
+
if (undispositioned.length > 0) {
|
|
6917
|
+
return { satisfied: false, reason: undispositionedReason(undispositioned) };
|
|
6918
|
+
}
|
|
6589
6919
|
return { satisfied: true };
|
|
6590
6920
|
}
|
|
6591
6921
|
case "all-tickets-terminal": {
|
|
@@ -6602,6 +6932,10 @@ function checkGate(ctx, check, feature) {
|
|
|
6602
6932
|
return { satisfied: false, reason: "use the Merge button to ship" };
|
|
6603
6933
|
}
|
|
6604
6934
|
}
|
|
6935
|
+
function undispositionedReason(defects) {
|
|
6936
|
+
const titles = defects.map((defect) => `"${defect.title}"`).join(", ");
|
|
6937
|
+
return `${defects.length} defect${defects.length === 1 ? "" : "s"} from an earlier lap ${defects.length === 1 ? "is" : "are"} still un-dispositioned: ${titles} \u2014 for each, either emit this lap's ticket for it with ` + "`originFindingId` set (link), or call `resolve_finding` to carry it or close it as " + "addressed. The human can also dismiss it from the review page.";
|
|
6938
|
+
}
|
|
6605
6939
|
function notYetTerminal(noun, statuses, order) {
|
|
6606
6940
|
const counts = new Map;
|
|
6607
6941
|
for (const s of statuses)
|
|
@@ -6615,7 +6949,7 @@ function notYetTerminal(noun, statuses, order) {
|
|
|
6615
6949
|
}
|
|
6616
6950
|
function fileGate(ctx, feature, fileName, reason) {
|
|
6617
6951
|
const project = projectForFeature(ctx, feature);
|
|
6618
|
-
return
|
|
6952
|
+
return existsSync12(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
|
|
6619
6953
|
}
|
|
6620
6954
|
function lapWorked(ctx, feature) {
|
|
6621
6955
|
return ctx.db.select({ id: sessions.id }).from(sessions).where(and9(eq9(sessions.featureId, feature.id), eq9(sessions.lap, feature.lap), inArray4(sessions.kind, [...DOC_WRITING_KINDS]))).all().length > 0;
|
|
@@ -6675,20 +7009,21 @@ var init_gates = __esm(() => {
|
|
|
6675
7009
|
init_events();
|
|
6676
7010
|
init_feature_docs();
|
|
6677
7011
|
init_repo();
|
|
7012
|
+
init_review_findings();
|
|
6678
7013
|
init_tickets();
|
|
6679
7014
|
init_waypoints();
|
|
6680
7015
|
DOC_WRITING_KINDS = ["ideation", "revisit", "converge"];
|
|
6681
7016
|
});
|
|
6682
7017
|
|
|
6683
7018
|
// src/services/knowledge.ts
|
|
6684
|
-
import { existsSync as
|
|
6685
|
-
import { join as
|
|
7019
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
7020
|
+
import { join as join12, relative as relative2, resolve as resolve8, sep } from "path";
|
|
6686
7021
|
function scaffoldDocs(ctx, feature, opts = {}) {
|
|
6687
7022
|
const project = projectForFeature(ctx, feature);
|
|
6688
7023
|
const dir = featureDocsDir(project, feature);
|
|
6689
7024
|
mkdirSync5(dir, { recursive: true });
|
|
6690
7025
|
const briefPath = featureDocPath(project, feature, "brief.md");
|
|
6691
|
-
if (!
|
|
7026
|
+
if (!existsSync13(briefPath)) {
|
|
6692
7027
|
const override = opts.brief?.trim();
|
|
6693
7028
|
const created = new Date().toISOString();
|
|
6694
7029
|
const brief = override ? `${override}
|
|
@@ -6715,7 +7050,7 @@ function scaffoldDocs(ctx, feature, opts = {}) {
|
|
|
6715
7050
|
function scaffoldMapDoc(project, feature, seed) {
|
|
6716
7051
|
mkdirSync5(featureDocsDir(project, feature), { recursive: true });
|
|
6717
7052
|
const mapPath = featureDocPath(project, feature, "map.md");
|
|
6718
|
-
if (
|
|
7053
|
+
if (existsSync13(mapPath))
|
|
6719
7054
|
return;
|
|
6720
7055
|
writeFileSync4(mapPath, mapDocBody(feature, seed), "utf8");
|
|
6721
7056
|
}
|
|
@@ -6735,18 +7070,18 @@ function mapDocBody(feature, seed) {
|
|
|
6735
7070
|
`);
|
|
6736
7071
|
}
|
|
6737
7072
|
function readCharter(repoRoot) {
|
|
6738
|
-
const path =
|
|
6739
|
-
if (!
|
|
7073
|
+
const path = join12(repoRoot, CHARTER_FILE);
|
|
7074
|
+
if (!existsSync13(path))
|
|
6740
7075
|
return;
|
|
6741
7076
|
try {
|
|
6742
|
-
return
|
|
7077
|
+
return readFileSync6(path, "utf8");
|
|
6743
7078
|
} catch {
|
|
6744
7079
|
return;
|
|
6745
7080
|
}
|
|
6746
7081
|
}
|
|
6747
7082
|
function listLiveAdrs(repoRoot) {
|
|
6748
|
-
const dir =
|
|
6749
|
-
if (!
|
|
7083
|
+
const dir = join12(repoRoot, ADR_DIR_REL);
|
|
7084
|
+
if (!existsSync13(dir))
|
|
6750
7085
|
return [];
|
|
6751
7086
|
const docs2 = [];
|
|
6752
7087
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
@@ -6754,7 +7089,7 @@ function listLiveAdrs(repoRoot) {
|
|
|
6754
7089
|
continue;
|
|
6755
7090
|
let content;
|
|
6756
7091
|
try {
|
|
6757
|
-
content =
|
|
7092
|
+
content = readFileSync6(join12(dir, entry.name), "utf8");
|
|
6758
7093
|
} catch {
|
|
6759
7094
|
continue;
|
|
6760
7095
|
}
|
|
@@ -6767,10 +7102,10 @@ function listLiveAdrs(repoRoot) {
|
|
|
6767
7102
|
function listDocs(ctx, feature) {
|
|
6768
7103
|
const project = projectForFeature(ctx, feature);
|
|
6769
7104
|
const dir = featureDocsDir(project, feature);
|
|
6770
|
-
if (!
|
|
7105
|
+
if (!existsSync13(dir))
|
|
6771
7106
|
return [];
|
|
6772
7107
|
return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => {
|
|
6773
|
-
const path =
|
|
7108
|
+
const path = join12(dir, e.name);
|
|
6774
7109
|
return { relPath: e.name, title: readTitle(path, e.name), ...writtenAt(path) };
|
|
6775
7110
|
}).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
6776
7111
|
}
|
|
@@ -6783,20 +7118,20 @@ function readDoc(ctx, feature, relPath) {
|
|
|
6783
7118
|
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
6784
7119
|
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
6785
7120
|
}
|
|
6786
|
-
if (!
|
|
7121
|
+
if (!existsSync13(target))
|
|
6787
7122
|
throw new NotFoundError(`doc not found: ${relPath}`);
|
|
6788
|
-
return { content:
|
|
7123
|
+
return { content: readFileSync6(target, "utf8") };
|
|
6789
7124
|
}
|
|
6790
7125
|
function writtenAt(path) {
|
|
6791
7126
|
try {
|
|
6792
|
-
return { updatedAt: Math.floor(
|
|
7127
|
+
return { updatedAt: Math.floor(statSync3(path).mtimeMs) };
|
|
6793
7128
|
} catch {
|
|
6794
7129
|
return {};
|
|
6795
7130
|
}
|
|
6796
7131
|
}
|
|
6797
7132
|
function readTitle(path, fallback) {
|
|
6798
7133
|
try {
|
|
6799
|
-
const firstLine =
|
|
7134
|
+
const firstLine = readFileSync6(path, "utf8").split(`
|
|
6800
7135
|
`, 1)[0] ?? "";
|
|
6801
7136
|
const heading = firstLine.match(/^#\s+(.+)$/);
|
|
6802
7137
|
return heading ? heading[1].trim() : fallback;
|
|
@@ -7009,6 +7344,13 @@ var require$1, __create, __defProp2, __getOwnPropDesc, __getOwnPropNames, __getP
|
|
|
7009
7344
|
toString() {
|
|
7010
7345
|
return this.items.join(this.separator);
|
|
7011
7346
|
}
|
|
7347
|
+
}, elidePayloadTokens = (command) => command.replace(/'[A-Za-z0-9+\/=]{120,}'/g, "'\u2026'"), formatExecFailureMessage = (command, result, maxTailChars = MAX_TAIL_CHARS) => {
|
|
7348
|
+
const output = [result.stdout, result.stderr].map((stream) => (stream ?? "").trimEnd()).filter((stream) => stream.length > 0).join(`
|
|
7349
|
+
`);
|
|
7350
|
+
const tail = output.length > maxTailChars ? output.slice(output.length - maxTailChars) : output;
|
|
7351
|
+
const headline = `Command failed (exit ${result.exitCode}): ${elidePayloadTokens(command)}`;
|
|
7352
|
+
return tail ? `${headline}
|
|
7353
|
+
${tail}` : headline;
|
|
7012
7354
|
};
|
|
7013
7355
|
var init_chunk_NGBM7T3E = __esm(() => {
|
|
7014
7356
|
require$1 = createRequire3(import.meta.url);
|
|
@@ -7033,7 +7375,7 @@ import { createRequire as createRequire4 } from "module";
|
|
|
7033
7375
|
import * as clack from "@clack/prompts";
|
|
7034
7376
|
import { join as join$1, basename as basename2, dirname as dirname5, resolve as resolve9, posix as posix2, normalize, isAbsolute as isAbsolute2 } from "path";
|
|
7035
7377
|
import { styleText, promisify } from "util";
|
|
7036
|
-
import { existsSync as
|
|
7378
|
+
import { existsSync as existsSync14, statSync as statSync4 } from "fs";
|
|
7037
7379
|
import { tmpdir as tmpdir2, homedir as homedir3 } from "os";
|
|
7038
7380
|
import { mkdtemp, writeFile, mkdir, readFile, rm, readdir, stat } from "fs/promises";
|
|
7039
7381
|
import { exec, execFile } from "child_process";
|
|
@@ -8112,7 +8454,7 @@ var Effect_exports, isFunction = (input) => typeof input === "function", dual =
|
|
|
8112
8454
|
next4 = b2;
|
|
8113
8455
|
}
|
|
8114
8456
|
return out;
|
|
8115
|
-
}, getEquivalence2, dedupeWith, dedupe = (self) => dedupeWith(self, equivalence()),
|
|
8457
|
+
}, getEquivalence2, dedupeWith, dedupe = (self) => dedupeWith(self, equivalence()), join13, TypeId4, emptyArray, getEquivalence3 = (isEquivalent) => make((self, that) => self.length === that.length && toReadonlyArray(self).every((value5, i) => isEquivalent(value5, unsafeGet2(that, i)))), _equivalence3, ChunkProto, makeChunk = (backing) => {
|
|
8116
8458
|
const chunk4 = Object.create(ChunkProto);
|
|
8117
8459
|
chunk4.backing = backing;
|
|
8118
8460
|
switch (backing._tag) {
|
|
@@ -9811,7 +10153,7 @@ ${prefix}}`;
|
|
|
9811
10153
|
Object.defineProperty(error3, "toString", {
|
|
9812
10154
|
enumerable: false,
|
|
9813
10155
|
value() {
|
|
9814
|
-
const path2 = pipe(this.path,
|
|
10156
|
+
const path2 = pipe(this.path, join13(options.pathDelim));
|
|
9815
10157
|
return `(Invalid data at ${path2}: "${this.message}")`;
|
|
9816
10158
|
}
|
|
9817
10159
|
});
|
|
@@ -9826,7 +10168,7 @@ ${prefix}}`;
|
|
|
9826
10168
|
Object.defineProperty(error3, "toString", {
|
|
9827
10169
|
enumerable: false,
|
|
9828
10170
|
value() {
|
|
9829
|
-
const path2 = pipe(this.path,
|
|
10171
|
+
const path2 = pipe(this.path, join13(options.pathDelim));
|
|
9830
10172
|
return `(Missing data at ${path2}: "${this.message}")`;
|
|
9831
10173
|
}
|
|
9832
10174
|
});
|
|
@@ -9842,7 +10184,7 @@ ${prefix}}`;
|
|
|
9842
10184
|
Object.defineProperty(error3, "toString", {
|
|
9843
10185
|
enumerable: false,
|
|
9844
10186
|
value() {
|
|
9845
|
-
const path2 = pipe(this.path,
|
|
10187
|
+
const path2 = pipe(this.path, join13(options.pathDelim));
|
|
9846
10188
|
return `(Source unavailable at ${path2}: "${this.message}")`;
|
|
9847
10189
|
}
|
|
9848
10190
|
});
|
|
@@ -9857,7 +10199,7 @@ ${prefix}}`;
|
|
|
9857
10199
|
Object.defineProperty(error3, "toString", {
|
|
9858
10200
|
enumerable: false,
|
|
9859
10201
|
value() {
|
|
9860
|
-
const path2 = pipe(this.path,
|
|
10202
|
+
const path2 = pipe(this.path, join13(options.pathDelim));
|
|
9861
10203
|
return `(Unsupported operation at ${path2}: "${this.message}")`;
|
|
9862
10204
|
}
|
|
9863
10205
|
});
|
|
@@ -9887,7 +10229,7 @@ ${prefix}}`;
|
|
|
9887
10229
|
pathDelim: "_",
|
|
9888
10230
|
seqDelim: ","
|
|
9889
10231
|
}, options);
|
|
9890
|
-
const makePathString = (path) => pipe(path,
|
|
10232
|
+
const makePathString = (path) => pipe(path, join13(pathDelim));
|
|
9891
10233
|
const unmakePathString = (pathString) => pathString.split(pathDelim);
|
|
9892
10234
|
const getEnv = () => typeof process !== "undefined" && ("env" in process) && typeof process.env === "object" ? process.env : {};
|
|
9893
10235
|
const load = (path, primitive2, split2 = true) => {
|
|
@@ -10008,7 +10350,7 @@ ${prefix}}`;
|
|
|
10008
10350
|
return fail2(right3.left);
|
|
10009
10351
|
}
|
|
10010
10352
|
if (isRight2(left3) && isRight2(right3)) {
|
|
10011
|
-
const path = pipe(prefix,
|
|
10353
|
+
const path = pipe(prefix, join13("."));
|
|
10012
10354
|
const fail19 = fromFlatLoopFail(prefix, path);
|
|
10013
10355
|
const [lefts, rights] = extend(fail19, fail19, pipe(left3.right, map3(right2)), pipe(right3.right, map3(right2)));
|
|
10014
10356
|
return pipe(lefts, zip(rights), forEachSequential(([left4, right4]) => pipe(zip2(left4, right4), map10(([left5, right5]) => op.zip(left5, right5)))));
|
|
@@ -17940,7 +18282,7 @@ schema (${ast._tag}): ${ast}`;
|
|
|
17940
18282
|
const cowFlags = getCopyOnWriteFlags(process.platform);
|
|
17941
18283
|
for (const relativePath of paths) {
|
|
17942
18284
|
const src = join$1(hostRepoDir, relativePath);
|
|
17943
|
-
if (!
|
|
18285
|
+
if (!existsSync14(src)) {
|
|
17944
18286
|
continue;
|
|
17945
18287
|
}
|
|
17946
18288
|
const dest = join$1(worktreePath, relativePath);
|
|
@@ -18082,7 +18424,7 @@ ${e instanceof Error ? e.message : String(e)}`
|
|
|
18082
18424
|
await mkdir(patchesRoot, { recursive: true });
|
|
18083
18425
|
let dirName = base;
|
|
18084
18426
|
let counter6 = 0;
|
|
18085
|
-
while (
|
|
18427
|
+
while (existsSync14(join$1(patchesRoot, dirName))) {
|
|
18086
18428
|
counter6++;
|
|
18087
18429
|
dirName = `${base}-${counter6}`;
|
|
18088
18430
|
}
|
|
@@ -18254,8 +18596,7 @@ ${msg}`);
|
|
|
18254
18596
|
}), GIT_SETUP_TIMEOUT_MS = 1e4, HOOK_TIMEOUT_MS = 60000, COMMIT_COLLECTION_TIMEOUT_MS = 30000, MERGE_TO_HOST_TIMEOUT_MS = 30000, GIT_SETUP_MAX_RETRIES = 2, GIT_SETUP_RETRY_DELAY_MS = 250, TRANSIENT_EXEC_EXIT_CODES, isTransientExecError = (err) => err._tag === "ExecError" && err.exitCode !== undefined && TRANSIENT_EXEC_EXIT_CODES.has(err.exitCode), execOk2 = (sandbox3, command, options) => Effect_exports.flatMap(sandbox3.exec(command, options), (result) => result.exitCode !== 0 ? Effect_exports.fail(new ExecError({
|
|
18255
18597
|
command,
|
|
18256
18598
|
exitCode: result.exitCode,
|
|
18257
|
-
message:
|
|
18258
|
-
${result.stderr}`
|
|
18599
|
+
message: formatExecFailureMessage(command, result, sandbox3.maxOutputTailChars)
|
|
18259
18600
|
})) : Effect_exports.succeed(result)), execOkWithGitTimeout = (sandbox3, command, gitSetupTimeoutMs, options) => execOk2(sandbox3, command, options).pipe(withTimeout(gitSetupTimeoutMs, () => new GitSetupTimeoutError({
|
|
18260
18601
|
message: `Git command timed out after ${gitSetupTimeoutMs}ms: ${command}`,
|
|
18261
18602
|
timeoutMs: gitSetupTimeoutMs,
|
|
@@ -18591,7 +18932,7 @@ ${result.stderr}`
|
|
|
18591
18932
|
yield* Effect_exports.gen(function* () {
|
|
18592
18933
|
for (const relativePath of pathsToCopy) {
|
|
18593
18934
|
const hostPath = join$1(options.hostRepoDir, relativePath);
|
|
18594
|
-
if (!
|
|
18935
|
+
if (!existsSync14(hostPath)) {
|
|
18595
18936
|
continue;
|
|
18596
18937
|
}
|
|
18597
18938
|
const sandboxPath = posix2.join(handle.worktreePath, relativePath);
|
|
@@ -18634,6 +18975,7 @@ ${result.stderr}`
|
|
|
18634
18975
|
message: "copyIn is not supported for this sandbox provider"
|
|
18635
18976
|
}));
|
|
18636
18977
|
}, makeSandboxFromHandle = (handle) => ({
|
|
18978
|
+
maxOutputTailChars: handle.maxOutputTailChars,
|
|
18637
18979
|
exec: (command, options) => Effect_exports.tryPromise({
|
|
18638
18980
|
try: () => handle.exec(command, options),
|
|
18639
18981
|
catch: (e) => new ExecError({
|
|
@@ -18732,7 +19074,7 @@ Worktree removed (no uncommitted changes)`);
|
|
|
18732
19074
|
};
|
|
18733
19075
|
}
|
|
18734
19076
|
const resolvedHostPath = resolveHostPath(m.hostPath);
|
|
18735
|
-
if (!
|
|
19077
|
+
if (!existsSync14(resolvedHostPath)) {
|
|
18736
19078
|
throw new Error(`Mount hostPath does not exist: ${m.hostPath}` + (m.hostPath !== resolvedHostPath ? ` (resolved to ${resolvedHostPath})` : ""));
|
|
18737
19079
|
}
|
|
18738
19080
|
return {
|
|
@@ -18845,7 +19187,7 @@ Worktree removed (no uncommitted changes)`);
|
|
|
18845
19187
|
const selinux = mount.volume !== undefined ? undefined : selinuxLabel || undefined;
|
|
18846
19188
|
const options = [mount.readonly ? "ro" : undefined, selinux].filter((option5) => option5 !== undefined).join(",");
|
|
18847
19189
|
return options ? `${base}:${options}` : base;
|
|
18848
|
-
}, processFileMountParents = (mounts, sandboxHomedir, statFn =
|
|
19190
|
+
}, processFileMountParents = (mounts, sandboxHomedir, statFn = statSync4) => {
|
|
18849
19191
|
const parentDirs = /* @__PURE__ */ new Set;
|
|
18850
19192
|
for (const mount of mounts) {
|
|
18851
19193
|
let isFile2;
|
|
@@ -19846,7 +20188,7 @@ var init_chunk_VOG34SRF = __esm(() => {
|
|
|
19846
20188
|
}
|
|
19847
20189
|
return [];
|
|
19848
20190
|
});
|
|
19849
|
-
|
|
20191
|
+
join13 = /* @__PURE__ */ dual(2, (self, sep2) => fromIterable2(self).join(sep2));
|
|
19850
20192
|
TypeId4 = /* @__PURE__ */ Symbol.for("effect/Chunk");
|
|
19851
20193
|
emptyArray = [];
|
|
19852
20194
|
_equivalence3 = /* @__PURE__ */ getEquivalence3(equals);
|
|
@@ -53503,6 +53845,7 @@ var noSandbox = (options) => ({
|
|
|
53503
53845
|
const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;
|
|
53504
53846
|
const handle = {
|
|
53505
53847
|
worktreePath,
|
|
53848
|
+
maxOutputTailChars,
|
|
53506
53849
|
exec: (command, opts) => {
|
|
53507
53850
|
const cwd = opts?.cwd ?? worktreePath;
|
|
53508
53851
|
const isWindows = process.platform === "win32";
|
|
@@ -53599,7 +53942,7 @@ var init_chunk_62WN33RK = __esm(() => {
|
|
|
53599
53942
|
// ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/index.js
|
|
53600
53943
|
import { createRequire as createRequire8 } from "module";
|
|
53601
53944
|
import { mkdirSync as mkdirSync6, appendFileSync } from "fs";
|
|
53602
|
-
import path, { join as
|
|
53945
|
+
import path, { join as join15, posix as posix4, dirname as dirname7, relative as relative3 } from "path";
|
|
53603
53946
|
import { styleText as styleText2 } from "util";
|
|
53604
53947
|
import * as clack2 from "@clack/prompts";
|
|
53605
53948
|
import { readdir as readdir2, access as access3, readFile as readFile3, mkdir as mkdir3, writeFile as writeFile3, rm as rm3 } from "fs/promises";
|
|
@@ -53668,7 +54011,7 @@ async function run6(options) {
|
|
|
53668
54011
|
const targetBranch = effectiveBranchType === "merge-to-head" ? currentHostBranch : undefined;
|
|
53669
54012
|
const resolvedLogging = options.logging ?? {
|
|
53670
54013
|
type: "file",
|
|
53671
|
-
path:
|
|
54014
|
+
path: join15(hostRepoDir, ".sandcastle", "logs", buildLogFilename(resolvedBranch, targetBranch, options.name))
|
|
53672
54015
|
};
|
|
53673
54016
|
const displayLayer = resolvedLogging.type === "file" ? (() => {
|
|
53674
54017
|
printFileDisplayStartup({
|
|
@@ -54302,7 +54645,7 @@ ${errorDetail}`
|
|
|
54302
54645
|
}
|
|
54303
54646
|
return vars;
|
|
54304
54647
|
}), resolveEnv = (repoDir) => Effect_exports.gen(function* () {
|
|
54305
|
-
const sandcastleEnv = yield* parseEnvFile2(
|
|
54648
|
+
const sandcastleEnv = yield* parseEnvFile2(join15(repoDir, ".sandcastle", ".env"));
|
|
54306
54649
|
const result = {};
|
|
54307
54650
|
for (const key of Object.keys(sandcastleEnv)) {
|
|
54308
54651
|
const value = sandcastleEnv[key] || process.env[key];
|
|
@@ -54524,11 +54867,11 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54524
54867
|
const normalized = isRoot ? cwd : cwd.replace(/[\\/]+$/, "");
|
|
54525
54868
|
return normalized.replace(/^([A-Za-z]):/, "$1").replace(/[\\/]/g, "-");
|
|
54526
54869
|
}, claudeHostSessionPath = (cwd, id3, projectsDir) => {
|
|
54527
|
-
const base = projectsDir ??
|
|
54528
|
-
return
|
|
54870
|
+
const base = projectsDir ?? join15(process.env.HOME ?? "~", ".claude", "projects");
|
|
54871
|
+
return join15(base, encodeProjectPath(cwd), `${id3}.jsonl`);
|
|
54529
54872
|
}, claudeSandboxSessionPath = (cwd, id3, projectsDir) => posix4.join(projectsDir, encodeProjectPath(cwd), `${id3}.jsonl`), claudeSubagentsDirInSandbox = (cwd, id3, projectsDir) => posix4.join(projectsDir, encodeProjectPath(cwd), id3, "subagents"), claudeSubagentsDirOnHost = (cwd, id3, projectsDir) => {
|
|
54530
|
-
const base = projectsDir ??
|
|
54531
|
-
return
|
|
54873
|
+
const base = projectsDir ?? join15(process.env.HOME ?? "~", ".claude", "projects");
|
|
54874
|
+
return join15(base, encodeProjectPath(cwd), id3, "subagents");
|
|
54532
54875
|
}, listClaudeSubagentSessionsInSandbox = async (cwd, id3, handle, sandboxProjectsDir) => {
|
|
54533
54876
|
const dir = claudeSubagentsDirInSandbox(cwd, id3, sandboxProjectsDir);
|
|
54534
54877
|
const result = await handle.exec(`find ${JSON.stringify(dir)} -type f -name ${JSON.stringify("agent-*.jsonl")} 2>/dev/null`);
|
|
@@ -54540,7 +54883,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54540
54883
|
return stdout.split(`
|
|
54541
54884
|
`).filter((line) => line !== "");
|
|
54542
54885
|
}, findClaudeSessionOnHost = async (id3, projectsDir) => {
|
|
54543
|
-
const root = projectsDir ??
|
|
54886
|
+
const root = projectsDir ?? join15(process.env.HOME ?? "~", ".claude", "projects");
|
|
54544
54887
|
let entries;
|
|
54545
54888
|
try {
|
|
54546
54889
|
entries = await readdir2(root, { withFileTypes: true });
|
|
@@ -54550,7 +54893,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54550
54893
|
for (const entry of entries) {
|
|
54551
54894
|
if (!entry.isDirectory())
|
|
54552
54895
|
continue;
|
|
54553
|
-
const candidate =
|
|
54896
|
+
const candidate = join15(root, entry.name, `${id3}.jsonl`);
|
|
54554
54897
|
if (await pathExists(candidate)) {
|
|
54555
54898
|
return { path: candidate, searchedRoot: root };
|
|
54556
54899
|
}
|
|
@@ -54586,7 +54929,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54586
54929
|
return;
|
|
54587
54930
|
}
|
|
54588
54931
|
for (const entry of entries) {
|
|
54589
|
-
const child =
|
|
54932
|
+
const child = join15(dir, entry.name);
|
|
54590
54933
|
if (entry.isFile() && isCodexSessionFilename(entry.name, id3)) {
|
|
54591
54934
|
return child;
|
|
54592
54935
|
}
|
|
@@ -54600,11 +54943,11 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54600
54943
|
};
|
|
54601
54944
|
return visit(rootDir);
|
|
54602
54945
|
}, findCodexSessionOnHost = async (id3, sessionsDir) => {
|
|
54603
|
-
const root = sessionsDir ??
|
|
54946
|
+
const root = sessionsDir ?? join15(process.env.HOME ?? "~", ".codex", "sessions");
|
|
54604
54947
|
const path2 = await findCodexSessionPath(root, id3);
|
|
54605
54948
|
return { path: path2, searchedRoot: root };
|
|
54606
54949
|
}, locateCodexHostSession = async (id3, sessionsDir) => {
|
|
54607
|
-
const root = sessionsDir ??
|
|
54950
|
+
const root = sessionsDir ?? join15(process.env.HOME ?? "~", ".codex", "sessions");
|
|
54608
54951
|
const path2 = await findCodexSessionPath(root, id3);
|
|
54609
54952
|
if (!path2)
|
|
54610
54953
|
throw new Error(`session ${id3} not found in ${root}`);
|
|
@@ -54680,7 +55023,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54680
55023
|
} catch {}
|
|
54681
55024
|
return [];
|
|
54682
55025
|
}, CURSOR_PRINT_PROMPT_MAX_BYTES, readSandboxFile = async (handle, sandboxPath, tag) => {
|
|
54683
|
-
const tmpPath =
|
|
55026
|
+
const tmpPath = join15(tmpdir4(), `sandcastle-${tag}-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`);
|
|
54684
55027
|
await handle.copyFileOut(sandboxPath, tmpPath);
|
|
54685
55028
|
try {
|
|
54686
55029
|
return await readFile3(tmpPath, "utf-8");
|
|
@@ -54688,7 +55031,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54688
55031
|
await rm3(tmpPath, { force: true }).catch(() => {});
|
|
54689
55032
|
}
|
|
54690
55033
|
}, writeSandboxFile = async (handle, sandboxPath, content, tag) => {
|
|
54691
|
-
const tmpPath =
|
|
55034
|
+
const tmpPath = join15(tmpdir4(), `sandcastle-${tag}-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`);
|
|
54692
55035
|
await writeFile3(tmpPath, content);
|
|
54693
55036
|
try {
|
|
54694
55037
|
await handle.exec(`mkdir -p ${JSON.stringify(posix4.dirname(sandboxPath))}`);
|
|
@@ -54738,7 +55081,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54738
55081
|
sourcePath: sandboxSubagentPath,
|
|
54739
55082
|
fromCwd: sandboxCwd,
|
|
54740
55083
|
toCwd: hostCwd,
|
|
54741
|
-
destPath:
|
|
55084
|
+
destPath: join15(hostSubagentsDir, posix4.basename(sandboxSubagentPath)),
|
|
54742
55085
|
tag: "claude-sub"
|
|
54743
55086
|
});
|
|
54744
55087
|
} catch (err) {
|
|
@@ -54775,8 +55118,8 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54775
55118
|
const located = await locateCodexSandboxSession(sessionId, handle, sandboxSessionsDir);
|
|
54776
55119
|
const jsonl = await readSandboxFile(handle, located.path, "codex-cap");
|
|
54777
55120
|
const rewritten = transferCodexSession(jsonl, sandboxCwd, hostCwd);
|
|
54778
|
-
const root = hostSessionsDir ??
|
|
54779
|
-
const target =
|
|
55121
|
+
const root = hostSessionsDir ?? join15(process.env.HOME ?? "~", ".codex", "sessions");
|
|
55122
|
+
const target = join15(root, located.relativePath);
|
|
54780
55123
|
await mkdir3(dirname7(target), { recursive: true });
|
|
54781
55124
|
await writeFile3(target, rewritten);
|
|
54782
55125
|
capturedPaths.set(sessionId, target);
|
|
@@ -54937,6 +55280,7 @@ var init_dist = __esm(() => {
|
|
|
54937
55280
|
init_chunk_BIWNFKGV();
|
|
54938
55281
|
init_chunk_62WN33RK();
|
|
54939
55282
|
init_chunk_NGBM7T3E();
|
|
55283
|
+
init_chunk_NGBM7T3E();
|
|
54940
55284
|
createRequire8(import.meta.url);
|
|
54941
55285
|
AgentStreamEmitter = class extends Context_exports.Tag("AgentStreamEmitter")() {
|
|
54942
55286
|
};
|
|
@@ -54982,7 +55326,7 @@ var init_dist = __esm(() => {
|
|
|
54982
55326
|
});
|
|
54983
55327
|
|
|
54984
55328
|
// src/services/settings.ts
|
|
54985
|
-
import { existsSync as
|
|
55329
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
54986
55330
|
import { dirname as dirname8 } from "path";
|
|
54987
55331
|
import * as z3 from "zod";
|
|
54988
55332
|
import { eq as eq10 } from "drizzle-orm";
|
|
@@ -54990,10 +55334,10 @@ function hostDefaults(logicalCpus) {
|
|
|
54990
55334
|
return { ...DEFAULTS, burnConcurrency: resolveDefaultBurnConcurrency(logicalCpus) };
|
|
54991
55335
|
}
|
|
54992
55336
|
function readRawConfig(configFile) {
|
|
54993
|
-
if (!
|
|
55337
|
+
if (!existsSync15(configFile))
|
|
54994
55338
|
return {};
|
|
54995
55339
|
try {
|
|
54996
|
-
const parsed = JSON.parse(
|
|
55340
|
+
const parsed = JSON.parse(readFileSync7(configFile, "utf8"));
|
|
54997
55341
|
if (typeof parsed !== "object" || parsed === null)
|
|
54998
55342
|
return {};
|
|
54999
55343
|
return foldLegacyModelConfig(parsed);
|
|
@@ -55020,6 +55364,7 @@ function projectOverrides(ctx, projectId) {
|
|
|
55020
55364
|
const row = ctx.db.select({
|
|
55021
55365
|
model: projects.model,
|
|
55022
55366
|
sandbox: projects.sandbox,
|
|
55367
|
+
sandboxImage: projects.sandboxImage,
|
|
55023
55368
|
sessionBranch: projects.sessionBranch,
|
|
55024
55369
|
devCommand: projects.devCommand,
|
|
55025
55370
|
setupCommand: projects.setupCommand,
|
|
@@ -55033,6 +55378,7 @@ function projectOverrides(ctx, projectId) {
|
|
|
55033
55378
|
return {
|
|
55034
55379
|
model: row?.model ?? null,
|
|
55035
55380
|
sandbox: row?.sandbox ?? null,
|
|
55381
|
+
sandboxImage: row?.sandboxImage ?? null,
|
|
55036
55382
|
sessionBranch: row?.sessionBranch ?? null,
|
|
55037
55383
|
devCommand: row?.devCommand ?? null,
|
|
55038
55384
|
setupCommand: row?.setupCommand ?? null,
|
|
@@ -55235,6 +55581,7 @@ var init_settings = __esm(() => {
|
|
|
55235
55581
|
key: "sandboxImage",
|
|
55236
55582
|
configKey: "sandboxImage",
|
|
55237
55583
|
envVar: "RUNCASTLE_SANDBOX_IMAGE",
|
|
55584
|
+
projectColumn: "sandboxImage",
|
|
55238
55585
|
restartRequired: false,
|
|
55239
55586
|
valueSchema: z3.string().min(1),
|
|
55240
55587
|
parseEnv: idEnv
|
|
@@ -55362,9 +55709,8 @@ var init_settings = __esm(() => {
|
|
|
55362
55709
|
});
|
|
55363
55710
|
|
|
55364
55711
|
// src/services/setup.ts
|
|
55365
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
55366
|
-
import { dirname as dirname9, join as
|
|
55367
|
-
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
55712
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync8, readdirSync as readdirSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
55713
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
55368
55714
|
async function writeGitIdentity(exec3, identity4) {
|
|
55369
55715
|
const name = identity4.name.trim();
|
|
55370
55716
|
const email = identity4.email.trim();
|
|
@@ -55464,55 +55810,26 @@ async function resolveRuntime(exec3, preferred) {
|
|
|
55464
55810
|
return other;
|
|
55465
55811
|
return preferred;
|
|
55466
55812
|
}
|
|
55467
|
-
function
|
|
55468
|
-
try {
|
|
55469
|
-
const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
|
|
55470
|
-
let dir = dirname9(fileURLToPath6(mainUrl));
|
|
55471
|
-
for (let hops = 0;hops < 6; hops++) {
|
|
55472
|
-
const manifestPath = join15(dir, "package.json");
|
|
55473
|
-
if (existsSync15(manifestPath)) {
|
|
55474
|
-
const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
|
|
55475
|
-
if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
|
|
55476
|
-
return join15(dir, manifest.bin.sandcastle);
|
|
55477
|
-
}
|
|
55478
|
-
}
|
|
55479
|
-
const parent = dirname9(dir);
|
|
55480
|
-
if (parent === dir)
|
|
55481
|
-
break;
|
|
55482
|
-
dir = parent;
|
|
55483
|
-
}
|
|
55484
|
-
return null;
|
|
55485
|
-
} catch {
|
|
55486
|
-
return null;
|
|
55487
|
-
}
|
|
55488
|
-
}
|
|
55489
|
-
function terminalSpec(kind, opts) {
|
|
55813
|
+
function terminalSpec(kind) {
|
|
55490
55814
|
if (kind === "setup-token")
|
|
55491
55815
|
return { cmd: "claude", args: ["setup-token"] };
|
|
55492
55816
|
for (const spec of Object.values(RUNTIME_SPECS)) {
|
|
55493
55817
|
if (kind === LOGIN_TERMINAL_KIND[spec.runtime])
|
|
55494
55818
|
return { cmd: spec.bin, args: spec.loginArgs };
|
|
55495
55819
|
}
|
|
55496
|
-
|
|
55497
|
-
throw new NotFoundError("The bundled sandcastle CLI (@ai-hero/sandcastle) could not be located \u2014 reinstall runcastle.");
|
|
55498
|
-
}
|
|
55499
|
-
return {
|
|
55500
|
-
cmd: "node",
|
|
55501
|
-
args: [opts.sandcastleBin, opts.runtime, "build-image", "--image-name", opts.imageName]
|
|
55502
|
-
};
|
|
55820
|
+
throw new InvalidInputError(`no terminal command for ${kind}`);
|
|
55503
55821
|
}
|
|
55504
55822
|
function refreshSandcastleConfig(templateDir, targetDir) {
|
|
55505
|
-
const dir =
|
|
55823
|
+
const dir = join16(targetDir, ".sandcastle");
|
|
55506
55824
|
mkdirSync8(dir, { recursive: true });
|
|
55507
55825
|
for (const name of readdirSync4(templateDir)) {
|
|
55508
|
-
copyFileSync2(
|
|
55826
|
+
copyFileSync2(join16(templateDir, name), join16(dir, name));
|
|
55509
55827
|
}
|
|
55510
55828
|
return dir;
|
|
55511
55829
|
}
|
|
55512
55830
|
function prepareSandboxBuildContext(templateDir = sandcastleTemplateDir(), target = sandboxBuildDir()) {
|
|
55513
55831
|
mkdirSync8(target, { recursive: true });
|
|
55514
|
-
refreshSandcastleConfig(templateDir, target);
|
|
55515
|
-
return target;
|
|
55832
|
+
return refreshSandcastleConfig(templateDir, target);
|
|
55516
55833
|
}
|
|
55517
55834
|
function createCredentialVerifier(exec3, runtime5 = DEFAULT_RUNTIME) {
|
|
55518
55835
|
const spec = RUNTIME_SPECS[runtime5];
|
|
@@ -55547,7 +55864,7 @@ function createCredentialVerifier(exec3, runtime5 = DEFAULT_RUNTIME) {
|
|
|
55547
55864
|
function fileAfkTokenIo(exec3, runtime5 = DEFAULT_RUNTIME) {
|
|
55548
55865
|
const path2 = envPath();
|
|
55549
55866
|
return {
|
|
55550
|
-
read: () =>
|
|
55867
|
+
read: () => existsSync16(path2) ? readFileSync8(path2, "utf8") : "",
|
|
55551
55868
|
write: (content) => {
|
|
55552
55869
|
mkdirSync8(dirname9(path2), { recursive: true });
|
|
55553
55870
|
writeFileSync6(path2, content, "utf8");
|
|
@@ -55791,13 +56108,13 @@ var init_no_sandbox = __esm(() => {
|
|
|
55791
56108
|
});
|
|
55792
56109
|
|
|
55793
56110
|
// src/workflows/review-ticket.ts
|
|
55794
|
-
import { existsSync as
|
|
55795
|
-
import { delimiter, join as
|
|
56111
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
56112
|
+
import { delimiter, join as join17 } from "path";
|
|
55796
56113
|
function reviewTemplatePath(ticket) {
|
|
55797
56114
|
return burnerAssetPath(ticket?.passKind === "verification" ? "verify-fixes.md" : "review-ticket.md");
|
|
55798
56115
|
}
|
|
55799
56116
|
function renderReviewPrompt(templateOrTicket, values) {
|
|
55800
|
-
const template = typeof templateOrTicket === "string" ? templateOrTicket :
|
|
56117
|
+
const template = typeof templateOrTicket === "string" ? templateOrTicket : readFileSync9(reviewTemplatePath(templateOrTicket), "utf8");
|
|
55801
56118
|
return renderTemplate(template, values);
|
|
55802
56119
|
}
|
|
55803
56120
|
function findOnPath(bin, env = process.env, platform = process.platform) {
|
|
@@ -55805,8 +56122,8 @@ function findOnPath(bin, env = process.env, platform = process.platform) {
|
|
|
55805
56122
|
const suffixes = platform === "win32" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
55806
56123
|
for (const dir of dirs) {
|
|
55807
56124
|
for (const suffix of suffixes) {
|
|
55808
|
-
const candidate =
|
|
55809
|
-
if (
|
|
56125
|
+
const candidate = join17(dir, `${bin}${suffix}`);
|
|
56126
|
+
if (existsSync17(candidate))
|
|
55810
56127
|
return candidate;
|
|
55811
56128
|
}
|
|
55812
56129
|
}
|
|
@@ -55828,7 +56145,7 @@ function buildDriveAvailability(browserPath, devCommand, inheritedMode) {
|
|
|
55828
56145
|
}
|
|
55829
56146
|
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.";
|
|
55830
56147
|
}
|
|
55831
|
-
function inheritedReviewMode(verifiedTicketId, fileExists2 =
|
|
56148
|
+
function inheritedReviewMode(verifiedTicketId, fileExists2 = existsSync17) {
|
|
55832
56149
|
return verifiedTicketId && fileExists2(reviewWalkthroughPath(verifiedTicketId)) ? "drive" : "gates";
|
|
55833
56150
|
}
|
|
55834
56151
|
function buildDriveInstructions(instructions) {
|
|
@@ -55867,18 +56184,18 @@ function writeReviewArtifacts(ticket, runId, config2) {
|
|
|
55867
56184
|
const dir = reviewDir(ticket.id);
|
|
55868
56185
|
rmSync2(dir, { recursive: true, force: true });
|
|
55869
56186
|
mkdirSync9(dir, { recursive: true });
|
|
55870
|
-
const mcpConfigPath =
|
|
56187
|
+
const mcpConfigPath = join17(dir, "mcp.json");
|
|
55871
56188
|
const mcpConfig = renderRunMcpConfig(runId, config2);
|
|
55872
56189
|
writeFileSync7(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
55873
56190
|
return {
|
|
55874
56191
|
dir,
|
|
55875
56192
|
mcp: { path: mcpConfigPath, config: mcpConfig },
|
|
55876
|
-
digestPath:
|
|
55877
|
-
blockedPath:
|
|
56193
|
+
digestPath: join17(dir, "DIGEST.md"),
|
|
56194
|
+
blockedPath: join17(dir, "BLOCKED.md"),
|
|
55878
56195
|
walkthroughPath: reviewWalkthroughPath(ticket.id)
|
|
55879
56196
|
};
|
|
55880
56197
|
}
|
|
55881
|
-
function shouldStopAfterDigest(iteration, digestPath, fileExists2 =
|
|
56198
|
+
function shouldStopAfterDigest(iteration, digestPath, fileExists2 = existsSync17) {
|
|
55882
56199
|
return iteration >= 2 && fileExists2(digestPath);
|
|
55883
56200
|
}
|
|
55884
56201
|
async function releaseDriveQuietly() {
|
|
@@ -55956,7 +56273,7 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
|
|
|
55956
56273
|
maxIterations: deps.config.burnMaxIterations,
|
|
55957
56274
|
logging: {
|
|
55958
56275
|
type: "file",
|
|
55959
|
-
path:
|
|
56276
|
+
path: join17(logsDir(), `review-${feature.id}-${ticket.seq}.log`),
|
|
55960
56277
|
onAgentStreamEvent: onStreamEvent
|
|
55961
56278
|
}
|
|
55962
56279
|
};
|
|
@@ -56145,6 +56462,7 @@ var dockerExec = (args2) => Effect_exports.async((resume2) => {
|
|
|
56145
56462
|
const unregisterShutdown = registerShutdown(removeContainerSync);
|
|
56146
56463
|
const handle = {
|
|
56147
56464
|
worktreePath,
|
|
56465
|
+
maxOutputTailChars,
|
|
56148
56466
|
exec: (command, opts) => {
|
|
56149
56467
|
const effectiveCommand = opts?.sudo ? `sudo ${command}` : command;
|
|
56150
56468
|
const args2 = ["exec"];
|
|
@@ -56405,6 +56723,7 @@ var podman = (options) => {
|
|
|
56405
56723
|
const unregisterShutdown = registerShutdown(removeContainerSync);
|
|
56406
56724
|
const handle = {
|
|
56407
56725
|
worktreePath,
|
|
56726
|
+
maxOutputTailChars,
|
|
56408
56727
|
exec: (command, opts) => {
|
|
56409
56728
|
const effectiveCommand = opts?.sudo ? `sudo ${command}` : command;
|
|
56410
56729
|
const args2 = ["exec"];
|
|
@@ -56558,13 +56877,47 @@ var init_podman = __esm(() => {
|
|
|
56558
56877
|
});
|
|
56559
56878
|
|
|
56560
56879
|
// src/workflows/ticket-burner.ts
|
|
56561
|
-
import { existsSync as
|
|
56562
|
-
import { dirname as dirname10, join as
|
|
56563
|
-
import { fileURLToPath as
|
|
56880
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync10, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
|
|
56881
|
+
import { dirname as dirname10, join as join18, posix as posixPath, win32 as winPath } from "path";
|
|
56882
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
56564
56883
|
function missingImageRuntimeMessage(runtime5, image) {
|
|
56565
56884
|
const binary = RUNTIME_BINARY[runtime5];
|
|
56566
56885
|
return `${binary} is not installed in image ${image} \u2014 the image predates the burner Dockerfile. Rebuild it from Settings \u2192 Burns (Rebuild image).`;
|
|
56567
56886
|
}
|
|
56887
|
+
function extractCommandNames(command) {
|
|
56888
|
+
const names = [];
|
|
56889
|
+
for (const segment of (command ?? "").split(COMMAND_SEPARATORS)) {
|
|
56890
|
+
const words = segment.replace(/[(){}'"`]/g, " ").split(/\s+/).filter(Boolean);
|
|
56891
|
+
const word = words.find((w) => !ASSIGNMENT_PREFIX.test(w)) ?? "";
|
|
56892
|
+
if (!PLAIN_COMMAND_NAME.test(word) || NON_COMMAND_WORDS.has(word))
|
|
56893
|
+
continue;
|
|
56894
|
+
if (!names.includes(word))
|
|
56895
|
+
names.push(word);
|
|
56896
|
+
}
|
|
56897
|
+
return names;
|
|
56898
|
+
}
|
|
56899
|
+
function preflightCommandNames(input) {
|
|
56900
|
+
return [
|
|
56901
|
+
...new Set([
|
|
56902
|
+
input.agentBinary,
|
|
56903
|
+
...extractCommandNames(input.setupCommand),
|
|
56904
|
+
...extractCommandNames(input.verifyCommands)
|
|
56905
|
+
])
|
|
56906
|
+
].sort();
|
|
56907
|
+
}
|
|
56908
|
+
function buildToolchainProbeArgs(image, names) {
|
|
56909
|
+
const script = `for c in ${names.join(" ")}; do command -v "$c" >/dev/null 2>&1 || echo "$c"; done`;
|
|
56910
|
+
return ["run", "--rm", "--entrypoint", "sh", image, "-c", script];
|
|
56911
|
+
}
|
|
56912
|
+
function parseMissingCommands(stdout, names) {
|
|
56913
|
+
const absent = new Set(stdout.split(`
|
|
56914
|
+
`).map((line) => line.trim()));
|
|
56915
|
+
return names.filter((name) => absent.has(name));
|
|
56916
|
+
}
|
|
56917
|
+
function missingToolchainMessage(names, image) {
|
|
56918
|
+
const one = names.length === 1;
|
|
56919
|
+
return `${names.join(", ")} ${one ? "is" : "are"} not installed in image ${image} \u2014 add ${one ? "it" : "them"} to .runcastle/sandbox/Dockerfile and rebuild.`;
|
|
56920
|
+
}
|
|
56568
56921
|
function burnAuthReady(runtime5, token, loggedIn = codexLoggedIn) {
|
|
56569
56922
|
if (token !== undefined)
|
|
56570
56923
|
return true;
|
|
@@ -56580,6 +56933,9 @@ function verificationDue(tickets2, runTicketIds) {
|
|
|
56580
56933
|
const verifies = [...reviews].reverse().find((ticket) => ticket.status === "done") ?? null;
|
|
56581
56934
|
if (lastReview?.status === "failed")
|
|
56582
56935
|
return { due: false, landed: [], verifies };
|
|
56936
|
+
const failed = inRun.some((ticket) => !isReviewTicket(ticket) && ticket.status === "failed");
|
|
56937
|
+
if (failed)
|
|
56938
|
+
return { due: false, landed: [], verifies };
|
|
56583
56939
|
const cutoff = verifies?.completedAt ?? Number.NEGATIVE_INFINITY;
|
|
56584
56940
|
const landed = inRun.filter((ticket) => !isReviewTicket(ticket) && ticket.status === "done" && ticket.completedAt !== null && (ticket.completedAt > cutoff || ticket.completedAt === cutoff && verifies !== null && ticket.seq > verifies.seq));
|
|
56585
56941
|
return { due: landed.length > 0, landed, verifies };
|
|
@@ -56721,10 +57077,10 @@ _(The "${DROPPED_MAP_SECTIONS.join('" and "')}" sections are omitted here: they
|
|
|
56721
57077
|
}
|
|
56722
57078
|
function buildProjectStandards(repoPath) {
|
|
56723
57079
|
const present = [];
|
|
56724
|
-
if (
|
|
57080
|
+
if (existsSync18(join18(repoPath, AGENT_CONVENTIONS_FILE))) {
|
|
56725
57081
|
present.push("`CLAUDE.md` \u2014 this repo's agent-facing conventions, and the highest authority on them");
|
|
56726
57082
|
}
|
|
56727
|
-
if (
|
|
57083
|
+
if (existsSync18(join18(repoPath, CHARTER_FILE))) {
|
|
56728
57084
|
present.push(`\`${CHARTER_FILE}\` \u2014 the project charter: what this codebase is for`);
|
|
56729
57085
|
}
|
|
56730
57086
|
const adrs = listLiveAdrs(repoPath).map((a) => a.relPath);
|
|
@@ -56755,7 +57111,7 @@ function buildDriveNotes(settings) {
|
|
|
56755
57111
|
add5("stop", settings.driveStopCommand);
|
|
56756
57112
|
add5("dev server", settings.devCommand);
|
|
56757
57113
|
add5("db reset", settings.dbResetCommand);
|
|
56758
|
-
const hasDir =
|
|
57114
|
+
const hasDir = existsSync18(join18(settings.repoPath, ".runcastle"));
|
|
56759
57115
|
if (configured.length === 0 && !hasDir) {
|
|
56760
57116
|
return [
|
|
56761
57117
|
"This project has no test-drive machinery configured, so there is nothing here for you to keep in step \u2014 do not invent any.",
|
|
@@ -56975,10 +57331,10 @@ function parseSetupMarker(text3) {
|
|
|
56975
57331
|
return { cold: m[1] === "1", syncMs: Number(m[2]), installMs: Number(m[3]) };
|
|
56976
57332
|
}
|
|
56977
57333
|
function takeSetupMarker(worktreePath) {
|
|
56978
|
-
const file =
|
|
57334
|
+
const file = join18(worktreePath, SETUP_MARKER_FILE);
|
|
56979
57335
|
let text3;
|
|
56980
57336
|
try {
|
|
56981
|
-
text3 =
|
|
57337
|
+
text3 = readFileSync10(file, "utf8");
|
|
56982
57338
|
} catch {
|
|
56983
57339
|
return;
|
|
56984
57340
|
}
|
|
@@ -57051,7 +57407,7 @@ function attachedNoteIds(context5) {
|
|
|
57051
57407
|
return ids3;
|
|
57052
57408
|
}
|
|
57053
57409
|
function attachmentSources(context5) {
|
|
57054
|
-
return attachedNoteIds(context5).map(annotationPath).filter((p) =>
|
|
57410
|
+
return attachedNoteIds(context5).map(annotationPath).filter((p) => existsSync18(p));
|
|
57055
57411
|
}
|
|
57056
57412
|
function buildAttachmentCopyCommands(sources, platform = process.platform) {
|
|
57057
57413
|
if (sources.length === 0)
|
|
@@ -57071,7 +57427,7 @@ function buildAttachmentCopyCommands(sources, platform = process.platform) {
|
|
|
57071
57427
|
];
|
|
57072
57428
|
}
|
|
57073
57429
|
async function clearAttachments(workspacePath, repoPath) {
|
|
57074
|
-
rmSync3(
|
|
57430
|
+
rmSync3(join18(workspacePath, ATTACHMENTS_DIR), { recursive: true, force: true });
|
|
57075
57431
|
await unexcludePath(repoPath, ATTACHMENTS_PREFIX);
|
|
57076
57432
|
}
|
|
57077
57433
|
function buildVerifyNotes(config2) {
|
|
@@ -57093,12 +57449,12 @@ function buildVerifyNotes(config2) {
|
|
|
57093
57449
|
`);
|
|
57094
57450
|
}
|
|
57095
57451
|
function readRepoToolchain(repoPath) {
|
|
57096
|
-
const pkgPath =
|
|
57097
|
-
const hasPackageJson =
|
|
57452
|
+
const pkgPath = join18(repoPath, "package.json");
|
|
57453
|
+
const hasPackageJson = existsSync18(pkgPath);
|
|
57098
57454
|
let packageManagerField;
|
|
57099
57455
|
if (hasPackageJson) {
|
|
57100
57456
|
try {
|
|
57101
|
-
const pkg = JSON.parse(
|
|
57457
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
57102
57458
|
if (typeof pkg.packageManager === "string")
|
|
57103
57459
|
packageManagerField = pkg.packageManager;
|
|
57104
57460
|
} catch {}
|
|
@@ -57107,10 +57463,10 @@ function readRepoToolchain(repoPath) {
|
|
|
57107
57463
|
hasPackageJson,
|
|
57108
57464
|
...packageManagerField !== undefined ? { packageManagerField } : {},
|
|
57109
57465
|
lockfiles: {
|
|
57110
|
-
bun:
|
|
57111
|
-
pnpm:
|
|
57112
|
-
yarn:
|
|
57113
|
-
npm:
|
|
57466
|
+
bun: existsSync18(join18(repoPath, "bun.lock")) || existsSync18(join18(repoPath, "bun.lockb")),
|
|
57467
|
+
pnpm: existsSync18(join18(repoPath, "pnpm-lock.yaml")),
|
|
57468
|
+
yarn: existsSync18(join18(repoPath, "yarn.lock")),
|
|
57469
|
+
npm: existsSync18(join18(repoPath, "package-lock.json"))
|
|
57114
57470
|
}
|
|
57115
57471
|
};
|
|
57116
57472
|
}
|
|
@@ -57246,19 +57602,36 @@ function isMergeConflictError(err) {
|
|
|
57246
57602
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
57247
57603
|
return /conflict/i.test(msg) || /git branch -D/i.test(msg) || /automatic merge failed/i.test(msg);
|
|
57248
57604
|
}
|
|
57605
|
+
function refusalNamingCredential(refusal) {
|
|
57606
|
+
return new RegExp(`(?:${refusal}[^\\n]*${CREDENTIAL_SUBJECT}|${CREDENTIAL_SUBJECT}[^\\n]*${refusal})`, "i");
|
|
57607
|
+
}
|
|
57608
|
+
function missingCommandRegex(name) {
|
|
57609
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
57610
|
+
return new RegExp(`\\b${escaped}\\b[^\\n]*(?:exited with code 127|command not found|not found|no such file or directory)`, "i");
|
|
57611
|
+
}
|
|
57249
57612
|
function missingAgentBinaryMessage(err, runtime5, image) {
|
|
57250
57613
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
57251
57614
|
const binary = AGENT_BINARY[runtime5];
|
|
57252
|
-
|
|
57253
|
-
if (!missing.test(msg))
|
|
57615
|
+
if (!missingCommandRegex(binary).test(msg))
|
|
57254
57616
|
return;
|
|
57255
57617
|
return `${binary} is not installed in image ${image} \u2014 the image predates the burner Dockerfile. Rebuild it from Settings \u2192 Burns (Rebuild image).`;
|
|
57256
57618
|
}
|
|
57257
|
-
function
|
|
57619
|
+
function missingSetupBinaryMessage(err, setupCommand, image) {
|
|
57620
|
+
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
57621
|
+
const missing = extractCommandNames(setupCommand).find((name) => missingCommandRegex(name).test(msg));
|
|
57622
|
+
return missing ? missingToolchainMessage([missing], image) : undefined;
|
|
57623
|
+
}
|
|
57624
|
+
function classifyTicketRunError(err, runtime5, setupCommand) {
|
|
57258
57625
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
57259
57626
|
const runtimes = runtime5 ? [runtime5] : ["claude-code", "codex"];
|
|
57260
57627
|
const forRuntimes = (table) => runtimes.flatMap((r) => table[r]);
|
|
57261
57628
|
if (runtimes.some((r) => missingAgentBinaryMessage(msg, r, "image")))
|
|
57629
|
+
return "run-fatal";
|
|
57630
|
+
if ([...RUN_FATAL_ERROR_PATTERNS, ...forRuntimes(RUNTIME_RUN_FATAL_ERROR_PATTERNS)].some((p) => p.test(msg)))
|
|
57631
|
+
return "run-fatal";
|
|
57632
|
+
if (missingSetupBinaryMessage(msg, setupCommand, "image"))
|
|
57633
|
+
return "fatal";
|
|
57634
|
+
if (MISSING_COMMAND_PATTERNS.some((p) => p.test(msg)))
|
|
57262
57635
|
return "fatal";
|
|
57263
57636
|
if ([...FATAL_ERROR_PATTERNS, ...forRuntimes(RUNTIME_FATAL_ERROR_PATTERNS)].some((p) => p.test(msg)))
|
|
57264
57637
|
return "fatal";
|
|
@@ -57300,11 +57673,20 @@ function delayUnlessAborted(ms, signal) {
|
|
|
57300
57673
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
57301
57674
|
});
|
|
57302
57675
|
}
|
|
57303
|
-
|
|
57676
|
+
function stopTicketRun(ticketId) {
|
|
57677
|
+
return abortTicketRun(ticketId, new Error("ticket stopped by user"));
|
|
57678
|
+
}
|
|
57679
|
+
function haltTicketRun(ticketId, headline) {
|
|
57680
|
+
return abortTicketRun(ticketId, new RunHaltedAbort(headline));
|
|
57681
|
+
}
|
|
57682
|
+
function ticketStopReason(reason) {
|
|
57683
|
+
return reason instanceof RunHaltedAbort ? `stopped: run halted (${reason.headline})` : "stopped by user";
|
|
57684
|
+
}
|
|
57685
|
+
async function abortTicketRun(ticketId, reason) {
|
|
57304
57686
|
const controller = activeTicketAborts.get(ticketId);
|
|
57305
57687
|
if (!controller)
|
|
57306
57688
|
return { stopped: false, confirmed: true };
|
|
57307
|
-
controller.abort(
|
|
57689
|
+
controller.abort(reason);
|
|
57308
57690
|
const { confirmed } = await killRegistry().killAndWait(ticketId);
|
|
57309
57691
|
return { stopped: true, confirmed };
|
|
57310
57692
|
}
|
|
@@ -57368,11 +57750,7 @@ function errorHeadline(s) {
|
|
|
57368
57750
|
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
57369
57751
|
return causes.at(-1) ?? lines[0] ?? "";
|
|
57370
57752
|
}
|
|
57371
|
-
function
|
|
57372
|
-
const list = [...seqs].sort((a, b) => a - b).join(", ");
|
|
57373
|
-
return `> Reviewed with failed implementation ticket(s): ${list}.`;
|
|
57374
|
-
}
|
|
57375
|
-
async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
57753
|
+
async function burnTickets(ctx, tickets2, execute, concurrency = 1, ticketRuntime) {
|
|
57376
57754
|
const width = Math.max(1, Math.floor(concurrency));
|
|
57377
57755
|
const scheduled = [...tickets2];
|
|
57378
57756
|
const bySeq = indexBySeq(scheduled);
|
|
@@ -57380,10 +57758,23 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
57380
57758
|
const pending3 = new Set(scheduled.filter((t) => t.status === "pending").map((t) => t.seq));
|
|
57381
57759
|
const inFlight = new Map;
|
|
57382
57760
|
const digests = [];
|
|
57761
|
+
const deferred = [];
|
|
57762
|
+
const collapsed = [];
|
|
57383
57763
|
let admitted = 0;
|
|
57384
57764
|
let verificationChecked = false;
|
|
57385
|
-
|
|
57765
|
+
let halted;
|
|
57766
|
+
const satisfied = (s) => s === "done" || s === "cancelled";
|
|
57386
57767
|
const implementationsSettled = () => scheduled.every((t) => isReviewTicket(t) || !pending3.has(t.seq) && !inFlight.has(t.seq));
|
|
57768
|
+
const failedImplementations = () => scheduled.filter((t) => !isReviewTicket(t) && status3.get(t.seq) === "failed").map((t) => t.seq);
|
|
57769
|
+
const reviewGate = () => {
|
|
57770
|
+
if (!implementationsSettled())
|
|
57771
|
+
return "wait";
|
|
57772
|
+
if (failedImplementations().length > 0)
|
|
57773
|
+
return "defer";
|
|
57774
|
+
const implementations = scheduled.filter((t) => !isReviewTicket(t));
|
|
57775
|
+
const landed = implementations.some((t) => status3.get(t.seq) === "done");
|
|
57776
|
+
return implementations.length > 0 && !landed ? "collapsed" : "ready";
|
|
57777
|
+
};
|
|
57387
57778
|
const admitNewTickets = () => {
|
|
57388
57779
|
const added = [];
|
|
57389
57780
|
for (const t of ctx.listTickets?.() ?? []) {
|
|
@@ -57458,20 +57849,18 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
57458
57849
|
for (const b of t.blockedBy) {
|
|
57459
57850
|
const present = bySeq.has(b);
|
|
57460
57851
|
const bs = present ? status3.get(b) : undefined;
|
|
57461
|
-
if (!present
|
|
57852
|
+
if (!present)
|
|
57853
|
+
return { blockedBy: b, present };
|
|
57854
|
+
if (bs === "failed" && !isReviewTicket(t))
|
|
57462
57855
|
return { blockedBy: b, present };
|
|
57463
57856
|
}
|
|
57464
|
-
if (isReviewTicket(t)
|
|
57465
|
-
return
|
|
57466
|
-
return t.blockedBy.every((b) => satisfied(
|
|
57857
|
+
if (isReviewTicket(t))
|
|
57858
|
+
return reviewGate();
|
|
57859
|
+
return t.blockedBy.every((b) => satisfied(status3.get(b))) ? "ready" : "wait";
|
|
57467
57860
|
};
|
|
57468
57861
|
const harvestDigest2 = (t, digest) => {
|
|
57469
|
-
|
|
57470
|
-
|
|
57471
|
-
|
|
57472
|
-
`);
|
|
57473
|
-
if (body)
|
|
57474
|
-
digests.push({ seq: t.seq, title: t.title, digest: body });
|
|
57862
|
+
if (digest)
|
|
57863
|
+
digests.push({ seq: t.seq, title: t.title, digest });
|
|
57475
57864
|
};
|
|
57476
57865
|
const failTicket = (seq2, error, extra, digest) => {
|
|
57477
57866
|
const t = bySeq.get(seq2);
|
|
@@ -57484,10 +57873,34 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
57484
57873
|
if (extra)
|
|
57485
57874
|
ctx.emitEvent({ ...extra, ticketId: t.id });
|
|
57486
57875
|
};
|
|
57876
|
+
const haltRun = async (t, outcome) => {
|
|
57877
|
+
const halt = {
|
|
57878
|
+
runtime: outcome.runtime,
|
|
57879
|
+
ticketSeq: t.seq,
|
|
57880
|
+
headline: errorHeadline(outcome.error)
|
|
57881
|
+
};
|
|
57882
|
+
halted = halt;
|
|
57883
|
+
ctx.emitEvent({
|
|
57884
|
+
type: "run.halted",
|
|
57885
|
+
message: `run halted: ${halt.headline}`,
|
|
57886
|
+
ticketId: t.id,
|
|
57887
|
+
data: { runtime: halt.runtime, ticketSeq: halt.ticketSeq }
|
|
57888
|
+
});
|
|
57889
|
+
const doomed = [...inFlight.keys()].filter((other) => other !== t.seq).map((other) => bySeq.get(other)).filter((x) => x !== undefined).filter((x) => ticketRuntime === undefined || ticketRuntime(x) === outcome.runtime);
|
|
57890
|
+
await Promise.all(doomed.map((x) => haltTicketRun(x.id, halt.headline)));
|
|
57891
|
+
};
|
|
57892
|
+
const refreshedForLaunch = (t) => {
|
|
57893
|
+
const fresh3 = ctx.listTickets?.().find((row) => row.id === t.id);
|
|
57894
|
+
if (!fresh3)
|
|
57895
|
+
return t;
|
|
57896
|
+
bySeq.set(t.seq, fresh3);
|
|
57897
|
+
return fresh3;
|
|
57898
|
+
};
|
|
57487
57899
|
const runOne = async (seq2) => {
|
|
57488
|
-
const
|
|
57489
|
-
if (!
|
|
57900
|
+
const snapshot = bySeq.get(seq2);
|
|
57901
|
+
if (!snapshot)
|
|
57490
57902
|
return;
|
|
57903
|
+
const t = refreshedForLaunch(snapshot);
|
|
57491
57904
|
status3.set(seq2, "burning");
|
|
57492
57905
|
ctx.updateTicket(t.id, { status: "burning" });
|
|
57493
57906
|
mirrorFinding(t, "fixing");
|
|
@@ -57530,35 +57943,67 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
57530
57943
|
ticketId: t.id,
|
|
57531
57944
|
data: { error: outcome.error }
|
|
57532
57945
|
});
|
|
57946
|
+
if (outcome.runFatal && !halted) {
|
|
57947
|
+
await haltRun(t, { error: outcome.error, runtime: outcome.runFatal.runtime });
|
|
57948
|
+
}
|
|
57533
57949
|
}
|
|
57534
57950
|
if (isReviewTicket(t))
|
|
57535
57951
|
admitNewTickets();
|
|
57536
57952
|
};
|
|
57537
57953
|
while (pending3.size > 0 || inFlight.size > 0 || !verificationChecked) {
|
|
57538
57954
|
ctx.signal.throwIfAborted();
|
|
57539
|
-
if (
|
|
57955
|
+
if (halted && inFlight.size === 0)
|
|
57956
|
+
break;
|
|
57957
|
+
if (!halted && pending3.size === 0 && inFlight.size === 0) {
|
|
57540
57958
|
appendVerificationIfDue();
|
|
57541
57959
|
if (pending3.size === 0)
|
|
57542
57960
|
break;
|
|
57543
57961
|
}
|
|
57544
|
-
for (const seq2 of [...pending3]) {
|
|
57962
|
+
for (const seq2 of halted ? [] : [...pending3]) {
|
|
57545
57963
|
const st = readyState(seq2);
|
|
57546
|
-
if (
|
|
57547
|
-
|
|
57548
|
-
|
|
57549
|
-
|
|
57550
|
-
|
|
57964
|
+
if (st === "ready" || st === "wait")
|
|
57965
|
+
continue;
|
|
57966
|
+
pending3.delete(seq2);
|
|
57967
|
+
const t = bySeq.get(seq2);
|
|
57968
|
+
if (st === "defer") {
|
|
57969
|
+
deferred.push(seq2);
|
|
57551
57970
|
if (t) {
|
|
57971
|
+
const failed = failedImplementations();
|
|
57552
57972
|
ctx.emitEvent({
|
|
57553
|
-
type: "ticket.
|
|
57554
|
-
message: `ticket ${t.seq} ${
|
|
57973
|
+
type: "ticket.deferred",
|
|
57974
|
+
message: `ticket ${t.seq} deferred to the next burn: implementation ticket(s) ${failed.join(", ")} failed \u2014 retry or cancel them, then burn again`,
|
|
57555
57975
|
ticketId: t.id,
|
|
57556
|
-
data: {
|
|
57976
|
+
data: { failed }
|
|
57557
57977
|
});
|
|
57558
57978
|
}
|
|
57979
|
+
continue;
|
|
57980
|
+
}
|
|
57981
|
+
if (st === "collapsed") {
|
|
57982
|
+
collapsed.push(seq2);
|
|
57983
|
+
status3.set(seq2, "cancelled");
|
|
57984
|
+
if (t) {
|
|
57985
|
+
ctx.updateTicket(t.id, { status: "cancelled", error: NOTHING_LANDED });
|
|
57986
|
+
ctx.emitEvent({
|
|
57987
|
+
type: "ticket.cancelled",
|
|
57988
|
+
message: `ticket ${t.seq} cancelled: ${NOTHING_LANDED}`,
|
|
57989
|
+
ticketId: t.id,
|
|
57990
|
+
data: { reason: NOTHING_LANDED }
|
|
57991
|
+
});
|
|
57992
|
+
}
|
|
57993
|
+
continue;
|
|
57994
|
+
}
|
|
57995
|
+
const reason = st.present ? `blocked by failed ticket ${st.blockedBy}` : `blocked by missing ticket ${st.blockedBy}`;
|
|
57996
|
+
failTicket(seq2, reason);
|
|
57997
|
+
if (t) {
|
|
57998
|
+
ctx.emitEvent({
|
|
57999
|
+
type: "ticket.blocked",
|
|
58000
|
+
message: `ticket ${t.seq} ${reason}`,
|
|
58001
|
+
ticketId: t.id,
|
|
58002
|
+
data: { reason, blockedBy: st.blockedBy }
|
|
58003
|
+
});
|
|
57559
58004
|
}
|
|
57560
58005
|
}
|
|
57561
|
-
while (inFlight.size < width) {
|
|
58006
|
+
while (!halted && inFlight.size < width) {
|
|
57562
58007
|
const readySeq = [...pending3].find((seq2) => readyState(seq2) === "ready");
|
|
57563
58008
|
if (readySeq === undefined)
|
|
57564
58009
|
break;
|
|
@@ -57594,7 +58039,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
57594
58039
|
for (const s of status3.values())
|
|
57595
58040
|
if (s === "done")
|
|
57596
58041
|
done10 += 1;
|
|
57597
|
-
return { done: done10, admitted, digests };
|
|
58042
|
+
return { done: done10, admitted, digests, deferred, collapsed, ...halted ? { halted } : {} };
|
|
57598
58043
|
}
|
|
57599
58044
|
function composeRunDigest(entries) {
|
|
57600
58045
|
if (entries.length === 0)
|
|
@@ -57627,18 +58072,18 @@ async function burnRun(ctx, deps) {
|
|
|
57627
58072
|
return { status: "failed", summary: "burn aborted: auth token missing" };
|
|
57628
58073
|
}
|
|
57629
58074
|
if (deps.config.sandbox !== "noSandbox" && deps.exec) {
|
|
57630
|
-
const image = resolveSandboxImage(deps.config);
|
|
58075
|
+
const image = resolveSandboxImage(deps.config, ctx.project);
|
|
57631
58076
|
const binary = RUNTIME_BINARY[deps.runtime];
|
|
57632
|
-
const
|
|
57633
|
-
|
|
57634
|
-
|
|
57635
|
-
|
|
57636
|
-
|
|
57637
|
-
|
|
57638
|
-
|
|
57639
|
-
]
|
|
57640
|
-
if (
|
|
57641
|
-
const message = missingImageRuntimeMessage(deps.runtime, image);
|
|
58077
|
+
const prepared = resolvePreparedSettings(deps.config, ctx.project);
|
|
58078
|
+
const names = preflightCommandNames({
|
|
58079
|
+
agentBinary: binary,
|
|
58080
|
+
setupCommand: resolveSetupCommand(readRepoToolchain(ctx.project.repoPath), prepared.setupCommand),
|
|
58081
|
+
verifyCommands: prepared.verifyCommands
|
|
58082
|
+
});
|
|
58083
|
+
const probe = await deps.exec(deps.config.sandbox, buildToolchainProbeArgs(image, names));
|
|
58084
|
+
const missing = probe.ok && probe.code === 0 ? parseMissingCommands(probe.stdout, names) : [binary];
|
|
58085
|
+
if (missing.length > 0) {
|
|
58086
|
+
const message = missing.includes(binary) ? missingImageRuntimeMessage(deps.runtime, image) : missingToolchainMessage(missing, image);
|
|
57642
58087
|
ctx.emitEvent({ type: IMAGE_RUNTIME_MISSING_EVENT, message });
|
|
57643
58088
|
return { status: "failed", summary: message };
|
|
57644
58089
|
}
|
|
@@ -57653,12 +58098,22 @@ async function burnRun(ctx, deps) {
|
|
|
57653
58098
|
});
|
|
57654
58099
|
return { status: "failed", summary: `dependency cycle: ${path2}` };
|
|
57655
58100
|
}
|
|
57656
|
-
const { done: done10, admitted, digests } = await burnTickets(ctx, tickets2, gateTicketAuth(deps), deps.concurrency);
|
|
57657
|
-
const total = burnable.length + admitted;
|
|
57658
|
-
const
|
|
57659
|
-
|
|
58101
|
+
const { done: done10, admitted, digests, halted, deferred, collapsed } = await burnTickets(ctx, tickets2, gateTicketAuth(deps), deps.concurrency, deps.ticketRuntime);
|
|
58102
|
+
const total = burnable.length + admitted - collapsed.length;
|
|
58103
|
+
const cancelledCount = cancelled + collapsed.length;
|
|
58104
|
+
const counts = cancelledCount > 0 ? `${done10}/${total} tickets done (${cancelledCount} cancelled)` : `${done10}/${total} tickets done`;
|
|
58105
|
+
const summary = halted ? `run halted at ticket ${halted.ticketSeq}: ${halted.headline} \u2014 ${counts}` : deferred.length > 0 ? `${counts} \u2014 review deferred until the failed ticket(s) are retried or cancelled` : counts;
|
|
58106
|
+
ctx.emitEvent({
|
|
58107
|
+
type: "burn.summary",
|
|
58108
|
+
message: summary,
|
|
58109
|
+
data: { done: done10, total, cancelled: cancelledCount }
|
|
58110
|
+
});
|
|
57660
58111
|
const digest = composeRunDigest(digests);
|
|
57661
|
-
return {
|
|
58112
|
+
return {
|
|
58113
|
+
status: !halted && done10 === total ? "succeeded" : "failed",
|
|
58114
|
+
summary,
|
|
58115
|
+
...digest ? { digest } : {}
|
|
58116
|
+
};
|
|
57662
58117
|
}
|
|
57663
58118
|
function burnerTemplatePath() {
|
|
57664
58119
|
return burnerAssetPath("implement-ticket.md");
|
|
@@ -57667,8 +58122,8 @@ function resolverTemplatePath() {
|
|
|
57667
58122
|
return burnerAssetPath("resolve-conflict.md");
|
|
57668
58123
|
}
|
|
57669
58124
|
function burnerAssetPath(file) {
|
|
57670
|
-
const here = dirname10(
|
|
57671
|
-
return
|
|
58125
|
+
const here = dirname10(fileURLToPath6(import.meta.url));
|
|
58126
|
+
return join18(resolveSkillsRoot(here), "burner", file);
|
|
57672
58127
|
}
|
|
57673
58128
|
function listFeatureDocs(docsDir, prefix = "") {
|
|
57674
58129
|
const out = [];
|
|
@@ -57677,7 +58132,7 @@ function listFeatureDocs(docsDir, prefix = "") {
|
|
|
57677
58132
|
if (e.isFile() && e.name.endsWith(".md"))
|
|
57678
58133
|
out.push(rel);
|
|
57679
58134
|
else if (e.isDirectory() && !prefix)
|
|
57680
|
-
out.push(...listFeatureDocs(
|
|
58135
|
+
out.push(...listFeatureDocs(join18(docsDir, e.name), rel));
|
|
57681
58136
|
}
|
|
57682
58137
|
return out;
|
|
57683
58138
|
}
|
|
@@ -57691,11 +58146,11 @@ function readDocsDigest(projectId, slug) {
|
|
|
57691
58146
|
...missing ? { missing } : {}
|
|
57692
58147
|
});
|
|
57693
58148
|
const worktree = worktreeDir(projectId, slug);
|
|
57694
|
-
if (!
|
|
58149
|
+
if (!existsSync18(worktree)) {
|
|
57695
58150
|
return done10("_No talk worktree on disk \u2014 docs digest skipped._", "no-worktree");
|
|
57696
58151
|
}
|
|
57697
|
-
const docsDir =
|
|
57698
|
-
if (!
|
|
58152
|
+
const docsDir = join18(worktree, ...docsRel.split("/"));
|
|
58153
|
+
if (!existsSync18(docsDir)) {
|
|
57699
58154
|
return done10("_No docs/features dir in the talk worktree \u2014 docs digest skipped._", "no-docs-dir");
|
|
57700
58155
|
}
|
|
57701
58156
|
const names = listFeatureDocs(docsDir).sort((a, b) => agentDigestDocOrder(a) - agentDigestDocOrder(b) || a.localeCompare(b));
|
|
@@ -57711,7 +58166,7 @@ function readDocsDigest(projectId, slug) {
|
|
|
57711
58166
|
}
|
|
57712
58167
|
let content;
|
|
57713
58168
|
try {
|
|
57714
|
-
content =
|
|
58169
|
+
content = readFileSync10(join18(docsDir, ...name.split("/")), "utf8");
|
|
57715
58170
|
} catch {
|
|
57716
58171
|
continue;
|
|
57717
58172
|
}
|
|
@@ -57757,10 +58212,10 @@ function readAgentFile(dirs, file) {
|
|
|
57757
58212
|
for (const dir of dirs) {
|
|
57758
58213
|
if (!dir)
|
|
57759
58214
|
continue;
|
|
57760
|
-
const p =
|
|
57761
|
-
if (
|
|
58215
|
+
const p = join18(dir, file);
|
|
58216
|
+
if (existsSync18(p)) {
|
|
57762
58217
|
try {
|
|
57763
|
-
return
|
|
58218
|
+
return readFileSync10(p, "utf8");
|
|
57764
58219
|
} catch {}
|
|
57765
58220
|
}
|
|
57766
58221
|
}
|
|
@@ -57828,8 +58283,8 @@ function buildAgentExtraArgs(runtime5, options) {
|
|
|
57828
58283
|
function quoteArg(value) {
|
|
57829
58284
|
return `"${value}"`;
|
|
57830
58285
|
}
|
|
57831
|
-
function selectSandbox(config2, mounts = [], env = {}, kill = {}) {
|
|
57832
|
-
const imageOpts = buildSandboxOptions(config2, mounts, env, kill.containerName);
|
|
58286
|
+
function selectSandbox(config2, project3, mounts = [], env = {}, kill = {}) {
|
|
58287
|
+
const imageOpts = buildSandboxOptions(config2, project3, mounts, env, kill.containerName);
|
|
57833
58288
|
switch (config2.sandbox) {
|
|
57834
58289
|
case "docker":
|
|
57835
58290
|
return docker(imageOpts);
|
|
@@ -57841,9 +58296,9 @@ function selectSandbox(config2, mounts = [], env = {}, kill = {}) {
|
|
|
57841
58296
|
throw new Error(`no sandbox provider for sandbox "${config2.sandbox}" \u2014 refusing to run the agent unsandboxed`);
|
|
57842
58297
|
}
|
|
57843
58298
|
}
|
|
57844
|
-
function buildSandboxOptions(config2, mounts = [], env = {}, containerName) {
|
|
58299
|
+
function buildSandboxOptions(config2, project3, mounts = [], env = {}, containerName) {
|
|
57845
58300
|
return {
|
|
57846
|
-
imageName: resolveSandboxImage(config2),
|
|
58301
|
+
imageName: resolveSandboxImage(config2, project3),
|
|
57847
58302
|
...mounts.length > 0 ? { mounts } : {},
|
|
57848
58303
|
...config2.burnCpus !== undefined ? { cpus: config2.burnCpus } : {},
|
|
57849
58304
|
...Object.keys(env).length > 0 ? { env } : {},
|
|
@@ -57904,7 +58359,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
|
|
|
57904
58359
|
const guardInstalled = config2.burnGuard && config2.sandbox !== "noSandbox";
|
|
57905
58360
|
const prepared = resolvePreparedSettings(config2, project3);
|
|
57906
58361
|
const verifyNotes = buildVerifyNotes(prepared);
|
|
57907
|
-
const basePrompt = renderTicketPrompt(
|
|
58362
|
+
const basePrompt = renderTicketPrompt(readFileSync10(burnerTemplatePath(), "utf8"), {
|
|
57908
58363
|
WORKSPACE_NOTES: workspaceNotes,
|
|
57909
58364
|
PROJECT_STANDARDS: blocks.projectStandards,
|
|
57910
58365
|
FEATURE_BRIEF: featureBrief,
|
|
@@ -57932,10 +58387,10 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
|
|
|
57932
58387
|
const guardInstall = guardInstalled ? buildGuardInstallCommand(model.runtime) : undefined;
|
|
57933
58388
|
const withPrelude = (setup) => chainSetupCommands(codexAuthMount ? buildCodexAuthCopyCommand() : undefined, guardInstall, setup);
|
|
57934
58389
|
const agentOptions = guardInstalled ? { bypassHookTrust: true } : {};
|
|
57935
|
-
const slotStamp = buildSlotStamp(resolveSandboxImage(config2), toolchain.packageManagerField);
|
|
58390
|
+
const slotStamp = buildSlotStamp(resolveSandboxImage(config2, project3), toolchain.packageManagerField);
|
|
57936
58391
|
const setupHookFor = (branch) => withPrelude(slot !== undefined ? buildSlotSetupCommand(slot, branch, setupCommand, pm, slotStamp) : workspaceMode === "isolated" ? buildIsolatedSetupCommand(branch, setupCommand, pm) : setupCommand);
|
|
57937
58392
|
mkdirSync10(logsDir(), { recursive: true });
|
|
57938
|
-
const logFilePath =
|
|
58393
|
+
const logFilePath = join18(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
|
|
57939
58394
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
57940
58395
|
beginTranscript(ticket.id);
|
|
57941
58396
|
const timer = createToolTimer();
|
|
@@ -58007,6 +58462,17 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
|
|
|
58007
58462
|
const preserveChain = (branch) => {
|
|
58008
58463
|
ctx.updateTicket(ticket.id, { attemptBranch: branch });
|
|
58009
58464
|
};
|
|
58465
|
+
const stoppedOutcome = (salvaged) => {
|
|
58466
|
+
const reason = ticketStopReason(ticketAbort.signal.reason);
|
|
58467
|
+
return {
|
|
58468
|
+
status: "failed",
|
|
58469
|
+
error: `${reason}${salvaged > 0 ? ` \u2014 ${salvaged} commit(s) preserved; retry to continue from them` : ""}`,
|
|
58470
|
+
event: {
|
|
58471
|
+
type: "ticket.stopped",
|
|
58472
|
+
message: `ticket ${ticket.seq} ${reason}${salvaged > 0 ? ` \u2014 ${salvaged} commit(s) preserved for retry` : ""}`
|
|
58473
|
+
}
|
|
58474
|
+
};
|
|
58475
|
+
};
|
|
58010
58476
|
const discards = new Map;
|
|
58011
58477
|
const discardWorktree = (branch) => {
|
|
58012
58478
|
const pending3 = discards.get(branch) ?? cleanupBurnWorktree(project3.repoPath, branch);
|
|
@@ -58017,7 +58483,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
|
|
|
58017
58483
|
const resolveBranch = ticketBranchName(feature.slug, ticket.seq, newId("r").slice(2, 10));
|
|
58018
58484
|
const containerName = burnContainerName(ctx.runId, ticket.seq, "resolve");
|
|
58019
58485
|
const otherSide = await commitSummaries(project3.repoPath, input.branch, feature.branch);
|
|
58020
|
-
const prompt = renderTemplate(
|
|
58486
|
+
const prompt = renderTemplate(readFileSync10(resolverTemplatePath(), "utf8"), {
|
|
58021
58487
|
TICKET_JSON: ticketJson,
|
|
58022
58488
|
FEATURE_BRIEF: featureBrief,
|
|
58023
58489
|
DOCS_DIGEST: docsDigest,
|
|
@@ -58046,7 +58512,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
|
|
|
58046
58512
|
makeKillable(containerName);
|
|
58047
58513
|
result = await run6({
|
|
58048
58514
|
agent: buildBurnAgent(config2, token, model, agentOptions),
|
|
58049
|
-
sandbox: selectSandbox(config2, mounts, sandboxEnv, killHandles(containerName)),
|
|
58515
|
+
sandbox: selectSandbox(config2, project3, mounts, sandboxEnv, killHandles(containerName)),
|
|
58050
58516
|
cwd: project3.repoPath,
|
|
58051
58517
|
prompt,
|
|
58052
58518
|
branchStrategy: { type: "branch", branch: resolveBranch, baseBranch: input.branch },
|
|
@@ -58179,7 +58645,7 @@ ${blocked3.trim()}`
|
|
|
58179
58645
|
}
|
|
58180
58646
|
const runOptions = {
|
|
58181
58647
|
agent: buildBurnAgent(config2, token, model, agentOptions),
|
|
58182
|
-
sandbox: selectSandbox(config2, mounts, sandboxEnv, killHandles(containerName)),
|
|
58648
|
+
sandbox: selectSandbox(config2, project3, mounts, sandboxEnv, killHandles(containerName)),
|
|
58183
58649
|
cwd: project3.repoPath,
|
|
58184
58650
|
prompt: retryNotes ? `${basePrompt}
|
|
58185
58651
|
|
|
@@ -58215,21 +58681,15 @@ ${retryNotes}` : basePrompt,
|
|
|
58215
58681
|
if (ctx.signal.aborted)
|
|
58216
58682
|
throw err;
|
|
58217
58683
|
const msg = err instanceof Error ? err.message : String(err);
|
|
58218
|
-
const
|
|
58684
|
+
const sandboxImage = resolveSandboxImage(config2, project3);
|
|
58685
|
+
const missingBinary = missingAgentBinaryMessage(err, model.runtime, sandboxImage) ?? missingSetupBinaryMessage(err, setupCommand, sandboxImage);
|
|
58219
58686
|
const salvaged = await branchCommitsAhead(project3.repoPath, feature.branch, tempBranch);
|
|
58220
58687
|
if (salvaged.length > 0)
|
|
58221
58688
|
baseBranch = tempBranch;
|
|
58222
58689
|
if (ticketAbort.signal.aborted) {
|
|
58223
58690
|
if (salvaged.length > 0)
|
|
58224
58691
|
preserveChain(tempBranch);
|
|
58225
|
-
return
|
|
58226
|
-
status: "failed",
|
|
58227
|
-
error: `stopped by user${salvaged.length > 0 ? ` \u2014 ${salvaged.length} commit(s) preserved; retry to continue from them` : ""}`,
|
|
58228
|
-
event: {
|
|
58229
|
-
type: "ticket.stopped",
|
|
58230
|
-
message: `ticket ${ticket.seq} stopped by user${salvaged.length > 0 ? ` \u2014 ${salvaged.length} commit(s) preserved for retry` : ""}`
|
|
58231
|
-
}
|
|
58232
|
-
};
|
|
58692
|
+
return stoppedOutcome(salvaged.length);
|
|
58233
58693
|
}
|
|
58234
58694
|
if (isWorktreeTeardownError(err)) {
|
|
58235
58695
|
const removed = await discardWorktree(tempBranch);
|
|
@@ -58260,9 +58720,10 @@ ${retryNotes}` : basePrompt,
|
|
|
58260
58720
|
if (missingBinary) {
|
|
58261
58721
|
if (salvaged.length > 0)
|
|
58262
58722
|
preserveChain(tempBranch);
|
|
58263
|
-
return { status: "failed", error: missingBinary };
|
|
58723
|
+
return { status: "failed", error: missingBinary, runFatal: { runtime: model.runtime } };
|
|
58264
58724
|
}
|
|
58265
|
-
|
|
58725
|
+
const verdict = classifyTicketRunError(err, model.runtime, setupCommand);
|
|
58726
|
+
if (verdict === "retryable" && attempt < maxAttempts) {
|
|
58266
58727
|
const headline = errorHeadline(msg);
|
|
58267
58728
|
retryNotes = buildRetryNotes({ error: headline, commitCount: salvaged.length });
|
|
58268
58729
|
ctx.emitEvent({
|
|
@@ -58282,20 +58743,17 @@ ${retryNotes}` : basePrompt,
|
|
|
58282
58743
|
if (ticketAbort.signal.aborted) {
|
|
58283
58744
|
if (salvaged.length > 0)
|
|
58284
58745
|
preserveChain(tempBranch);
|
|
58285
|
-
return
|
|
58286
|
-
status: "failed",
|
|
58287
|
-
error: `stopped by user${salvaged.length > 0 ? ` \u2014 ${salvaged.length} commit(s) preserved; retry to continue from them` : ""}`,
|
|
58288
|
-
event: {
|
|
58289
|
-
type: "ticket.stopped",
|
|
58290
|
-
message: `ticket ${ticket.seq} stopped by user${salvaged.length > 0 ? ` \u2014 ${salvaged.length} commit(s) preserved for retry` : ""}`
|
|
58291
|
-
}
|
|
58292
|
-
};
|
|
58746
|
+
return stoppedOutcome(salvaged.length);
|
|
58293
58747
|
}
|
|
58294
58748
|
continue;
|
|
58295
58749
|
}
|
|
58296
58750
|
if (salvaged.length > 0)
|
|
58297
58751
|
preserveChain(tempBranch);
|
|
58298
|
-
return {
|
|
58752
|
+
return {
|
|
58753
|
+
status: "failed",
|
|
58754
|
+
error: msg,
|
|
58755
|
+
...verdict === "run-fatal" ? { runFatal: { runtime: model.runtime } } : {}
|
|
58756
|
+
};
|
|
58299
58757
|
}
|
|
58300
58758
|
}
|
|
58301
58759
|
if (!result)
|
|
@@ -58339,7 +58797,7 @@ function resolveBurnDeps(ctx) {
|
|
|
58339
58797
|
const exec3 = createSystemExec({ cwd: ctx.project.repoPath });
|
|
58340
58798
|
const imageProbeCache = new Map;
|
|
58341
58799
|
const cachedExec = (command, args2) => {
|
|
58342
|
-
const key =
|
|
58800
|
+
const key = [resolveSandboxImage(config2, ctx.project), model.runtime, command, ...args2].join("\x00");
|
|
58343
58801
|
let result = imageProbeCache.get(key);
|
|
58344
58802
|
if (!result) {
|
|
58345
58803
|
result = exec3(command, args2);
|
|
@@ -58357,7 +58815,7 @@ function resolveBurnDeps(ctx) {
|
|
|
58357
58815
|
const engine = config2.sandbox === "podman" ? "podman" : "docker";
|
|
58358
58816
|
return cacheVolumeReady ??= ensureBurnCacheVolume({
|
|
58359
58817
|
engine,
|
|
58360
|
-
imageName: resolveSandboxImage(config2),
|
|
58818
|
+
imageName: resolveSandboxImage(config2, ctx.project),
|
|
58361
58819
|
projectId: ctx.project.id,
|
|
58362
58820
|
exec: exec3
|
|
58363
58821
|
});
|
|
@@ -58391,6 +58849,7 @@ function resolveBurnDeps(ctx) {
|
|
|
58391
58849
|
const { model: ticketModel, token: ticketToken } = ticketCredentials(ticket);
|
|
58392
58850
|
return burnAuthReady(ticketModel.runtime, ticketToken) ? undefined : ticketModel.runtime;
|
|
58393
58851
|
},
|
|
58852
|
+
ticketRuntime: (ticket) => ticketCredentials(ticket).model.runtime,
|
|
58394
58853
|
concurrency: config2.burnConcurrency,
|
|
58395
58854
|
executeTicketRun: (c, ticket, run7) => {
|
|
58396
58855
|
const { model: ticketModel, token: ticketToken } = ticketCredentials(ticket);
|
|
@@ -58407,9 +58866,9 @@ function resolveBurnDeps(ctx) {
|
|
|
58407
58866
|
function readTokenFromEnvFile(path2, runtime5) {
|
|
58408
58867
|
const key = RUNTIME_AUTH_KEY[runtime5];
|
|
58409
58868
|
let fromFile;
|
|
58410
|
-
if (
|
|
58869
|
+
if (existsSync18(path2)) {
|
|
58411
58870
|
try {
|
|
58412
|
-
fromFile = parseEnvFile3(
|
|
58871
|
+
fromFile = parseEnvFile3(readFileSync10(path2, "utf8"))[key];
|
|
58413
58872
|
} catch {
|
|
58414
58873
|
fromFile = undefined;
|
|
58415
58874
|
}
|
|
@@ -58417,7 +58876,7 @@ function readTokenFromEnvFile(path2, runtime5) {
|
|
|
58417
58876
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env[key];
|
|
58418
58877
|
return token && token.length > 0 ? token : undefined;
|
|
58419
58878
|
}
|
|
58420
|
-
var ATTACHMENTS_PREFIX, 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, TOOLCHAIN_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;
|
|
58879
|
+
var ATTACHMENTS_PREFIX, AUTH_MISSING_EVENT = "auth.missing", IMAGE_RUNTIME_MISSING_EVENT = "burn.image_runtime_missing", RUNTIME_BINARY, COMMAND_SEPARATORS, ASSIGNMENT_PREFIX, NON_COMMAND_WORDS, PLAIN_COMMAND_NAME, DROPPED_MAP_SECTIONS, AGENT_CONVENTIONS_FILE = "CLAUDE.md", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, TOOLCHAIN_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, REFUSAL_STATUS, DETACHED_FORBIDDEN, CREDENTIAL_SUBJECT, ACCOUNT_PERMISSION_DENIED, RUN_FATAL_ERROR_PATTERNS, REFUSED_CREDENTIAL_STATUS, RUNTIME_RUN_FATAL_ERROR_PATTERNS, FATAL_ERROR_PATTERNS, RUNTIME_FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, RUNTIME_RETRYABLE_ERROR_PATTERNS, AGENT_BINARY, MISSING_COMMAND_PATTERNS, activeTicketAborts, RunHaltedAbort, NOTHING_LANDED = "nothing landed \u2014 every implementation ticket in the run was cancelled", SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
58421
58880
|
var init_ticket_burner = __esm(() => {
|
|
58422
58881
|
init_src();
|
|
58423
58882
|
init_config_load();
|
|
@@ -58442,6 +58901,26 @@ var init_ticket_burner = __esm(() => {
|
|
|
58442
58901
|
"claude-code": "claude",
|
|
58443
58902
|
codex: "codex"
|
|
58444
58903
|
};
|
|
58904
|
+
COMMAND_SEPARATORS = /&&|\|\||[;|\n]/;
|
|
58905
|
+
ASSIGNMENT_PREFIX = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
58906
|
+
NON_COMMAND_WORDS = new Set([
|
|
58907
|
+
"cd",
|
|
58908
|
+
"export",
|
|
58909
|
+
"set",
|
|
58910
|
+
"unset",
|
|
58911
|
+
"source",
|
|
58912
|
+
".",
|
|
58913
|
+
"exec",
|
|
58914
|
+
"env",
|
|
58915
|
+
"eval",
|
|
58916
|
+
"command",
|
|
58917
|
+
"time",
|
|
58918
|
+
"sudo",
|
|
58919
|
+
"echo",
|
|
58920
|
+
"true",
|
|
58921
|
+
"false"
|
|
58922
|
+
]);
|
|
58923
|
+
PLAIN_COMMAND_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
58445
58924
|
DROPPED_MAP_SECTIONS = ["Not yet specified", "Out of scope"];
|
|
58446
58925
|
PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
58447
58926
|
PM_CACHE_SANDBOX_PATHS = {
|
|
@@ -58485,27 +58964,42 @@ var init_ticket_burner = __esm(() => {
|
|
|
58485
58964
|
/is not a working tree/i,
|
|
58486
58965
|
/resource busy|ebusy|eperm|eacces/i
|
|
58487
58966
|
];
|
|
58488
|
-
|
|
58967
|
+
REFUSAL_STATUS = String.raw`\b(?:401|403)\b`;
|
|
58968
|
+
DETACHED_FORBIDDEN = String.raw`(?<!${REFUSAL_STATUS}[\W_]*)forbidden(?![\W_]*${REFUSAL_STATUS})`;
|
|
58969
|
+
CREDENTIAL_SUBJECT = String.raw`(?:api[ _-]?key|token|credential|login|auth(?:entic\w*|ori[sz]\w*|[nz])?\b|unauthorized|${DETACHED_FORBIDDEN}|account|organi[sz]ation|\borg\b)`;
|
|
58970
|
+
ACCOUNT_PERMISSION_DENIED = refusalNamingCredential(String.raw`permission denied`);
|
|
58971
|
+
RUN_FATAL_ERROR_PATTERNS = [
|
|
58489
58972
|
/invalid (api key|x-api-key)/i,
|
|
58490
|
-
/authentication|unauthorized
|
|
58973
|
+
/authentication|unauthorized/i,
|
|
58974
|
+
ACCOUNT_PERMISSION_DENIED,
|
|
58491
58975
|
/credit balance|billing/i,
|
|
58492
58976
|
/oauth token|setup-token/i,
|
|
58493
|
-
/
|
|
58494
|
-
/does not support resumeSession|resumeSession .* not found/i
|
|
58977
|
+
/usage limit/i
|
|
58495
58978
|
];
|
|
58496
|
-
|
|
58979
|
+
REFUSED_CREDENTIAL_STATUS = refusalNamingCredential(String.raw`(?:${REFUSAL_STATUS}|forbidden)`);
|
|
58980
|
+
RUNTIME_RUN_FATAL_ERROR_PATTERNS = {
|
|
58497
58981
|
"claude-code": [],
|
|
58498
58982
|
codex: [
|
|
58499
|
-
/invalid_api_key
|
|
58500
|
-
|
|
58983
|
+
/invalid_api_key/i,
|
|
58984
|
+
REFUSED_CREDENTIAL_STATUS,
|
|
58501
58985
|
/insufficient_quota|exceeded your current quota/i,
|
|
58502
|
-
/model_not_found|does not exist or you do not have access/i,
|
|
58503
58986
|
/CODEX_API_KEY/,
|
|
58504
58987
|
/not logged in/i,
|
|
58505
58988
|
/\bauth (failed|required)\b/i,
|
|
58506
58989
|
/refresh token/i
|
|
58507
58990
|
]
|
|
58508
58991
|
};
|
|
58992
|
+
FATAL_ERROR_PATTERNS = [
|
|
58993
|
+
/issue with the selected model|model not found|unknown model/i,
|
|
58994
|
+
/does not support resumeSession|resumeSession .* not found/i
|
|
58995
|
+
];
|
|
58996
|
+
RUNTIME_FATAL_ERROR_PATTERNS = {
|
|
58997
|
+
"claude-code": [],
|
|
58998
|
+
codex: [
|
|
58999
|
+
/invalid_request_error/i,
|
|
59000
|
+
/model_not_found|does not exist or you do not have access/i
|
|
59001
|
+
]
|
|
59002
|
+
};
|
|
58509
59003
|
RETRYABLE_ERROR_PATTERNS = [
|
|
58510
59004
|
/exited with code \d+/i,
|
|
58511
59005
|
/idle timeout|AgentIdleTimeout/i,
|
|
@@ -58524,7 +59018,19 @@ var init_ticket_burner = __esm(() => {
|
|
|
58524
59018
|
"claude-code": "claude",
|
|
58525
59019
|
codex: "codex"
|
|
58526
59020
|
};
|
|
59021
|
+
MISSING_COMMAND_PATTERNS = [
|
|
59022
|
+
/\bexit(?:ed with)?(?: code)? 127\b/i,
|
|
59023
|
+
/command not found/i
|
|
59024
|
+
];
|
|
58527
59025
|
activeTicketAborts = new Map;
|
|
59026
|
+
RunHaltedAbort = class RunHaltedAbort extends Error {
|
|
59027
|
+
headline;
|
|
59028
|
+
constructor(headline) {
|
|
59029
|
+
super(`ticket stopped: run halted (${headline})`);
|
|
59030
|
+
this.headline = headline;
|
|
59031
|
+
this.name = "RunHaltedAbort";
|
|
59032
|
+
}
|
|
59033
|
+
};
|
|
58528
59034
|
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
58529
59035
|
ticketBurner = {
|
|
58530
59036
|
id: "ticket-burner",
|
|
@@ -58533,9 +59039,9 @@ var init_ticket_burner = __esm(() => {
|
|
|
58533
59039
|
});
|
|
58534
59040
|
|
|
58535
59041
|
// src/workflows/research.ts
|
|
58536
|
-
import { mkdirSync as mkdirSync11, readFileSync as
|
|
58537
|
-
import { dirname as dirname11, join as
|
|
58538
|
-
import { fileURLToPath as
|
|
59042
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync11 } from "fs";
|
|
59043
|
+
import { dirname as dirname11, join as join19 } from "path";
|
|
59044
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
58539
59045
|
function waypointSlug(waypoint) {
|
|
58540
59046
|
const base = waypoint.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
58541
59047
|
return `${waypoint.seq}-${base || "waypoint"}`;
|
|
@@ -58602,8 +59108,8 @@ function interpretResearchResult(result, docRelPath) {
|
|
|
58602
59108
|
return { status: "failed", error: "agent produced no commits \u2014 no research summary landed" };
|
|
58603
59109
|
}
|
|
58604
59110
|
function researchTemplatePath() {
|
|
58605
|
-
const here = dirname11(
|
|
58606
|
-
return
|
|
59111
|
+
const here = dirname11(fileURLToPath7(import.meta.url));
|
|
59112
|
+
return join19(resolveSkillsRoot(here), "burner", "research-waypoint.md");
|
|
58607
59113
|
}
|
|
58608
59114
|
function researchContainerName(runId, seq2) {
|
|
58609
59115
|
return `runcastle-${runId}-w${seq2}`;
|
|
@@ -58619,7 +59125,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
58619
59125
|
const { project: project3, feature } = ctx;
|
|
58620
59126
|
const docRelPath = researchDocRel(feature.slug, waypoint);
|
|
58621
59127
|
const tempBranch = researchBranchName(feature.slug, waypoint.seq, newId("b").slice(2, 10));
|
|
58622
|
-
const template =
|
|
59128
|
+
const template = readFileSync11(researchTemplatePath(), "utf8");
|
|
58623
59129
|
const prompt = renderResearchPrompt(template, {
|
|
58624
59130
|
WAYPOINT_JSON: buildWaypointJson(waypoint),
|
|
58625
59131
|
FEATURE_BRIEF: buildFeatureBrief(feature),
|
|
@@ -58627,12 +59133,12 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
58627
59133
|
RESEARCH_DOC_PATH: docRelPath
|
|
58628
59134
|
});
|
|
58629
59135
|
mkdirSync11(logsDir(), { recursive: true });
|
|
58630
|
-
const logFilePath =
|
|
59136
|
+
const logFilePath = join19(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
|
|
58631
59137
|
const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
|
|
58632
59138
|
const codexAuthMount = codexAuthMountFor(model.runtime, config2.sandbox);
|
|
58633
59139
|
const runOptions = {
|
|
58634
59140
|
agent: buildBurnAgent(config2, token, model),
|
|
58635
|
-
sandbox: selectSandbox(config2, codexAuthMount ? [codexAuthMount] : [], {}, registerResearchKill(config2, ctx.runId, waypoint.seq)),
|
|
59141
|
+
sandbox: selectSandbox(config2, project3, codexAuthMount ? [codexAuthMount] : [], {}, registerResearchKill(config2, ctx.runId, waypoint.seq)),
|
|
58636
59142
|
cwd: project3.repoPath,
|
|
58637
59143
|
prompt,
|
|
58638
59144
|
branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
|
|
@@ -59233,16 +59739,30 @@ async function rethinkAndLaunch(ctx, featureId, launch3) {
|
|
|
59233
59739
|
function errMsg2(e) {
|
|
59234
59740
|
return e instanceof Error ? e.message : String(e);
|
|
59235
59741
|
}
|
|
59742
|
+
function latestRunDeniedDirty(ctx, featureId) {
|
|
59743
|
+
const latestRun = listRunsByFeature(ctx, featureId)[0];
|
|
59744
|
+
if (!latestRun)
|
|
59745
|
+
return false;
|
|
59746
|
+
const deniedAt = latestEventTs(ctx, featureId, "reviewdrive.denied");
|
|
59747
|
+
return deniedAt !== undefined && deniedAt >= latestRun.startedAt;
|
|
59748
|
+
}
|
|
59236
59749
|
async function retryTicket(ctx, ticketId, opts = {}) {
|
|
59237
59750
|
const ticket = getTicket(ctx, ticketId);
|
|
59238
59751
|
const feature = getFeatureRow(ctx, ticket.featureId);
|
|
59239
|
-
|
|
59752
|
+
const retryingDeniedReview = ticket.kind === "review" && ticket.status === "done" && latestRunDeniedDirty(ctx, feature.id);
|
|
59753
|
+
if (ticket.status !== "failed" && !retryingDeniedReview) {
|
|
59240
59754
|
throw new GateError(`only failed tickets can be retried \u2014 ticket ${ticket.seq} is ${ticket.status}`);
|
|
59241
59755
|
}
|
|
59242
59756
|
if (hasActiveRun(ctx, feature.id)) {
|
|
59243
59757
|
throw new GateError("a run is live for this feature \u2014 retry after it finishes, or cancel it first");
|
|
59244
59758
|
}
|
|
59245
59759
|
const project3 = projectForFeature(ctx, feature);
|
|
59760
|
+
if (retryingDeniedReview) {
|
|
59761
|
+
const stillDirty = await driveBlockingPaths(project3.repoPath);
|
|
59762
|
+
if (stillDirty.length > 0) {
|
|
59763
|
+
throw new GateError(`the working tree is still dirty \u2014 the review drive would be denied again. Commit or ` + `discard ${stillDirty.length} file(s) first: ${truncatedFileList(stillDirty)}`);
|
|
59764
|
+
}
|
|
59765
|
+
}
|
|
59246
59766
|
const all8 = listByFeature2(ctx, feature.id);
|
|
59247
59767
|
const bySeq = new Map(all8.map((t) => [t.seq, t]));
|
|
59248
59768
|
const toReset = new Map([[ticket.seq, ticket]]);
|
|
@@ -59424,7 +59944,7 @@ var init_features = __esm(() => {
|
|
|
59424
59944
|
});
|
|
59425
59945
|
|
|
59426
59946
|
// src/services/test-notes.ts
|
|
59427
|
-
import { existsSync as
|
|
59947
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync12, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
59428
59948
|
import { dirname as dirname12 } from "path";
|
|
59429
59949
|
import { and as and10, asc as asc5, eq as eq13, lt } from "drizzle-orm";
|
|
59430
59950
|
function rowToNote(row) {
|
|
@@ -59439,7 +59959,7 @@ function rowToNote(row) {
|
|
|
59439
59959
|
reviewTicketId: row.reviewTicketId ?? undefined,
|
|
59440
59960
|
carriedLap: row.carriedLap ?? undefined,
|
|
59441
59961
|
videoTimestamp: row.videoTimestamp ?? undefined,
|
|
59442
|
-
screenshotUrl:
|
|
59962
|
+
screenshotUrl: existsSync20(annotationPath(row.id)) ? noteScreenshotUrl(row.id) : undefined,
|
|
59443
59963
|
createdAt: row.createdAt,
|
|
59444
59964
|
updatedAt: row.updatedAt
|
|
59445
59965
|
});
|
|
@@ -59709,8 +60229,8 @@ var init_test_notes = __esm(() => {
|
|
|
59709
60229
|
});
|
|
59710
60230
|
|
|
59711
60231
|
// src/mcp/server.ts
|
|
59712
|
-
import { existsSync as
|
|
59713
|
-
import { join as
|
|
60232
|
+
import { existsSync as existsSync21, readFileSync as readFileSync12 } from "fs";
|
|
60233
|
+
import { join as join20, relative as relative4, resolve as resolve10, sep as sep2 } from "path";
|
|
59714
60234
|
import { StreamableHTTPTransport } from "@hono/mcp";
|
|
59715
60235
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
59716
60236
|
import { Hono } from "hono";
|
|
@@ -59795,6 +60315,7 @@ function featureContext(ctx, reader) {
|
|
|
59795
60315
|
moreDocs,
|
|
59796
60316
|
docsNote: DOCS_NOTE,
|
|
59797
60317
|
openDefects: carried.openDefects,
|
|
60318
|
+
carriedDefects: carried.carriedDefects,
|
|
59798
60319
|
tickets: listByFeature2(ctx, feature.id).map(stripDigest),
|
|
59799
60320
|
annotatedModels: annotatedModels(ctx)
|
|
59800
60321
|
};
|
|
@@ -59851,7 +60372,13 @@ function toolEmitTickets(ctx, session, input) {
|
|
|
59851
60372
|
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
59852
60373
|
refuseIfReadOnly(session, "emitting tickets");
|
|
59853
60374
|
refuseMisKindedReview(input.tickets);
|
|
60375
|
+
const linkedFindingIds = input.tickets.flatMap((t) => t.originFindingId ?? []);
|
|
60376
|
+
requireLinkableFindings(ctx, feature.id, linkedFindingIds);
|
|
59854
60377
|
const stored = storeTickets(ctx, feature.id, input.tickets);
|
|
60378
|
+
for (const ticket of stored) {
|
|
60379
|
+
if (ticket.originFindingId)
|
|
60380
|
+
linkFixTicket(ctx, feature.id, ticket.originFindingId, ticket.id);
|
|
60381
|
+
}
|
|
59855
60382
|
return {
|
|
59856
60383
|
stored: stored.length,
|
|
59857
60384
|
tickets: stored.map((t) => ({ id: t.id, seq: t.seq, title: t.title }))
|
|
@@ -59876,6 +60403,13 @@ function toolCancelTicket(ctx, session, input) {
|
|
|
59876
60403
|
refuseIfReadOnly(session, "cancelling a ticket");
|
|
59877
60404
|
return { ok: true, ticket: cancelTicket(ctx, input.id, input.reason) };
|
|
59878
60405
|
}
|
|
60406
|
+
function toolResolveFinding(ctx, session, input) {
|
|
60407
|
+
const featureId = requireFeatureId(session);
|
|
60408
|
+
refuseIfReadOnly(session, "resolving a finding");
|
|
60409
|
+
const { findingId, disposition, note: note2 } = ResolveFindingInput.parse(input);
|
|
60410
|
+
const finding = disposition === "carry" ? carryFinding(ctx, featureId, findingId, note2) : closeAsAddressed(ctx, featureId, findingId, note2 ?? "");
|
|
60411
|
+
return { ok: true, finding };
|
|
60412
|
+
}
|
|
59879
60413
|
function toolEscalateToMap(ctx, session, input) {
|
|
59880
60414
|
return escalateToMap(ctx, requireFeatureId(session), input);
|
|
59881
60415
|
}
|
|
@@ -60142,16 +60676,16 @@ function adrRef(adr) {
|
|
|
60142
60676
|
}
|
|
60143
60677
|
function toolReadAdr(ctx, session, input) {
|
|
60144
60678
|
requireProject(ctx, session);
|
|
60145
|
-
const root = resolve10(
|
|
60679
|
+
const root = resolve10(join20(session.worktreePath, ADR_DIR_REL));
|
|
60146
60680
|
const name = input.relPath.replace(/\\/g, "/").replace(/^\.?\/*docs\/adr\//i, "");
|
|
60147
60681
|
const target = resolve10(root, name);
|
|
60148
60682
|
const rel = relative4(root, target);
|
|
60149
60683
|
if (rel === "" || rel.startsWith("..") || rel.split(sep2).includes("..")) {
|
|
60150
60684
|
throw new InvalidInputError(`adr path escapes ${ADR_DIR_REL}: ${input.relPath}`);
|
|
60151
60685
|
}
|
|
60152
|
-
if (!
|
|
60686
|
+
if (!existsSync21(target))
|
|
60153
60687
|
throw new NotFoundError(`adr not found: ${input.relPath}`);
|
|
60154
|
-
return { relPath: `${ADR_DIR_REL}/${name}`, content:
|
|
60688
|
+
return { relPath: `${ADR_DIR_REL}/${name}`, content: readFileSync12(target, "utf8") };
|
|
60155
60689
|
}
|
|
60156
60690
|
function featureIndexLine(feature) {
|
|
60157
60691
|
if (feature.status === "shipped") {
|
|
@@ -60290,7 +60824,7 @@ function buildMcpServer(audience) {
|
|
|
60290
60824
|
if (wants("review_drive")) {
|
|
60291
60825
|
server.registerTool("review_drive", {
|
|
60292
60826
|
title: "Drive the feature branch for review",
|
|
60293
|
-
description: "Boot the integrated feature branch on the human's checkout, under your run's " + "identity, so you can review it. ALWAYS stop what you started, including when the " + "review goes wrong: the drive holds a machine-wide slot and the human cannot use their " + "own checkout until you release it.
|
|
60827
|
+
description: "Boot the integrated feature branch on the human's checkout, under your run's " + "identity, so you can review it. ALWAYS stop what you started, including when the " + "review goes wrong: the drive holds a machine-wide slot and the human cannot use their " + "own checkout until you release it. A refused `start` says which refusal it is: " + '`deniedCode: "slot_held"` (`retriable: true`) means somebody else \u2014 a drive or a ' + "preparation dry run \u2014 holds the machine-wide slot, which frees itself when they " + "finish, so call `start` again about ten times roughly thirty seconds apart before you " + 'give up on it. `deniedCode: "dirty"` (`retriable: false`) is final \u2014 the uncommitted ' + "files in `dirtyFiles` are the human's to clear and no wait will do it, so report it " + "and review without the app. Refused unless your call carries a live run identity.",
|
|
60294
60828
|
inputSchema: {
|
|
60295
60829
|
action: z4.enum(["start", "status", "stop"]).describe("`start` switches the checkout to the feature branch, renders driveEnv (per-branch " + "database), runs driveSetupCommand and spawns devCommand. `status` reports the " + "drive and hands back the localhost URL once the dev server has printed one \u2014 " + "poll it, the URL is not ready at start. `stop` tears the environment down and " + "puts the checkout back.")
|
|
60296
60830
|
}
|
|
@@ -60510,6 +61044,18 @@ function buildMcpServer(audience) {
|
|
|
60510
61044
|
return ok(result);
|
|
60511
61045
|
});
|
|
60512
61046
|
}
|
|
61047
|
+
if (wants("resolve_finding")) {
|
|
61048
|
+
server.registerTool("resolve_finding", {
|
|
61049
|
+
title: "Resolve a review finding",
|
|
61050
|
+
description: "Say what this lap did about a defect an earlier lap\u2019s review left open: `carry` it into " + "a later lap, or close it as `addressed` because this lap\u2019s work already answers it. To " + "link it instead, emit the ticket that fixes it with `originFindingId` set \u2014 the burn " + "then closes the finding itself when that ticket lands. Every earlier-lap open defect " + 'must be linked, carried or closed before `complete_phase("tickets")` will pass.',
|
|
61051
|
+
inputSchema: ResolveFindingShape
|
|
61052
|
+
}, async (args2, extra) => {
|
|
61053
|
+
const rs = await resolveCtxSession(extra);
|
|
61054
|
+
if (!rs)
|
|
61055
|
+
return noSession();
|
|
61056
|
+
return ok(toolResolveFinding(rs.ctx, rs.session, args2));
|
|
61057
|
+
});
|
|
61058
|
+
}
|
|
60513
61059
|
if (wants("escalate_to_map")) {
|
|
60514
61060
|
server.registerTool("escalate_to_map", {
|
|
60515
61061
|
title: "Escalate to map",
|
|
@@ -60606,7 +61152,23 @@ async function resolveAudience(sessionId, runId) {
|
|
|
60606
61152
|
return;
|
|
60607
61153
|
}
|
|
60608
61154
|
}
|
|
60609
|
-
|
|
61155
|
+
async function readMcpBody(c) {
|
|
61156
|
+
if (c.req.method !== "POST")
|
|
61157
|
+
return;
|
|
61158
|
+
let text3;
|
|
61159
|
+
try {
|
|
61160
|
+
text3 = await c.req.text();
|
|
61161
|
+
} catch (e) {
|
|
61162
|
+
console.error(`[mcp] request body read failed after ${c.req.header("content-length") ?? "unknown"} ` + `declared bytes: ${e instanceof Error ? e.message : String(e)}`);
|
|
61163
|
+
return;
|
|
61164
|
+
}
|
|
61165
|
+
try {
|
|
61166
|
+
return JSON.parse(text3);
|
|
61167
|
+
} catch {
|
|
61168
|
+
return;
|
|
61169
|
+
}
|
|
61170
|
+
}
|
|
61171
|
+
var DOCS_NOTE, REVIEW_TITLE, ResolveFindingShape, ResolveFindingInput, DRAFTING_KINDS, ADRS_NOTE, FEATURE_KINDS, FEATURE_WRITE_KINDS, PROJECT_KINDS, ALL_AUDIENCES, TOOL_AUDIENCES, CompletablePhase, EventType, mcp, server_default;
|
|
60610
61172
|
var init_server = __esm(() => {
|
|
60611
61173
|
init_src();
|
|
60612
61174
|
init_paths();
|
|
@@ -60627,6 +61189,20 @@ var init_server = __esm(() => {
|
|
|
60627
61189
|
init_waypoints();
|
|
60628
61190
|
DOCS_NOTE = "docs[] holds this feature\u2019s canonical docs in full. moreDocs[] is everything else that " + "exists in docs/features/<slug>/ \u2014 not inlined, not gone: read any of them with " + "read_feature_doc({ relPath }). A `withheld` reason means it was left out on purpose; fetch " + "it anyway if a ticket points at it.";
|
|
60629
61191
|
REVIEW_TITLE = /^review[: ]/i;
|
|
61192
|
+
ResolveFindingShape = {
|
|
61193
|
+
findingId: z4.string().min(1).describe("The finding id, from `get_feature_context`\u2019s openDefects or carriedDefects."),
|
|
61194
|
+
disposition: z4.enum(["carry", "addressed"]).describe("`carry` parks it for a later lap \u2014 out of the open count, still visible, and the human can " + "reopen it. `addressed` closes it because this lap\u2019s work already answers it."),
|
|
61195
|
+
note: z4.string().optional().describe('REQUIRED for `addressed`: what addressed it, e.g. "lap 2\u2019s ticket 7 rewrote the endpoint". ' + "Optional for `carry`, where it records why it was parked.")
|
|
61196
|
+
};
|
|
61197
|
+
ResolveFindingInput = z4.object(ResolveFindingShape).superRefine((input, refinement) => {
|
|
61198
|
+
if (input.disposition === "addressed" && !input.note?.trim()) {
|
|
61199
|
+
refinement.addIssue({
|
|
61200
|
+
code: "custom",
|
|
61201
|
+
path: ["note"],
|
|
61202
|
+
message: "closing a defect as addressed requires a note saying what addressed it"
|
|
61203
|
+
});
|
|
61204
|
+
}
|
|
61205
|
+
});
|
|
60630
61206
|
DRAFTING_KINDS = ["ideation", "revisit", "waypoint", "converge"];
|
|
60631
61207
|
ADRS_NOTE = "adrs[] is an index of the project\u2019s LIVE decisions (superseded ones are already omitted), not " + "their text. Read the ones your work touches with read_adr({ relPath }) \u2014 they bind you the " + "same either way. They are also plain files in this worktree at docs/adr/.";
|
|
60632
61208
|
FEATURE_KINDS = [
|
|
@@ -60648,6 +61224,7 @@ var init_server = __esm(() => {
|
|
|
60648
61224
|
update_ticket: FEATURE_WRITE_KINDS,
|
|
60649
61225
|
cancel_ticket: FEATURE_WRITE_KINDS,
|
|
60650
61226
|
complete_phase: FEATURE_WRITE_KINDS,
|
|
61227
|
+
resolve_finding: FEATURE_WRITE_KINDS,
|
|
60651
61228
|
escalate_to_map: FEATURE_KINDS,
|
|
60652
61229
|
emit_waypoints: FEATURE_KINDS,
|
|
60653
61230
|
resolve_waypoint: FEATURE_KINDS,
|
|
@@ -60673,11 +61250,12 @@ var init_server = __esm(() => {
|
|
|
60673
61250
|
}
|
|
60674
61251
|
});
|
|
60675
61252
|
mcp.all("*", async (c) => {
|
|
61253
|
+
const body = await readMcpBody(c);
|
|
60676
61254
|
const audience = await resolveAudience(c.req.header("X-Runcastle-Session"), c.req.header(RUN_HEADER));
|
|
60677
61255
|
const server = buildMcpServer(audience);
|
|
60678
61256
|
const transport = new StreamableHTTPTransport({ enableJsonResponse: true });
|
|
60679
61257
|
await server.connect(transport);
|
|
60680
|
-
const res = await transport.handleRequest(c);
|
|
61258
|
+
const res = await transport.handleRequest(c, body);
|
|
60681
61259
|
return res ?? c.body(null, 202);
|
|
60682
61260
|
});
|
|
60683
61261
|
server_default = mcp;
|
|
@@ -61018,12 +61596,12 @@ var init_hooks = __esm(() => {
|
|
|
61018
61596
|
});
|
|
61019
61597
|
|
|
61020
61598
|
// src/routes/reviews.ts
|
|
61021
|
-
import { createReadStream, statSync as
|
|
61599
|
+
import { createReadStream, statSync as statSync5 } from "fs";
|
|
61022
61600
|
import { Readable } from "stream";
|
|
61023
61601
|
import { Hono as Hono3 } from "hono";
|
|
61024
61602
|
function fileSize(path2) {
|
|
61025
61603
|
try {
|
|
61026
|
-
const stat3 =
|
|
61604
|
+
const stat3 = statSync5(path2);
|
|
61027
61605
|
return stat3.isFile() ? stat3.size : undefined;
|
|
61028
61606
|
} catch {
|
|
61029
61607
|
return;
|
|
@@ -61210,15 +61788,15 @@ var init_stream = __esm(() => {
|
|
|
61210
61788
|
});
|
|
61211
61789
|
|
|
61212
61790
|
// src/routes/web.ts
|
|
61213
|
-
import { existsSync as
|
|
61791
|
+
import { existsSync as existsSync22, statSync as statSync6 } from "fs";
|
|
61214
61792
|
import { readFile as readFile4 } from "fs/promises";
|
|
61215
|
-
import { extname, join as
|
|
61793
|
+
import { extname, join as join21, normalize as normalize2, resolve as resolve11, sep as sep3 } from "path";
|
|
61216
61794
|
function contentType(path2) {
|
|
61217
61795
|
return MIME[extname(path2).toLowerCase()] ?? "application/octet-stream";
|
|
61218
61796
|
}
|
|
61219
61797
|
function isFile2(path2) {
|
|
61220
61798
|
try {
|
|
61221
|
-
return
|
|
61799
|
+
return statSync6(path2).isFile();
|
|
61222
61800
|
} catch {
|
|
61223
61801
|
return false;
|
|
61224
61802
|
}
|
|
@@ -61230,7 +61808,7 @@ function safeJoin(root, pathname) {
|
|
|
61230
61808
|
} catch {
|
|
61231
61809
|
return null;
|
|
61232
61810
|
}
|
|
61233
|
-
const joined = normalize2(
|
|
61811
|
+
const joined = normalize2(join21(root, decoded));
|
|
61234
61812
|
if (joined !== root && !joined.startsWith(root + sep3))
|
|
61235
61813
|
return null;
|
|
61236
61814
|
return joined;
|
|
@@ -61243,7 +61821,7 @@ function resolveWebDist() {
|
|
|
61243
61821
|
}
|
|
61244
61822
|
function mountWebApp(app, distDir) {
|
|
61245
61823
|
const root = resolve11(distDir);
|
|
61246
|
-
const indexHtml =
|
|
61824
|
+
const indexHtml = join21(root, "index.html");
|
|
61247
61825
|
app.get("*", async (c, next4) => {
|
|
61248
61826
|
const { pathname } = new URL(c.req.url);
|
|
61249
61827
|
if (/^\/(api|mcp|ws|health)(\/|$)/.test(pathname))
|
|
@@ -61263,7 +61841,7 @@ function mountWebApp(app, distDir) {
|
|
|
61263
61841
|
}
|
|
61264
61842
|
function mountWebAppIfBuilt(app) {
|
|
61265
61843
|
const dist = resolveWebDist();
|
|
61266
|
-
if (
|
|
61844
|
+
if (existsSync22(join21(dist, "index.html")))
|
|
61267
61845
|
mountWebApp(app, dist);
|
|
61268
61846
|
}
|
|
61269
61847
|
var MIME;
|
|
@@ -61290,16 +61868,16 @@ var init_web = __esm(() => {
|
|
|
61290
61868
|
});
|
|
61291
61869
|
|
|
61292
61870
|
// src/version.ts
|
|
61293
|
-
import { readFileSync as
|
|
61294
|
-
import { dirname as dirname13, join as
|
|
61295
|
-
import { fileURLToPath as
|
|
61871
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
61872
|
+
import { dirname as dirname13, join as join23 } from "path";
|
|
61873
|
+
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
61296
61874
|
function runcastleVersion() {
|
|
61297
61875
|
if (cached4)
|
|
61298
61876
|
return cached4;
|
|
61299
|
-
const here = dirname13(
|
|
61300
|
-
for (const pkgPath of [
|
|
61877
|
+
const here = dirname13(fileURLToPath8(import.meta.url));
|
|
61878
|
+
for (const pkgPath of [join23(here, "..", "package.json"), join23(here, "package.json")]) {
|
|
61301
61879
|
try {
|
|
61302
|
-
const pkg = JSON.parse(
|
|
61880
|
+
const pkg = JSON.parse(readFileSync13(pkgPath, "utf8"));
|
|
61303
61881
|
if (typeof pkg.version === "string") {
|
|
61304
61882
|
cached4 = pkg.version;
|
|
61305
61883
|
return cached4;
|
|
@@ -61314,13 +61892,13 @@ var init_version = () => {};
|
|
|
61314
61892
|
|
|
61315
61893
|
// src/services/install-id.ts
|
|
61316
61894
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
61317
|
-
import { existsSync as
|
|
61895
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
|
|
61318
61896
|
import { dirname as dirname14 } from "path";
|
|
61319
61897
|
function getInstallId() {
|
|
61320
61898
|
const file = installIdPath();
|
|
61321
61899
|
try {
|
|
61322
|
-
if (
|
|
61323
|
-
const existing =
|
|
61900
|
+
if (existsSync23(file)) {
|
|
61901
|
+
const existing = readFileSync14(file, "utf8").trim();
|
|
61324
61902
|
if (UUID_RE.test(existing))
|
|
61325
61903
|
return existing;
|
|
61326
61904
|
}
|
|
@@ -61519,8 +62097,8 @@ var init_events2 = __esm(() => {
|
|
|
61519
62097
|
});
|
|
61520
62098
|
|
|
61521
62099
|
// src/launcher/launcher.ts
|
|
61522
|
-
import { existsSync as
|
|
61523
|
-
import { join as
|
|
62100
|
+
import { existsSync as existsSync24 } from "fs";
|
|
62101
|
+
import { join as join25 } from "path";
|
|
61524
62102
|
import { and as and12, eq as eq15 } from "drizzle-orm";
|
|
61525
62103
|
function ptyExitMessage(exitCode) {
|
|
61526
62104
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
@@ -61995,7 +62573,7 @@ async function buildPrepareBrief(ctx, project3) {
|
|
|
61995
62573
|
};
|
|
61996
62574
|
}
|
|
61997
62575
|
function probePrepareHost(repoPath) {
|
|
61998
|
-
const has4 = (rel) =>
|
|
62576
|
+
const has4 = (rel) => existsSync24(join25(repoPath, rel));
|
|
61999
62577
|
const lock = LOCKFILES.find(([file]) => has4(file));
|
|
62000
62578
|
return {
|
|
62001
62579
|
platform: process.platform,
|
|
@@ -62188,7 +62766,7 @@ var init_launcher = __esm(() => {
|
|
|
62188
62766
|
|
|
62189
62767
|
// src/services/outcome.ts
|
|
62190
62768
|
import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
62191
|
-
import { join as
|
|
62769
|
+
import { join as join26 } from "path";
|
|
62192
62770
|
function digestHasSubstance(text3) {
|
|
62193
62771
|
return text3.split(`
|
|
62194
62772
|
`).some((line) => {
|
|
@@ -62288,9 +62866,9 @@ async function promoteOutcomeDoc(ctx, project3, feature, target, delta) {
|
|
|
62288
62866
|
delta: { commits: delta.commits ?? 0, files: delta.files ?? 0 }
|
|
62289
62867
|
});
|
|
62290
62868
|
await onBranch(project3.repoPath, target, async () => {
|
|
62291
|
-
const docsDir =
|
|
62869
|
+
const docsDir = join26(project3.repoPath, ...featureDocsRel(feature.slug).split("/"));
|
|
62292
62870
|
mkdirSync14(docsDir, { recursive: true });
|
|
62293
|
-
writeFileSync10(
|
|
62871
|
+
writeFileSync10(join26(docsDir, "outcome.md"), doc, "utf8");
|
|
62294
62872
|
await commitDocs(project3.repoPath, `runcastle: outcome for ${feature.slug}`);
|
|
62295
62873
|
});
|
|
62296
62874
|
} catch (error) {
|
|
@@ -62417,9 +62995,9 @@ var init_feature = __esm(() => {
|
|
|
62417
62995
|
});
|
|
62418
62996
|
|
|
62419
62997
|
// src/services/fsbrowse.ts
|
|
62420
|
-
import { existsSync as
|
|
62998
|
+
import { existsSync as existsSync25, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
|
|
62421
62999
|
import { homedir as homedir4 } from "os";
|
|
62422
|
-
import { basename as basename3, dirname as dirname15, isAbsolute as isAbsolute3, join as
|
|
63000
|
+
import { basename as basename3, dirname as dirname15, isAbsolute as isAbsolute3, join as join27, resolve as resolve12, sep as sep4 } from "path";
|
|
62423
63001
|
function expandPath(input, home = homedir4()) {
|
|
62424
63002
|
const trimmed = input.trim();
|
|
62425
63003
|
if (trimmed === "")
|
|
@@ -62428,12 +63006,12 @@ function expandPath(input, home = homedir4()) {
|
|
|
62428
63006
|
if (out === "~")
|
|
62429
63007
|
out = home;
|
|
62430
63008
|
else if (out.startsWith("~/") || out.startsWith("~\\"))
|
|
62431
|
-
out =
|
|
63009
|
+
out = join27(home, out.slice(2));
|
|
62432
63010
|
return resolve12(out);
|
|
62433
63011
|
}
|
|
62434
63012
|
function looksLikeRepo(dir) {
|
|
62435
63013
|
try {
|
|
62436
|
-
return
|
|
63014
|
+
return existsSync25(join27(dir, ".git"));
|
|
62437
63015
|
} catch {
|
|
62438
63016
|
return false;
|
|
62439
63017
|
}
|
|
@@ -62444,20 +63022,20 @@ function listRoots(home = homedir4()) {
|
|
|
62444
63022
|
for (let code = 67;code <= 90; code++) {
|
|
62445
63023
|
const drive = `${String.fromCharCode(code)}:${sep4}`;
|
|
62446
63024
|
try {
|
|
62447
|
-
if (
|
|
63025
|
+
if (existsSync25(drive))
|
|
62448
63026
|
roots3.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
|
|
62449
63027
|
} catch {}
|
|
62450
63028
|
}
|
|
62451
63029
|
} else {
|
|
62452
63030
|
roots3.push({ label: "/", path: "/", kind: "drive" });
|
|
62453
63031
|
}
|
|
62454
|
-
if (
|
|
63032
|
+
if (existsSync25(home))
|
|
62455
63033
|
roots3.push({ label: "Home", path: home, kind: "home" });
|
|
62456
63034
|
const seen = new Set(roots3.map(pathKey));
|
|
62457
63035
|
for (const name of COMMON_DIRS) {
|
|
62458
|
-
const candidate =
|
|
63036
|
+
const candidate = join27(home, name);
|
|
62459
63037
|
const key = pathKey({ path: candidate });
|
|
62460
|
-
if (seen.has(key) || !
|
|
63038
|
+
if (seen.has(key) || !existsSync25(candidate))
|
|
62461
63039
|
continue;
|
|
62462
63040
|
seen.add(key);
|
|
62463
63041
|
roots3.push({ label: name, path: candidate, kind: "common" });
|
|
@@ -62488,12 +63066,12 @@ function browseDir(input, showHidden = false, home = homedir4()) {
|
|
|
62488
63066
|
if (!isAbsolute3(target)) {
|
|
62489
63067
|
throw new InvalidInputError(`path is not absolute: ${target}`);
|
|
62490
63068
|
}
|
|
62491
|
-
if (!
|
|
63069
|
+
if (!existsSync25(target)) {
|
|
62492
63070
|
throw new InvalidInputError(`path does not exist: ${target}`);
|
|
62493
63071
|
}
|
|
62494
63072
|
let dir = target;
|
|
62495
63073
|
try {
|
|
62496
|
-
if (!
|
|
63074
|
+
if (!statSync7(target).isDirectory())
|
|
62497
63075
|
dir = dirname15(target);
|
|
62498
63076
|
} catch (e) {
|
|
62499
63077
|
throw new InvalidInputError(`cannot read path: ${target} (${errMsg4(e)})`);
|
|
@@ -62516,14 +63094,14 @@ function browseDir(input, showHidden = false, home = homedir4()) {
|
|
|
62516
63094
|
continue;
|
|
62517
63095
|
if (!showHidden && name.startsWith("."))
|
|
62518
63096
|
continue;
|
|
62519
|
-
const path2 =
|
|
63097
|
+
const path2 = join27(dir, name);
|
|
62520
63098
|
const isSymlink = dirent.isSymbolicLink();
|
|
62521
63099
|
if (isSymlink && !showHidden)
|
|
62522
63100
|
continue;
|
|
62523
63101
|
let isDirectory = dirent.isDirectory();
|
|
62524
63102
|
if (isSymlink) {
|
|
62525
63103
|
try {
|
|
62526
|
-
isDirectory =
|
|
63104
|
+
isDirectory = statSync7(path2).isDirectory();
|
|
62527
63105
|
} catch {
|
|
62528
63106
|
continue;
|
|
62529
63107
|
}
|
|
@@ -62553,7 +63131,7 @@ var init_fsbrowse = __esm(() => {
|
|
|
62553
63131
|
});
|
|
62554
63132
|
|
|
62555
63133
|
// src/services/transcripts.ts
|
|
62556
|
-
import { readFileSync as
|
|
63134
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
62557
63135
|
function* entries(jsonl) {
|
|
62558
63136
|
for (const line of jsonl.split(`
|
|
62559
63137
|
`)) {
|
|
@@ -62634,7 +63212,7 @@ function readTranscript2(path2, runtime5) {
|
|
|
62634
63212
|
if (!path2)
|
|
62635
63213
|
return { status: "ok", turns: [] };
|
|
62636
63214
|
try {
|
|
62637
|
-
return parseTranscript(
|
|
63215
|
+
return parseTranscript(readFileSync15(path2, "utf8"), runtime5);
|
|
62638
63216
|
} catch {
|
|
62639
63217
|
return { status: "ok", turns: [] };
|
|
62640
63218
|
}
|
|
@@ -62747,8 +63325,8 @@ var init_conversations = __esm(() => {
|
|
|
62747
63325
|
});
|
|
62748
63326
|
|
|
62749
63327
|
// src/services/projects.ts
|
|
62750
|
-
import { existsSync as
|
|
62751
|
-
import { basename as basename4, join as
|
|
63328
|
+
import { existsSync as existsSync26 } from "fs";
|
|
63329
|
+
import { basename as basename4, join as join28 } from "path";
|
|
62752
63330
|
import { eq as eq16, isNull } from "drizzle-orm";
|
|
62753
63331
|
function listProjects(ctx) {
|
|
62754
63332
|
return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
|
|
@@ -62827,7 +63405,7 @@ async function assertRepoTolerant(ctx, repoPath) {
|
|
|
62827
63405
|
} catch (e) {
|
|
62828
63406
|
if (!isNotImplemented(e))
|
|
62829
63407
|
throw e;
|
|
62830
|
-
if (!
|
|
63408
|
+
if (!existsSync26(join28(repoPath, ".git"))) {
|
|
62831
63409
|
throw new InvalidInputError(`not a git repository: ${repoPath}`);
|
|
62832
63410
|
}
|
|
62833
63411
|
}
|
|
@@ -62892,6 +63470,7 @@ var init_review_findings2 = __esm(() => {
|
|
|
62892
63470
|
reviewFindingsRouter = router({
|
|
62893
63471
|
listByFeature: publicProcedure.input(z9.object({ featureId: z9.string() })).query(({ ctx, input }) => viewByFeature(ctx, input.featureId)),
|
|
62894
63472
|
dismiss: publicProcedure.input(z9.object({ findingId: z9.string() })).mutation(({ ctx, input }) => dismiss(ctx, input.findingId)),
|
|
63473
|
+
reopen: publicProcedure.input(z9.object({ findingId: z9.string() })).mutation(({ ctx, input }) => reopenFinding(ctx, input.findingId)),
|
|
62895
63474
|
fixOpenDefects: publicProcedure.input(z9.object({ featureId: z9.string() })).mutation(async ({ ctx, input }) => {
|
|
62896
63475
|
const promoted = promoteOpenDefects(ctx, input.featureId);
|
|
62897
63476
|
const { runId } = await burn(ctx, input.featureId);
|
|
@@ -62977,13 +63556,13 @@ var init_settings2 = __esm(() => {
|
|
|
62977
63556
|
});
|
|
62978
63557
|
|
|
62979
63558
|
// src/doctor/afk-env.ts
|
|
62980
|
-
import { existsSync as
|
|
63559
|
+
import { existsSync as existsSync27, readFileSync as readFileSync16 } from "fs";
|
|
62981
63560
|
function envWithAfkCredentials() {
|
|
62982
63561
|
const merged = { ...process.env };
|
|
62983
63562
|
try {
|
|
62984
63563
|
const path2 = envPath();
|
|
62985
|
-
if (
|
|
62986
|
-
const fromFile = parseEnvFile3(
|
|
63564
|
+
if (existsSync27(path2)) {
|
|
63565
|
+
const fromFile = parseEnvFile3(readFileSync16(path2, "utf8"));
|
|
62987
63566
|
for (const runtime5 of AGENT_RUNTIMES) {
|
|
62988
63567
|
const key = RUNTIME_SPECS[runtime5].afkKey;
|
|
62989
63568
|
const value = fromFile[key];
|
|
@@ -63001,9 +63580,84 @@ var init_afk_env = __esm(() => {
|
|
|
63001
63580
|
init_doctor();
|
|
63002
63581
|
});
|
|
63003
63582
|
|
|
63583
|
+
// src/services/project-image.ts
|
|
63584
|
+
function adoptProjectImage(ctx, projectId, tag) {
|
|
63585
|
+
if (!isOverwritable(ctx, projectId, "sandboxImage"))
|
|
63586
|
+
return false;
|
|
63587
|
+
recordFinding(ctx, projectId, {
|
|
63588
|
+
key: "sandboxImage",
|
|
63589
|
+
value: tag,
|
|
63590
|
+
source: "build",
|
|
63591
|
+
evidence: `Built from .runcastle/sandbox/Dockerfile as ${tag}.`
|
|
63592
|
+
});
|
|
63593
|
+
emitProject(ctx, projectId, {
|
|
63594
|
+
type: "settings.updated",
|
|
63595
|
+
message: `sandboxImage set to ${tag} by the image build`,
|
|
63596
|
+
data: { key: "sandboxImage", scope: "project", value: tag }
|
|
63597
|
+
});
|
|
63598
|
+
return true;
|
|
63599
|
+
}
|
|
63600
|
+
function releaseProjectImage(ctx, projectId) {
|
|
63601
|
+
if (!isOverwritable(ctx, projectId, "sandboxImage"))
|
|
63602
|
+
return false;
|
|
63603
|
+
recordFinding(ctx, projectId, { key: "sandboxImage", value: null, source: "build" });
|
|
63604
|
+
emitProject(ctx, projectId, {
|
|
63605
|
+
type: "settings.updated",
|
|
63606
|
+
message: "sandboxImage cleared \u2014 .runcastle/sandbox/Dockerfile is gone",
|
|
63607
|
+
data: { key: "sandboxImage", scope: "project", value: null }
|
|
63608
|
+
});
|
|
63609
|
+
return true;
|
|
63610
|
+
}
|
|
63611
|
+
var init_project_image = __esm(() => {
|
|
63612
|
+
init_events();
|
|
63613
|
+
init_findings();
|
|
63614
|
+
});
|
|
63615
|
+
|
|
63004
63616
|
// src/trpc/routers/setup.ts
|
|
63617
|
+
import { join as join29 } from "path";
|
|
63005
63618
|
import { isNotNull as isNotNull4 } from "drizzle-orm";
|
|
63006
63619
|
import * as z12 from "zod";
|
|
63620
|
+
function projectImageEnv(ctx, project3) {
|
|
63621
|
+
return {
|
|
63622
|
+
id: project3.id,
|
|
63623
|
+
repoPath: project3.repoPath,
|
|
63624
|
+
stored: project3.sandboxImage ?? null,
|
|
63625
|
+
overwritable: isOverwritable(ctx, project3.id, "sandboxImage"),
|
|
63626
|
+
clearStored: () => releaseProjectImage(ctx, project3.id)
|
|
63627
|
+
};
|
|
63628
|
+
}
|
|
63629
|
+
async function buildImageTerminal(ctx, projectId) {
|
|
63630
|
+
const exec3 = createSystemExec();
|
|
63631
|
+
const runtime5 = await resolveRuntime(exec3, ctx.config.sandbox === "podman" ? "podman" : "docker");
|
|
63632
|
+
const project3 = projectId ? requireProjectById(ctx, projectId) : null;
|
|
63633
|
+
const stockContext = prepareSandboxBuildContext();
|
|
63634
|
+
const stockHash = hashDockerfile(join29(stockContext, "Dockerfile"));
|
|
63635
|
+
const plan = planImageBuild({
|
|
63636
|
+
config: ctx.config,
|
|
63637
|
+
project: project3 ? {
|
|
63638
|
+
id: project3.id,
|
|
63639
|
+
repoPath: project3.repoPath,
|
|
63640
|
+
sandboxImage: project3.sandboxImage,
|
|
63641
|
+
sandboxImageOverwritable: isOverwritable(ctx, project3.id, "sandboxImage")
|
|
63642
|
+
} : null,
|
|
63643
|
+
stockContext,
|
|
63644
|
+
stockFresh: stockHash !== null && (await inspectBuiltImage(exec3, runtime5, DEFAULT_SANDBOX_IMAGE)).hash === stockHash,
|
|
63645
|
+
buildArgs: stockBuildArgs(runtime5)
|
|
63646
|
+
});
|
|
63647
|
+
if (plan.kind === "refused")
|
|
63648
|
+
throw new InvalidInputError(plan.reason);
|
|
63649
|
+
const spec = imageBuildTerminal(runtime5, plan);
|
|
63650
|
+
if (plan.kind !== "chain" || !project3)
|
|
63651
|
+
return { spec };
|
|
63652
|
+
const { projectTag } = plan;
|
|
63653
|
+
return {
|
|
63654
|
+
spec,
|
|
63655
|
+
onExit: ({ exitCode }) => {
|
|
63656
|
+
if (exitCode === 0)
|
|
63657
|
+
adoptProjectImage(ctx, project3.id, projectTag);
|
|
63658
|
+
}
|
|
63659
|
+
};
|
|
63660
|
+
}
|
|
63007
63661
|
var setupRouter;
|
|
63008
63662
|
var init_setup2 = __esm(() => {
|
|
63009
63663
|
init_src();
|
|
@@ -63011,41 +63665,48 @@ var init_setup2 = __esm(() => {
|
|
|
63011
63665
|
init_afk_env();
|
|
63012
63666
|
init_doctor();
|
|
63013
63667
|
init_system_exec();
|
|
63668
|
+
init_errors();
|
|
63014
63669
|
init_asset_paths();
|
|
63015
63670
|
init_registry();
|
|
63671
|
+
init_findings();
|
|
63672
|
+
init_project_image();
|
|
63673
|
+
init_repo();
|
|
63674
|
+
init_sandbox_image();
|
|
63016
63675
|
init_setup();
|
|
63017
63676
|
init_resolve_executable();
|
|
63018
63677
|
init_context();
|
|
63019
63678
|
setupRouter = router({
|
|
63020
|
-
doctor: publicProcedure.
|
|
63679
|
+
doctor: publicProcedure.input(z12.object({
|
|
63680
|
+
projectId: z12.string().optional()
|
|
63681
|
+
}).optional()).query(({ ctx, input }) => {
|
|
63021
63682
|
const projectModels = ctx.db.select({ model: projects.model }).from(projects).where(isNotNull4(projects.model)).all().map((p) => p.model);
|
|
63683
|
+
const project3 = input?.projectId ? requireProjectById(ctx, input.projectId) : null;
|
|
63022
63684
|
return runDoctor({
|
|
63023
63685
|
exec: createSystemExec(),
|
|
63024
63686
|
burnerDockerfile: burnerDockerfilePath(),
|
|
63025
63687
|
env: envWithAfkCredentials(),
|
|
63026
63688
|
runtimes: configuredRuntimes(ctx.config, projectModels),
|
|
63027
|
-
|
|
63689
|
+
imageName: resolveSandboxImage(ctx.config),
|
|
63690
|
+
...project3 ? { projectImage: projectImageEnv(ctx, project3) } : {}
|
|
63028
63691
|
});
|
|
63029
63692
|
}),
|
|
63030
63693
|
runtimeGuide: publicProcedure.query(() => runtimeInstallGuide(process.platform)),
|
|
63031
63694
|
gitIdentity: publicProcedure.input(z12.object({ name: z12.string(), email: z12.string() })).mutation(({ input }) => writeGitIdentity(createSystemExec(), input)),
|
|
63032
63695
|
afkToken: publicProcedure.input(z12.object({ token: z12.string(), runtime: AgentRuntime.default("claude-code") })).mutation(({ input }) => saveAfkCredential(fileAfkTokenIo(createSystemExec(), input.runtime), input.token, input.runtime)),
|
|
63033
63696
|
seedModelDefaults: publicProcedure.input(z12.object({ runtimes: z12.array(AgentRuntime) })).mutation(({ ctx, input }) => seedModelDefaults(ctx, input.runtimes)),
|
|
63034
|
-
startTerminal: publicProcedure.input(z12.object({
|
|
63035
|
-
|
|
63036
|
-
|
|
63037
|
-
|
|
63038
|
-
const imageName = resolveSandboxImage(ctx.config);
|
|
63039
|
-
const sandcastleBin = input.kind === "build-image" ? resolveSandcastleBin() ?? undefined : undefined;
|
|
63040
|
-
const spec = terminalSpec(input.kind, { runtime: runtime5, imageName, sandcastleBin });
|
|
63697
|
+
startTerminal: publicProcedure.input(z12.object({
|
|
63698
|
+
kind: z12.enum(["setup-token", "build-image", "claude-login", "codex-login"]),
|
|
63699
|
+
projectId: z12.string().optional()
|
|
63700
|
+
})).mutation(async ({ ctx, input }) => {
|
|
63041
63701
|
const sessionId = newId("setup");
|
|
63042
|
-
const
|
|
63702
|
+
const { spec, onExit: onExit4 } = input.kind === "build-image" ? await buildImageTerminal(ctx, input.projectId) : { spec: { ...terminalSpec(input.kind), cwd: process.cwd() }, onExit: undefined };
|
|
63043
63703
|
const { file, args: args2 } = resolveSpawnTarget(spec.cmd, spec.args);
|
|
63044
63704
|
ptyRegistry().create({
|
|
63045
63705
|
sessionId,
|
|
63046
63706
|
cmd: file,
|
|
63047
63707
|
args: args2,
|
|
63048
|
-
opts: { cwd, env: process.env }
|
|
63708
|
+
opts: { cwd: spec.cwd, env: process.env },
|
|
63709
|
+
...onExit4 ? { onExit: onExit4 } : {}
|
|
63049
63710
|
});
|
|
63050
63711
|
return { sessionId };
|
|
63051
63712
|
})
|