runcastle 1.3.4 → 1.3.6
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 +1113 -462
- package/drizzle/0037_finding_carry_and_provenance.sql +5 -0
- package/drizzle/0037_project_sandbox_image.sql +1 -0
- package/index.js +1109 -460
- 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-jbVdnCzO.js +186 -0
- package/web/index.html +1 -1
- package/web/assets/index-CgCA-f62.js +0 -186
package/bin/runcastle.js
CHANGED
|
@@ -53,8 +53,8 @@ var init_docs = __esm(() => {
|
|
|
53
53
|
|
|
54
54
|
// ../core/src/config.ts
|
|
55
55
|
import * as z from "zod";
|
|
56
|
-
function resolveSandboxImage(config) {
|
|
57
|
-
return config.sandboxImage
|
|
56
|
+
function resolveSandboxImage(config, project) {
|
|
57
|
+
return project?.sandboxImage?.trim() || config.sandboxImage?.trim() || DEFAULT_SANDBOX_IMAGE;
|
|
58
58
|
}
|
|
59
59
|
function resolveBurnCacheMode(config) {
|
|
60
60
|
return BURN_CACHE_SANDBOXES.includes(config.sandbox) ? config.burnCache : "off";
|
|
@@ -182,6 +182,14 @@ import * as z2 from "zod";
|
|
|
182
182
|
function isProjectSessionKind(kind) {
|
|
183
183
|
return PROJECT_SESSION_KINDS.includes(kind);
|
|
184
184
|
}
|
|
185
|
+
function driveDenialOf(result) {
|
|
186
|
+
return {
|
|
187
|
+
...result.deniedReason ? { deniedReason: result.deniedReason } : {},
|
|
188
|
+
...result.deniedCode ? { deniedCode: result.deniedCode } : {},
|
|
189
|
+
...result.retriable !== undefined ? { retriable: result.retriable } : {},
|
|
190
|
+
...result.dirtyFiles ? { dirtyFiles: result.dirtyFiles } : {}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
185
193
|
function unresolvedMergeConflict(events) {
|
|
186
194
|
let conflict = null;
|
|
187
195
|
for (const event of events) {
|
|
@@ -198,7 +206,7 @@ function unresolvedMergeConflict(events) {
|
|
|
198
206
|
}
|
|
199
207
|
return conflict;
|
|
200
208
|
}
|
|
201
|
-
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;
|
|
209
|
+
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;
|
|
202
210
|
var init_schemas = __esm(() => {
|
|
203
211
|
init_config();
|
|
204
212
|
Phase = z2.enum([
|
|
@@ -232,8 +240,9 @@ var init_schemas = __esm(() => {
|
|
|
232
240
|
TicketKind = z2.enum(["implementation", "review"]);
|
|
233
241
|
FindingKind = z2.enum(["defect", "observation"]);
|
|
234
242
|
FindingSeverity = z2.enum(["high", "medium", "low"]);
|
|
235
|
-
FindingStatus = z2.enum(["open", "fixing", "fixed", "failed", "dismissed"]);
|
|
243
|
+
FindingStatus = z2.enum(["open", "fixing", "fixed", "failed", "dismissed", "carried"]);
|
|
236
244
|
FindingOpenReason = z2.enum(["over-cap", "fix-failed", "verification"]);
|
|
245
|
+
FindingResolvedBy = z2.enum(["fix-ticket", "session"]);
|
|
237
246
|
ReviewFindingInput = z2.object({
|
|
238
247
|
kind: FindingKind,
|
|
239
248
|
severity: FindingSeverity,
|
|
@@ -257,6 +266,9 @@ var init_schemas = __esm(() => {
|
|
|
257
266
|
openReason: FindingOpenReason.nullable(),
|
|
258
267
|
failureReason: z2.string().nullable(),
|
|
259
268
|
fixTicketId: z2.string().nullable(),
|
|
269
|
+
carriedLap: z2.number().nullable(),
|
|
270
|
+
resolvedBy: FindingResolvedBy.nullable(),
|
|
271
|
+
resolutionNote: z2.string().nullable(),
|
|
260
272
|
createdAt: z2.number()
|
|
261
273
|
});
|
|
262
274
|
TicketInput = z2.object({
|
|
@@ -333,12 +345,14 @@ var init_schemas = __esm(() => {
|
|
|
333
345
|
"driveInstructions"
|
|
334
346
|
];
|
|
335
347
|
PreparedKey = z2.enum(PREPARED_KEYS);
|
|
348
|
+
PROVENANCE_KEYS = [...PREPARED_KEYS, "sandboxImage"];
|
|
349
|
+
ProvenanceKey = z2.enum(PROVENANCE_KEYS);
|
|
336
350
|
DRIVE_LOOP_KEYS = [
|
|
337
351
|
"devCommand",
|
|
338
352
|
"driveSetupCommand",
|
|
339
353
|
"driveStopCommand"
|
|
340
354
|
];
|
|
341
|
-
FindingSource = z2.enum(["prep", "human", "session"]);
|
|
355
|
+
FindingSource = z2.enum(["prep", "human", "session", "build"]);
|
|
342
356
|
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`);
|
|
343
357
|
Project = z2.object({
|
|
344
358
|
id: z2.string(),
|
|
@@ -347,6 +361,7 @@ var init_schemas = __esm(() => {
|
|
|
347
361
|
sessionBranch: z2.string().optional(),
|
|
348
362
|
devCommand: z2.string().optional(),
|
|
349
363
|
model: z2.string().optional(),
|
|
364
|
+
sandboxImage: z2.string().optional(),
|
|
350
365
|
setupCommand: z2.string().optional(),
|
|
351
366
|
verifyCommands: z2.string().optional(),
|
|
352
367
|
knownFailures: z2.string().optional(),
|
|
@@ -357,7 +372,7 @@ var init_schemas = __esm(() => {
|
|
|
357
372
|
closedAt: z2.number().optional()
|
|
358
373
|
});
|
|
359
374
|
ProjectFinding = z2.object({
|
|
360
|
-
key:
|
|
375
|
+
key: ProvenanceKey,
|
|
361
376
|
source: FindingSource,
|
|
362
377
|
evidence: z2.string().optional(),
|
|
363
378
|
establishedAt: z2.number(),
|
|
@@ -645,6 +660,7 @@ var init_db_schema = __esm(() => {
|
|
|
645
660
|
devCommand: text("dev_command"),
|
|
646
661
|
model: text("model"),
|
|
647
662
|
sandbox: text("sandbox"),
|
|
663
|
+
sandboxImage: text("sandbox_image"),
|
|
648
664
|
closedAt: integer("closed_at"),
|
|
649
665
|
setupCommand: text("setup_command"),
|
|
650
666
|
verifyCommands: text("verify_commands"),
|
|
@@ -739,6 +755,9 @@ var init_db_schema = __esm(() => {
|
|
|
739
755
|
openReason: text("open_reason").$type(),
|
|
740
756
|
failureReason: text("failure_reason"),
|
|
741
757
|
fixTicketId: text("fix_ticket_id"),
|
|
758
|
+
carriedLap: integer("carried_lap"),
|
|
759
|
+
resolvedBy: text("resolved_by").$type(),
|
|
760
|
+
resolutionNote: text("resolution_note"),
|
|
742
761
|
createdAt: integer("created_at").notNull()
|
|
743
762
|
});
|
|
744
763
|
testNotes = sqliteTable("test_notes", {
|
|
@@ -815,51 +834,15 @@ var init_src = __esm(() => {
|
|
|
815
834
|
});
|
|
816
835
|
|
|
817
836
|
// ../core/src/paths.ts
|
|
818
|
-
var exports_paths = {};
|
|
819
|
-
__export(exports_paths, {
|
|
820
|
-
worktreeDir: () => worktreeDir,
|
|
821
|
-
sessionDir: () => sessionDir,
|
|
822
|
-
sandboxBuildDir: () => sandboxBuildDir,
|
|
823
|
-
sameDataDir: () => sameDataDir,
|
|
824
|
-
reviewWalkthroughPath: () => reviewWalkthroughPath,
|
|
825
|
-
reviewDir: () => reviewDir,
|
|
826
|
-
projectWorktreesDir: () => projectWorktreesDir,
|
|
827
|
-
prodDataDir: () => prodDataDir,
|
|
828
|
-
logsDir: () => logsDir,
|
|
829
|
-
installIdPath: () => installIdPath,
|
|
830
|
-
featureDocsRel: () => featureDocsRel,
|
|
831
|
-
envPath: () => envPath,
|
|
832
|
-
devDataDir: () => devDataDir,
|
|
833
|
-
dbPath: () => dbPath,
|
|
834
|
-
dataDir: () => dataDir,
|
|
835
|
-
configPath: () => configPath,
|
|
836
|
-
burnCacheDir: () => burnCacheDir,
|
|
837
|
-
attachmentRelPath: () => attachmentRelPath,
|
|
838
|
-
annotationsDir: () => annotationsDir,
|
|
839
|
-
annotationPath: () => annotationPath,
|
|
840
|
-
PROJECT_WORKTREE_SLUG: () => PROJECT_WORKTREE_SLUG,
|
|
841
|
-
ATTACHMENTS_DIR: () => ATTACHMENTS_DIR
|
|
842
|
-
});
|
|
843
837
|
import { homedir } from "os";
|
|
844
838
|
import { join, resolve } from "path";
|
|
845
839
|
function prodDataDir() {
|
|
846
840
|
return join(homedir(), ".runcastle");
|
|
847
841
|
}
|
|
848
|
-
function devDataDir() {
|
|
849
|
-
const override = process.env.RUNCASTLE_DEV_DATA_DIR;
|
|
850
|
-
return override ? resolve(override) : join(homedir(), ".runcastle-dev");
|
|
851
|
-
}
|
|
852
842
|
function dataDir() {
|
|
853
843
|
const override = process.env.RUNCASTLE_DATA_DIR;
|
|
854
844
|
return override ? resolve(override) : prodDataDir();
|
|
855
845
|
}
|
|
856
|
-
function sameDataDir(a, b) {
|
|
857
|
-
const canon = (p) => {
|
|
858
|
-
const abs = resolve(p).replace(/[\\/]+$/, "");
|
|
859
|
-
return process.platform === "win32" ? abs.toLowerCase().replace(/\//g, "\\") : abs;
|
|
860
|
-
};
|
|
861
|
-
return canon(a) === canon(b);
|
|
862
|
-
}
|
|
863
846
|
function dbPath() {
|
|
864
847
|
return join(dataDir(), "runcastle.db");
|
|
865
848
|
}
|
|
@@ -912,11 +895,6 @@ var ATTACHMENTS_DIR = ".runcastle-attachments", PROJECT_WORKTREE_SLUG = "__proje
|
|
|
912
895
|
var init_paths = () => {};
|
|
913
896
|
|
|
914
897
|
// ../core/src/config-load.ts
|
|
915
|
-
var exports_config_load = {};
|
|
916
|
-
__export(exports_config_load, {
|
|
917
|
-
loadConfig: () => loadConfig,
|
|
918
|
-
hostLogicalCpus: () => hostLogicalCpus
|
|
919
|
-
});
|
|
920
898
|
import { existsSync, readFileSync } from "fs";
|
|
921
899
|
import { availableParallelism } from "os";
|
|
922
900
|
function loadConfig(env = process.env, logicalCpus = hostLogicalCpus()) {
|
|
@@ -2130,6 +2108,7 @@ function rowToProject(row) {
|
|
|
2130
2108
|
sessionBranch: row.sessionBranch ?? undefined,
|
|
2131
2109
|
devCommand: row.devCommand ?? undefined,
|
|
2132
2110
|
model: row.model ?? undefined,
|
|
2111
|
+
sandboxImage: row.sandboxImage ?? undefined,
|
|
2133
2112
|
setupCommand: row.setupCommand ?? undefined,
|
|
2134
2113
|
verifyCommands: row.verifyCommands ?? undefined,
|
|
2135
2114
|
knownFailures: row.knownFailures ?? undefined,
|
|
@@ -2798,8 +2777,8 @@ var init_drive_env = () => {};
|
|
|
2798
2777
|
|
|
2799
2778
|
// src/services/findings.ts
|
|
2800
2779
|
import { and as and4, eq as eq4 } from "drizzle-orm";
|
|
2801
|
-
function
|
|
2802
|
-
return
|
|
2780
|
+
function isProvenanceKey(key) {
|
|
2781
|
+
return PROVENANCE_SET.has(key);
|
|
2803
2782
|
}
|
|
2804
2783
|
function findingRow(ctx, projectId, key) {
|
|
2805
2784
|
return ctx.db.select({
|
|
@@ -2841,7 +2820,7 @@ function recordFinding(ctx, projectId, input) {
|
|
|
2841
2820
|
});
|
|
2842
2821
|
}
|
|
2843
2822
|
function recordHuman(ctx, projectId, key, value) {
|
|
2844
|
-
if (!
|
|
2823
|
+
if (!isProvenanceKey(key))
|
|
2845
2824
|
return;
|
|
2846
2825
|
const existing = findingRow(ctx, projectId, key);
|
|
2847
2826
|
if (value === null) {
|
|
@@ -2885,8 +2864,8 @@ async function listFindings(ctx, project) {
|
|
|
2885
2864
|
distances.set(sha, await commitsSince(project.repoPath, sha, mainLine));
|
|
2886
2865
|
}
|
|
2887
2866
|
}
|
|
2888
|
-
const order = new Map(
|
|
2889
|
-
return rows.filter((row) =>
|
|
2867
|
+
const order = new Map(PROVENANCE_KEYS.map((k, i) => [k, i]));
|
|
2868
|
+
return rows.filter((row) => isProvenanceKey(row.key)).sort((a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0)).map((row) => {
|
|
2890
2869
|
const staleCommits = row.establishedSha ? distances.get(row.establishedSha) : undefined;
|
|
2891
2870
|
return {
|
|
2892
2871
|
key: row.key,
|
|
@@ -2906,7 +2885,7 @@ function unsetPreparedKeys(project) {
|
|
|
2906
2885
|
return value === undefined || value.trim() === "";
|
|
2907
2886
|
});
|
|
2908
2887
|
}
|
|
2909
|
-
var VALUE_COLUMN, COLUMN_NAME, PREPARED_SET, UNVERIFIED;
|
|
2888
|
+
var VALUE_COLUMN, COLUMN_NAME, PREPARED_SET, PROVENANCE_SET, UNVERIFIED;
|
|
2910
2889
|
var init_findings = __esm(() => {
|
|
2911
2890
|
init_src();
|
|
2912
2891
|
init_schema();
|
|
@@ -2919,7 +2898,8 @@ var init_findings = __esm(() => {
|
|
|
2919
2898
|
driveSetupCommand: projects.driveSetupCommand,
|
|
2920
2899
|
driveStopCommand: projects.driveStopCommand,
|
|
2921
2900
|
dbResetCommand: projects.dbResetCommand,
|
|
2922
|
-
driveInstructions: projects.driveInstructions
|
|
2901
|
+
driveInstructions: projects.driveInstructions,
|
|
2902
|
+
sandboxImage: projects.sandboxImage
|
|
2923
2903
|
};
|
|
2924
2904
|
COLUMN_NAME = {
|
|
2925
2905
|
setupCommand: "setupCommand",
|
|
@@ -2929,9 +2909,11 @@ var init_findings = __esm(() => {
|
|
|
2929
2909
|
driveSetupCommand: "driveSetupCommand",
|
|
2930
2910
|
driveStopCommand: "driveStopCommand",
|
|
2931
2911
|
dbResetCommand: "dbResetCommand",
|
|
2932
|
-
driveInstructions: "driveInstructions"
|
|
2912
|
+
driveInstructions: "driveInstructions",
|
|
2913
|
+
sandboxImage: "sandboxImage"
|
|
2933
2914
|
};
|
|
2934
2915
|
PREPARED_SET = new Set(PREPARED_KEYS);
|
|
2916
|
+
PROVENANCE_SET = new Set(PROVENANCE_KEYS);
|
|
2935
2917
|
UNVERIFIED = { verifiedAt: null, verifiedSha: null };
|
|
2936
2918
|
});
|
|
2937
2919
|
|
|
@@ -2964,6 +2946,18 @@ function deriveDriveState(info) {
|
|
|
2964
2946
|
function migrationPaths(paths) {
|
|
2965
2947
|
return paths.filter((p) => MIGRATION_DIR_RE.test(p) || DRIZZLE_SQL_RE.test(p));
|
|
2966
2948
|
}
|
|
2949
|
+
function deniedStart(deniedReason, deniedCode, dirtyFiles) {
|
|
2950
|
+
return {
|
|
2951
|
+
ok: false,
|
|
2952
|
+
deniedReason,
|
|
2953
|
+
deniedCode,
|
|
2954
|
+
retriable: deniedCode === "slot_held",
|
|
2955
|
+
...dirtyFiles ? { dirtyFiles } : {}
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
function truncatedFileList(files) {
|
|
2959
|
+
return `${files.slice(0, 5).join(", ")}${files.length > 5 ? ", \u2026" : ""}`;
|
|
2960
|
+
}
|
|
2967
2961
|
function featureBranch(slug) {
|
|
2968
2962
|
return `feature/${slug}`;
|
|
2969
2963
|
}
|
|
@@ -3693,6 +3687,10 @@ async function commitPipelineDocs(repoPath) {
|
|
|
3693
3687
|
await commitDocs(repoPath, "runcastle: docs the pipeline left uncommitted");
|
|
3694
3688
|
} catch {}
|
|
3695
3689
|
}
|
|
3690
|
+
async function driveBlockingPaths(repoPath) {
|
|
3691
|
+
await commitPipelineDocs(repoPath);
|
|
3692
|
+
return porcelainPaths((await git(repoPath).raw(["status", "--porcelain"])).trim());
|
|
3693
|
+
}
|
|
3696
3694
|
function activeTestDriveFeatureId() {
|
|
3697
3695
|
return testDriveState?.kind === "feature" ? testDriveState.featureId : undefined;
|
|
3698
3696
|
}
|
|
@@ -3780,7 +3778,7 @@ async function testDrive(ctx, project, feature, action, purpose = "human") {
|
|
|
3780
3778
|
if (carriedChanges.length > 0) {
|
|
3781
3779
|
emit(ctx, feature.id, {
|
|
3782
3780
|
type: "testdrive.carried_changes",
|
|
3783
|
-
message: `${carriedChanges.length} uncommitted file(s) came back with you onto ${previousBranch2}: ${carriedChanges
|
|
3781
|
+
message: `${carriedChanges.length} uncommitted file(s) came back with you onto ${previousBranch2}: ${truncatedFileList(carriedChanges)}`,
|
|
3784
3782
|
data: { branch: previousBranch2, files: carriedChanges }
|
|
3785
3783
|
});
|
|
3786
3784
|
}
|
|
@@ -3793,18 +3791,22 @@ async function testDrive(ctx, project, feature, action, purpose = "human") {
|
|
|
3793
3791
|
...teardown?.failure ? { hookFailure: teardown.failure } : {}
|
|
3794
3792
|
};
|
|
3795
3793
|
}
|
|
3796
|
-
await
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3794
|
+
const dirtyFiles = await driveBlockingPaths(project.repoPath);
|
|
3795
|
+
if (dirtyFiles.length > 0) {
|
|
3796
|
+
if (purpose === "review") {
|
|
3797
|
+
emit(ctx, feature.id, {
|
|
3798
|
+
type: "reviewdrive.denied",
|
|
3799
|
+
message: `review drive denied \u2014 ${dirtyFiles.length} uncommitted file(s) in the working tree: ${truncatedFileList(dirtyFiles)}`,
|
|
3800
|
+
data: { code: "dirty", dirtyFiles }
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
return deniedStart(DENY_DIRTY, "dirty", dirtyFiles);
|
|
3804
|
+
}
|
|
3800
3805
|
if (testDriveState) {
|
|
3801
|
-
return
|
|
3802
|
-
ok: false,
|
|
3803
|
-
deniedReason: testDriveState.kind === "dryRun" ? DENY_DRY_RUN_ACTIVE : DENY_ACTIVE
|
|
3804
|
-
};
|
|
3806
|
+
return deniedStart(testDriveState.kind === "dryRun" ? DENY_DRY_RUN_ACTIVE : DENY_ACTIVE, "slot_held");
|
|
3805
3807
|
}
|
|
3806
3808
|
if (purpose === "human" && hasActiveRun(ctx, feature.id)) {
|
|
3807
|
-
return
|
|
3809
|
+
return deniedStart(DENY_ACTIVE_RUN, "active_run");
|
|
3808
3810
|
}
|
|
3809
3811
|
const talkWorktree = worktreeDir(project.id, feature.slug);
|
|
3810
3812
|
let detachedWorktree;
|
|
@@ -3864,7 +3866,7 @@ async function reviewDrive(ctx, project, feature, action) {
|
|
|
3864
3866
|
return {
|
|
3865
3867
|
ok: stop.ok,
|
|
3866
3868
|
action: "stop",
|
|
3867
|
-
...stop
|
|
3869
|
+
...driveDenialOf(stop),
|
|
3868
3870
|
drive: activeDriveInfo(),
|
|
3869
3871
|
...stop.hookFailure ? { hookFailure: stop.hookFailure } : {}
|
|
3870
3872
|
};
|
|
@@ -3893,7 +3895,7 @@ async function startReviewDrive(ctx, project, feature) {
|
|
|
3893
3895
|
return {
|
|
3894
3896
|
ok: start.ok,
|
|
3895
3897
|
action: "start",
|
|
3896
|
-
...start
|
|
3898
|
+
...driveDenialOf(start),
|
|
3897
3899
|
drive: activeDriveInfo(),
|
|
3898
3900
|
...start.hookFailure ? { hookFailure: start.hookFailure } : {}
|
|
3899
3901
|
};
|
|
@@ -4109,15 +4111,17 @@ ${result.output}` : ""}`,
|
|
|
4109
4111
|
}
|
|
4110
4112
|
async function dirtyPaths(g) {
|
|
4111
4113
|
try {
|
|
4112
|
-
|
|
4113
|
-
if (!out)
|
|
4114
|
-
return [];
|
|
4115
|
-
return out.split(`
|
|
4116
|
-
`).map((line) => line.slice(3).trim()).map((p) => p.includes(" -> ") ? p.split(" -> ").at(-1) ?? p : p).map((p) => p.replace(/^"|"$/g, "")).filter(Boolean);
|
|
4114
|
+
return porcelainPaths((await g.raw(["status", "--porcelain"])).trim());
|
|
4117
4115
|
} catch {
|
|
4118
4116
|
return [];
|
|
4119
4117
|
}
|
|
4120
4118
|
}
|
|
4119
|
+
function porcelainPaths(out) {
|
|
4120
|
+
if (!out)
|
|
4121
|
+
return [];
|
|
4122
|
+
return out.split(`
|
|
4123
|
+
`).map((line) => line.slice(3).trim()).map((p) => p.includes(" -> ") ? p.split(" -> ").at(-1) ?? p : p).map((p) => p.replace(/^"|"$/g, "")).filter(Boolean);
|
|
4124
|
+
}
|
|
4121
4125
|
async function detectDbDrift(ctx, project, feature, previousBranch, driveBranch) {
|
|
4122
4126
|
let files;
|
|
4123
4127
|
try {
|
|
@@ -4525,7 +4529,7 @@ function reportFinding(ctx, args) {
|
|
|
4525
4529
|
}
|
|
4526
4530
|
function updateStatus(ctx, findingId, patch) {
|
|
4527
4531
|
const current = getFinding(ctx, findingId);
|
|
4528
|
-
ctx.db.update(reviewFindings).set(patch).where(eq6(reviewFindings.id, findingId)).run();
|
|
4532
|
+
ctx.db.update(reviewFindings).set({ carriedLap: null, resolvedBy: null, resolutionNote: null, ...patch }).where(eq6(reviewFindings.id, findingId)).run();
|
|
4529
4533
|
emit(ctx, current.featureId, {
|
|
4530
4534
|
type: "finding.updated",
|
|
4531
4535
|
message: `finding ${findingId} marked ${patch.status}`,
|
|
@@ -4544,7 +4548,8 @@ function markFixed(ctx, findingId) {
|
|
|
4544
4548
|
return updateStatus(ctx, findingId, {
|
|
4545
4549
|
status: "fixed",
|
|
4546
4550
|
openReason: null,
|
|
4547
|
-
failureReason: null
|
|
4551
|
+
failureReason: null,
|
|
4552
|
+
resolvedBy: "fix-ticket"
|
|
4548
4553
|
});
|
|
4549
4554
|
}
|
|
4550
4555
|
function markFailed(ctx, findingId, reason) {
|
|
@@ -4571,6 +4576,8 @@ function dismiss(ctx, findingId) {
|
|
|
4571
4576
|
function defectState(finding, fixTicket) {
|
|
4572
4577
|
if (finding.status === "dismissed")
|
|
4573
4578
|
return "dismissed";
|
|
4579
|
+
if (finding.status === "carried")
|
|
4580
|
+
return "carried";
|
|
4574
4581
|
if (finding.status === "fixed" || fixTicket?.status === "done")
|
|
4575
4582
|
return "fixed";
|
|
4576
4583
|
if (fixTicket && fixTicket.status !== "failed" && fixTicket.status !== "cancelled") {
|
|
@@ -4579,11 +4586,15 @@ function defectState(finding, fixTicket) {
|
|
|
4579
4586
|
return finding.status === "open" || finding.status === "failed" || fixTicket?.status === "failed" || fixTicket?.status === "cancelled" ? "open" : "fixing";
|
|
4580
4587
|
}
|
|
4581
4588
|
function viewByFeature(ctx, featureId) {
|
|
4589
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
4582
4590
|
const findings = listByFeature3(ctx, featureId);
|
|
4583
4591
|
const tickets2 = listByFeature2(ctx, featureId);
|
|
4584
4592
|
const summary = { found: 0, fixed: 0, open: 0, observations: 0 };
|
|
4585
4593
|
const openDefects = [];
|
|
4594
|
+
const carriedFindings = findings.filter(isCarried);
|
|
4586
4595
|
for (const finding of findings) {
|
|
4596
|
+
if (finding.lap !== feature.lap)
|
|
4597
|
+
continue;
|
|
4587
4598
|
if (finding.kind === "observation") {
|
|
4588
4599
|
summary.observations += 1;
|
|
4589
4600
|
continue;
|
|
@@ -4597,11 +4608,75 @@ function viewByFeature(ctx, featureId) {
|
|
|
4597
4608
|
openDefects.push(finding);
|
|
4598
4609
|
}
|
|
4599
4610
|
}
|
|
4600
|
-
return { findings, openDefects, summary };
|
|
4611
|
+
return { findings, openDefects, carriedFindings, summary };
|
|
4612
|
+
}
|
|
4613
|
+
function openDefectsAcrossLaps(ctx, featureId) {
|
|
4614
|
+
const tickets2 = listByFeature2(ctx, featureId);
|
|
4615
|
+
return listByFeature3(ctx, featureId).filter((finding) => finding.kind === "defect" && defectState(finding, fixTicketOf(finding, tickets2)) === "open");
|
|
4616
|
+
}
|
|
4617
|
+
function isCarried(finding) {
|
|
4618
|
+
return finding.status === "carried";
|
|
4619
|
+
}
|
|
4620
|
+
function carriedDefectsAcrossLaps(ctx, featureId) {
|
|
4621
|
+
return listByFeature3(ctx, featureId).filter(isCarried);
|
|
4622
|
+
}
|
|
4623
|
+
function undispositionedDefects(ctx, featureId) {
|
|
4624
|
+
const { lap } = getFeatureRow(ctx, featureId);
|
|
4625
|
+
return openDefectsAcrossLaps(ctx, featureId).filter((finding) => finding.lap < lap);
|
|
4601
4626
|
}
|
|
4602
4627
|
function fixTicketOf(finding, tickets2) {
|
|
4603
4628
|
return finding.fixTicketId ? tickets2.find((t) => t.id === finding.fixTicketId) : undefined;
|
|
4604
4629
|
}
|
|
4630
|
+
function requireDispositionable(ctx, featureId, findingId) {
|
|
4631
|
+
const finding = getFinding(ctx, findingId);
|
|
4632
|
+
const eligible = finding.featureId === featureId && (finding.status === "carried" || openDefectsAcrossLaps(ctx, featureId).some((defect) => defect.id === findingId));
|
|
4633
|
+
if (!eligible) {
|
|
4634
|
+
throw new InvalidInputError(`finding ${findingId} is not an open or carried defect of this feature`);
|
|
4635
|
+
}
|
|
4636
|
+
return finding;
|
|
4637
|
+
}
|
|
4638
|
+
function carryFinding(ctx, featureId, findingId, note) {
|
|
4639
|
+
requireDispositionable(ctx, featureId, findingId);
|
|
4640
|
+
return updateStatus(ctx, findingId, {
|
|
4641
|
+
status: "carried",
|
|
4642
|
+
openReason: null,
|
|
4643
|
+
failureReason: null,
|
|
4644
|
+
carriedLap: getFeatureRow(ctx, featureId).lap,
|
|
4645
|
+
resolutionNote: note?.trim() || null
|
|
4646
|
+
});
|
|
4647
|
+
}
|
|
4648
|
+
function closeAsAddressed(ctx, featureId, findingId, note) {
|
|
4649
|
+
const attestation = note.trim();
|
|
4650
|
+
if (!attestation) {
|
|
4651
|
+
throw new InvalidInputError("closing a defect as addressed requires a note saying what addressed it");
|
|
4652
|
+
}
|
|
4653
|
+
requireDispositionable(ctx, featureId, findingId);
|
|
4654
|
+
return updateStatus(ctx, findingId, {
|
|
4655
|
+
status: "fixed",
|
|
4656
|
+
openReason: null,
|
|
4657
|
+
failureReason: null,
|
|
4658
|
+
resolvedBy: "session",
|
|
4659
|
+
resolutionNote: attestation
|
|
4660
|
+
});
|
|
4661
|
+
}
|
|
4662
|
+
function requireLinkableFindings(ctx, featureId, findingIds) {
|
|
4663
|
+
if (new Set(findingIds).size !== findingIds.length) {
|
|
4664
|
+
throw new InvalidInputError("two tickets in this batch link to the same finding");
|
|
4665
|
+
}
|
|
4666
|
+
for (const findingId of findingIds)
|
|
4667
|
+
requireDispositionable(ctx, featureId, findingId);
|
|
4668
|
+
}
|
|
4669
|
+
function linkFixTicket(ctx, featureId, findingId, fixTicketId) {
|
|
4670
|
+
requireDispositionable(ctx, featureId, findingId);
|
|
4671
|
+
ctx.db.update(reviewFindings).set({ fixTicketId }).where(eq6(reviewFindings.id, findingId)).run();
|
|
4672
|
+
return markFixing(ctx, findingId);
|
|
4673
|
+
}
|
|
4674
|
+
function reopenFinding(ctx, findingId) {
|
|
4675
|
+
const finding = getFinding(ctx, findingId);
|
|
4676
|
+
if (finding.status !== "carried")
|
|
4677
|
+
throw new InvalidInputError("only a carried finding can be reopened");
|
|
4678
|
+
return updateStatus(ctx, findingId, { status: "open", openReason: null, failureReason: null });
|
|
4679
|
+
}
|
|
4605
4680
|
function promoteOpenDefects(ctx, featureId, findingIds) {
|
|
4606
4681
|
const open = viewByFeature(ctx, featureId).openDefects;
|
|
4607
4682
|
const defects = findingIds ? findingIds.map((id) => {
|
|
@@ -4647,12 +4722,17 @@ function carriedWork(ctx, featureId) {
|
|
|
4647
4722
|
const carried = ctx.db.select({ id: testNotes.id }).from(testNotes).where(and7(eq7(testNotes.featureId, featureId), eq7(testNotes.status, "carried"))).all();
|
|
4648
4723
|
return {
|
|
4649
4724
|
carriedNotes: carried.length,
|
|
4650
|
-
openDefects:
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4725
|
+
openDefects: openDefectsAcrossLaps(ctx, featureId).map(toCarriedDefect),
|
|
4726
|
+
carriedDefects: carriedDefectsAcrossLaps(ctx, featureId).map(toCarriedDefect)
|
|
4727
|
+
};
|
|
4728
|
+
}
|
|
4729
|
+
function toCarriedDefect(defect) {
|
|
4730
|
+
return {
|
|
4731
|
+
id: defect.id,
|
|
4732
|
+
title: defect.title,
|
|
4733
|
+
location: defect.location,
|
|
4734
|
+
detail: defect.detail,
|
|
4735
|
+
reproStep: defect.reproStep
|
|
4656
4736
|
};
|
|
4657
4737
|
}
|
|
4658
4738
|
function hasCarriedWork(carried) {
|
|
@@ -4957,8 +5037,12 @@ function renderRevisitPrompt(feature, lap, purpose, runtime = DEFAULT_RUNTIME, c
|
|
|
4957
5037
|
`**${carriedSummary} \u2014 address them.** They are this lap's opening agenda:`,
|
|
4958
5038
|
`- \`${docs2}/test-notes.md\`, section \`## Carried, still open\` \u2014 every note carried`,
|
|
4959
5039
|
" and not yet done, whatever lap captured it. It exists; read it.",
|
|
4960
|
-
"- `get_feature_context` \u2192 `openDefects` \u2014 each open defect with its title,",
|
|
4961
|
-
" location, detail and repro step
|
|
5040
|
+
"- `get_feature_context` \u2192 `openDefects` \u2014 each open defect with its `id`, title,",
|
|
5041
|
+
" location, detail and repro step; and `carriedDefects`, the ones an earlier lap",
|
|
5042
|
+
" parked (agenda, never an obligation to re-carry). Every open defect from an",
|
|
5043
|
+
' EARLIER lap needs one of three answers before `complete_phase("tickets")` will',
|
|
5044
|
+
" pass: **link** it (emit this lap's ticket for it with `originFindingId` set),",
|
|
5045
|
+
" **carry** it, or **close it as addressed** \u2014 the last two are `resolve_finding`.",
|
|
4962
5046
|
`- \`${docs2}/spec.md\`, section \`## Later laps\` \u2014 scope parked by earlier laps,`,
|
|
4963
5047
|
" OPTIONAL; a missing one is normal, not an error."
|
|
4964
5048
|
] : [
|
|
@@ -5538,8 +5622,152 @@ function codexLoggedIn(env = process.env, fileExists = existsSync8) {
|
|
|
5538
5622
|
}
|
|
5539
5623
|
var init_codex_auth = () => {};
|
|
5540
5624
|
|
|
5625
|
+
// src/services/sandbox-image.ts
|
|
5626
|
+
import { createHash as createHash2 } from "crypto";
|
|
5627
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
|
|
5628
|
+
import { join as join10 } from "path";
|
|
5629
|
+
function projectSandboxDir(repoPath) {
|
|
5630
|
+
return join10(repoPath, ".runcastle", "sandbox");
|
|
5631
|
+
}
|
|
5632
|
+
function projectDockerfilePath(repoPath) {
|
|
5633
|
+
return join10(projectSandboxDir(repoPath), "Dockerfile");
|
|
5634
|
+
}
|
|
5635
|
+
function projectImageTag(projectId) {
|
|
5636
|
+
return `${DEFAULT_SANDBOX_IMAGE}-${projectId}`;
|
|
5637
|
+
}
|
|
5638
|
+
function hashDockerfileContents(contents) {
|
|
5639
|
+
return createHash2("sha256").update(contents).digest("hex");
|
|
5640
|
+
}
|
|
5641
|
+
function hashDockerfile(path) {
|
|
5642
|
+
try {
|
|
5643
|
+
return hashDockerfileContents(readFileSync4(path));
|
|
5644
|
+
} catch {
|
|
5645
|
+
return null;
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
async function inspectBuiltImage(exec, runtime, tag) {
|
|
5649
|
+
const out = await exec(runtime, [
|
|
5650
|
+
"image",
|
|
5651
|
+
"inspect",
|
|
5652
|
+
"--format",
|
|
5653
|
+
`{{index .Config.Labels "${DOCKERFILE_HASH_LABEL}"}}`,
|
|
5654
|
+
tag
|
|
5655
|
+
]);
|
|
5656
|
+
if (!(out.ok && out.code === 0))
|
|
5657
|
+
return { present: false, hash: null };
|
|
5658
|
+
const value = out.stdout.trim();
|
|
5659
|
+
return { present: true, hash: value === "" || value === "<no value>" ? null : value };
|
|
5660
|
+
}
|
|
5661
|
+
function unmanagedImageReason(imageName, projectDockerfilePresent = false) {
|
|
5662
|
+
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`;
|
|
5663
|
+
return `${imageName} is a custom image managed outside runcastle \u2014 ${route}.`;
|
|
5664
|
+
}
|
|
5665
|
+
function unmanagedImage(project) {
|
|
5666
|
+
const { id, overwritable } = project;
|
|
5667
|
+
const stored = project.stored?.trim() ?? "";
|
|
5668
|
+
if (stored === "" || overwritable)
|
|
5669
|
+
return null;
|
|
5670
|
+
if (stored === DEFAULT_SANDBOX_IMAGE || stored === projectImageTag(id))
|
|
5671
|
+
return null;
|
|
5672
|
+
return stored;
|
|
5673
|
+
}
|
|
5674
|
+
function stockBuildArgs(runtime, ids2 = {
|
|
5675
|
+
uid: process.getuid?.(),
|
|
5676
|
+
gid: process.getgid?.()
|
|
5677
|
+
}) {
|
|
5678
|
+
if (runtime === "podman")
|
|
5679
|
+
return {};
|
|
5680
|
+
return {
|
|
5681
|
+
...ids2.uid === undefined ? {} : { AGENT_UID: String(ids2.uid) },
|
|
5682
|
+
...ids2.gid === undefined ? {} : { AGENT_GID: String(ids2.gid) }
|
|
5683
|
+
};
|
|
5684
|
+
}
|
|
5685
|
+
function shippedDockerfile(project) {
|
|
5686
|
+
if (!project)
|
|
5687
|
+
return null;
|
|
5688
|
+
const path = projectDockerfilePath(project.repoPath);
|
|
5689
|
+
return existsSync9(path) ? path : null;
|
|
5690
|
+
}
|
|
5691
|
+
function planImageBuild(input) {
|
|
5692
|
+
const { config: config2, project, stockContext, stockFresh, buildArgs } = input;
|
|
5693
|
+
const stockHash = hashDockerfile(join10(stockContext, "Dockerfile")) ?? "";
|
|
5694
|
+
const stockStep = {
|
|
5695
|
+
tag: DEFAULT_SANDBOX_IMAGE,
|
|
5696
|
+
context: stockContext,
|
|
5697
|
+
dockerfileHash: stockHash,
|
|
5698
|
+
buildArgs
|
|
5699
|
+
};
|
|
5700
|
+
const projectDockerfile = shippedDockerfile(project);
|
|
5701
|
+
const handTyped = project ? unmanagedImage({
|
|
5702
|
+
id: project.id,
|
|
5703
|
+
stored: project.sandboxImage ?? null,
|
|
5704
|
+
overwritable: project.sandboxImageOverwritable
|
|
5705
|
+
}) : null;
|
|
5706
|
+
if (handTyped !== null) {
|
|
5707
|
+
return {
|
|
5708
|
+
kind: "refused",
|
|
5709
|
+
imageName: handTyped,
|
|
5710
|
+
reason: unmanagedImageReason(handTyped, projectDockerfile !== null)
|
|
5711
|
+
};
|
|
5712
|
+
}
|
|
5713
|
+
if (project && projectDockerfile) {
|
|
5714
|
+
const projectTag = projectImageTag(project.id);
|
|
5715
|
+
return {
|
|
5716
|
+
kind: "chain",
|
|
5717
|
+
projectTag,
|
|
5718
|
+
steps: [
|
|
5719
|
+
...stockFresh ? [] : [stockStep],
|
|
5720
|
+
{
|
|
5721
|
+
tag: projectTag,
|
|
5722
|
+
context: projectSandboxDir(project.repoPath),
|
|
5723
|
+
dockerfileHash: hashDockerfile(projectDockerfile) ?? "",
|
|
5724
|
+
buildArgs: {}
|
|
5725
|
+
}
|
|
5726
|
+
]
|
|
5727
|
+
};
|
|
5728
|
+
}
|
|
5729
|
+
const imageName = resolveSandboxImage(config2, project);
|
|
5730
|
+
const managed = imageName === DEFAULT_SANDBOX_IMAGE || project !== null && imageName === projectImageTag(project.id);
|
|
5731
|
+
if (!managed)
|
|
5732
|
+
return { kind: "refused", imageName, reason: unmanagedImageReason(imageName) };
|
|
5733
|
+
return { kind: "stock", steps: [stockStep] };
|
|
5734
|
+
}
|
|
5735
|
+
function buildStepArgs(step) {
|
|
5736
|
+
return [
|
|
5737
|
+
"build",
|
|
5738
|
+
"-t",
|
|
5739
|
+
step.tag,
|
|
5740
|
+
"--label",
|
|
5741
|
+
`${DOCKERFILE_HASH_LABEL}=${step.dockerfileHash}`,
|
|
5742
|
+
...Object.entries(step.buildArgs).flatMap(([key, value]) => ["--build-arg", `${key}=${value}`]),
|
|
5743
|
+
step.context
|
|
5744
|
+
];
|
|
5745
|
+
}
|
|
5746
|
+
function shellQuote(arg, platform) {
|
|
5747
|
+
if (/^[\w@%+=:,./\\-]+$/.test(arg))
|
|
5748
|
+
return arg;
|
|
5749
|
+
if (platform === "win32")
|
|
5750
|
+
return `"${arg}"`;
|
|
5751
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
5752
|
+
}
|
|
5753
|
+
function imageBuildTerminal(runtime, plan, platform = process.platform) {
|
|
5754
|
+
const steps = plan.steps;
|
|
5755
|
+
const last = steps[steps.length - 1];
|
|
5756
|
+
if (!last)
|
|
5757
|
+
throw new Error("image build plan has no steps");
|
|
5758
|
+
const cwd = last.context;
|
|
5759
|
+
if (steps.length === 1)
|
|
5760
|
+
return { cmd: runtime, args: buildStepArgs(last), cwd };
|
|
5761
|
+
const line = steps.map((step) => [runtime, ...buildStepArgs(step)].map((a) => shellQuote(a, platform)).join(" ")).join(" && ");
|
|
5762
|
+
return platform === "win32" ? { cmd: process.env.ComSpec ?? "cmd.exe", args: ["/d", "/s", "/c", line], cwd } : { cmd: "/bin/sh", args: ["-c", line], cwd };
|
|
5763
|
+
}
|
|
5764
|
+
var DOCKERFILE_HASH_LABEL = "runcastle.dockerfile-hash";
|
|
5765
|
+
var init_sandbox_image = __esm(() => {
|
|
5766
|
+
init_src();
|
|
5767
|
+
});
|
|
5768
|
+
|
|
5541
5769
|
// src/doctor/doctor.ts
|
|
5542
|
-
import { existsSync as
|
|
5770
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5543
5771
|
async function versionProbe(exec, id, label, bin, fix) {
|
|
5544
5772
|
const out = await exec(bin, ["--version"]);
|
|
5545
5773
|
if (out.ok && out.code === 0) {
|
|
@@ -5652,7 +5880,7 @@ function runtimeAfkKeyProbe(spec, env, severity = "error") {
|
|
|
5652
5880
|
};
|
|
5653
5881
|
}
|
|
5654
5882
|
async function runtimeProbes(spec, deps) {
|
|
5655
|
-
const fileExists = deps.fileExists ??
|
|
5883
|
+
const fileExists = deps.fileExists ?? existsSync10;
|
|
5656
5884
|
const afkKeyProbe = runtimeAfkKeyProbe(spec, deps.env, deps.severity);
|
|
5657
5885
|
return [
|
|
5658
5886
|
await runtimeBinaryProbe(spec, deps.exec, deps.severity),
|
|
@@ -5746,49 +5974,98 @@ async function containerRuntimeProbe(exec) {
|
|
|
5746
5974
|
fix: "Install Docker Desktop or Podman (see docs/research/PREREQS-NOTES.md \xA74). Not needed for interactive-only use."
|
|
5747
5975
|
};
|
|
5748
5976
|
}
|
|
5749
|
-
async function sandcastleImageProbe(
|
|
5750
|
-
const
|
|
5751
|
-
const
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5977
|
+
async function sandcastleImageProbe(input) {
|
|
5978
|
+
const { exec, burnerDockerfile, dockerfileHash, project } = input;
|
|
5979
|
+
const runtime = await presentRuntime(exec);
|
|
5980
|
+
if (!runtime) {
|
|
5981
|
+
return {
|
|
5982
|
+
...IMAGE_ROW,
|
|
5983
|
+
status: "missing",
|
|
5984
|
+
severity: "error",
|
|
5985
|
+
detail: "no container runtime available to inspect the image",
|
|
5986
|
+
fix: "Install a container runtime (Docker or Podman) first, then build the image from the Enable AFK burns card in the app."
|
|
5987
|
+
};
|
|
5988
|
+
}
|
|
5989
|
+
const projectHash = project ? dockerfileHash(projectDockerfilePath(project.repoPath)) : null;
|
|
5990
|
+
const handTyped = project ? unmanagedImage(project) : null;
|
|
5991
|
+
let stored = project?.stored ?? null;
|
|
5992
|
+
if (project && stored !== null && projectHash === null && project.overwritable) {
|
|
5993
|
+
project.clearStored();
|
|
5994
|
+
stored = null;
|
|
5995
|
+
}
|
|
5996
|
+
const stockHash = dockerfileHash(burnerDockerfile);
|
|
5997
|
+
const stock = await inspectBuiltImage(exec, runtime, DEFAULT_SANDBOX_IMAGE);
|
|
5998
|
+
const stockFresh = stock.present && (stockHash === null || stock.hash === stockHash);
|
|
5999
|
+
if (project && handTyped === null && projectHash !== null) {
|
|
6000
|
+
const tag = projectImageTag(project.id);
|
|
6001
|
+
const image = await inspectBuiltImage(exec, runtime, tag);
|
|
6002
|
+
if (!image.present || project.overwritable && stored !== tag) {
|
|
6003
|
+
return {
|
|
6004
|
+
...IMAGE_ROW,
|
|
6005
|
+
status: "not-built-yet",
|
|
6006
|
+
severity: "error",
|
|
6007
|
+
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`,
|
|
6008
|
+
fix: "Open Settings \u2192 Burns (Build image) \u2014 runcastle builds .runcastle/sandbox/Dockerfile for you."
|
|
6009
|
+
};
|
|
6010
|
+
}
|
|
6011
|
+
if (image.hash !== projectHash) {
|
|
6012
|
+
return staleImage(`${tag} no longer matches .runcastle/sandbox/Dockerfile`);
|
|
6013
|
+
}
|
|
6014
|
+
if (!stockFresh) {
|
|
6015
|
+
const base = stock.present ? "no longer matches the burner Dockerfile" : "is not built";
|
|
6016
|
+
return staleImage(`${tag} is built on ${DEFAULT_SANDBOX_IMAGE}, which ${base}`);
|
|
5773
6017
|
}
|
|
5774
6018
|
return {
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
6019
|
+
...IMAGE_ROW,
|
|
6020
|
+
status: "ok",
|
|
6021
|
+
severity: "error",
|
|
6022
|
+
detail: `${tag} built from .runcastle/sandbox/Dockerfile`
|
|
6023
|
+
};
|
|
6024
|
+
}
|
|
6025
|
+
const imageName = resolveSandboxImage({ sandboxImage: input.imageName }, { sandboxImage: stored });
|
|
6026
|
+
if (imageName !== DEFAULT_SANDBOX_IMAGE) {
|
|
6027
|
+
const custom = await inspectBuiltImage(exec, runtime, imageName);
|
|
6028
|
+
const clauses = [
|
|
6029
|
+
`${imageName} is a custom image, managed outside runcastle`,
|
|
6030
|
+
...projectHash === null ? [] : [`and outranks this repo's .runcastle/sandbox/Dockerfile`],
|
|
6031
|
+
...custom.present ? [] : ["and is not built locally"]
|
|
6032
|
+
];
|
|
6033
|
+
return {
|
|
6034
|
+
...IMAGE_ROW,
|
|
6035
|
+
status: "custom",
|
|
6036
|
+
severity: custom.present ? "info" : "error",
|
|
6037
|
+
detail: clauses.join(" \u2014 "),
|
|
6038
|
+
fix: unmanagedImageReason(imageName, projectHash !== null)
|
|
6039
|
+
};
|
|
6040
|
+
}
|
|
6041
|
+
if (!stock.present) {
|
|
6042
|
+
return {
|
|
6043
|
+
...IMAGE_ROW,
|
|
5778
6044
|
status: "missing",
|
|
5779
6045
|
severity: "error",
|
|
5780
6046
|
detail: `image ${imageName} not found locally`,
|
|
5781
6047
|
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.'
|
|
5782
6048
|
};
|
|
5783
6049
|
}
|
|
6050
|
+
if (!stockFresh)
|
|
6051
|
+
return staleImage(`${imageName} no longer matches the burner Dockerfile`);
|
|
6052
|
+
return { ...IMAGE_ROW, status: "ok", severity: "error", detail: `${imageName} present` };
|
|
6053
|
+
}
|
|
6054
|
+
async function presentRuntime(exec) {
|
|
6055
|
+
for (const runtime of ["docker", "podman"]) {
|
|
6056
|
+
const present = await exec(runtime, ["--version"]);
|
|
6057
|
+
if (present.ok && present.code === 0)
|
|
6058
|
+
return runtime;
|
|
6059
|
+
}
|
|
6060
|
+
return null;
|
|
6061
|
+
}
|
|
6062
|
+
function staleImage(detail) {
|
|
5784
6063
|
return {
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
tier: 2,
|
|
5788
|
-
status: "missing",
|
|
6064
|
+
...IMAGE_ROW,
|
|
6065
|
+
status: "stale",
|
|
5789
6066
|
severity: "error",
|
|
5790
|
-
detail:
|
|
5791
|
-
fix: "
|
|
6067
|
+
detail: `${detail} \u2014 rebuild`,
|
|
6068
|
+
fix: "Open Settings \u2192 Burns (Rebuild image)."
|
|
5792
6069
|
};
|
|
5793
6070
|
}
|
|
5794
6071
|
async function runDoctor(env) {
|
|
@@ -5796,7 +6073,7 @@ async function runDoctor(env) {
|
|
|
5796
6073
|
const processEnv = env.env ?? process.env;
|
|
5797
6074
|
const imageName = env.imageName ?? DEFAULT_SANDBOX_IMAGE;
|
|
5798
6075
|
const burnerDockerfile = env.burnerDockerfile ?? burnerDockerfilePath();
|
|
5799
|
-
const
|
|
6076
|
+
const dockerfileHash = env.dockerfileHash ?? hashDockerfile;
|
|
5800
6077
|
const required = new Set(env.runtimes ?? [DEFAULT_RUNTIME]);
|
|
5801
6078
|
const results = [
|
|
5802
6079
|
await bunProbe(exec),
|
|
@@ -5811,7 +6088,13 @@ async function runDoctor(env) {
|
|
|
5811
6088
|
severity: required.has(runtime) ? "error" : "info"
|
|
5812
6089
|
}));
|
|
5813
6090
|
}
|
|
5814
|
-
results.push(await gitIdentityProbe(exec, env.cwd), await containerRuntimeProbe(exec), await sandcastleImageProbe(
|
|
6091
|
+
results.push(await gitIdentityProbe(exec, env.cwd), await containerRuntimeProbe(exec), await sandcastleImageProbe({
|
|
6092
|
+
exec,
|
|
6093
|
+
imageName,
|
|
6094
|
+
burnerDockerfile,
|
|
6095
|
+
dockerfileHash,
|
|
6096
|
+
...env.projectImage ? { project: env.projectImage } : {}
|
|
6097
|
+
}));
|
|
5815
6098
|
const counts = (r) => r.severity === "error";
|
|
5816
6099
|
return {
|
|
5817
6100
|
results,
|
|
@@ -5824,11 +6107,12 @@ function exitCodeFor(report, mode) {
|
|
|
5824
6107
|
return report.tier1Ok ? 0 : 1;
|
|
5825
6108
|
return report.ok ? 0 : 1;
|
|
5826
6109
|
}
|
|
5827
|
-
var RUNTIME_SPECS;
|
|
6110
|
+
var RUNTIME_SPECS, IMAGE_ROW;
|
|
5828
6111
|
var init_doctor = __esm(() => {
|
|
5829
6112
|
init_src();
|
|
5830
6113
|
init_asset_paths();
|
|
5831
6114
|
init_codex_auth();
|
|
6115
|
+
init_sandbox_image();
|
|
5832
6116
|
RUNTIME_SPECS = {
|
|
5833
6117
|
"claude-code": {
|
|
5834
6118
|
runtime: "claude-code",
|
|
@@ -5860,21 +6144,22 @@ var init_doctor = __esm(() => {
|
|
|
5860
6144
|
authFile: codexAuthFile
|
|
5861
6145
|
}
|
|
5862
6146
|
};
|
|
6147
|
+
IMAGE_ROW = { id: "sandcastle-image", label: "Sandcastle container image", tier: 2 };
|
|
5863
6148
|
});
|
|
5864
6149
|
|
|
5865
6150
|
// src/launcher/runtimes/codex.ts
|
|
5866
6151
|
import {
|
|
5867
6152
|
copyFileSync,
|
|
5868
|
-
existsSync as
|
|
6153
|
+
existsSync as existsSync11,
|
|
5869
6154
|
mkdirSync as mkdirSync4,
|
|
5870
|
-
readFileSync as
|
|
6155
|
+
readFileSync as readFileSync5,
|
|
5871
6156
|
readdirSync as readdirSync2,
|
|
5872
|
-
statSync
|
|
6157
|
+
statSync,
|
|
5873
6158
|
writeFileSync as writeFileSync3
|
|
5874
6159
|
} from "fs";
|
|
5875
|
-
import { basename, dirname as dirname4, join as
|
|
6160
|
+
import { basename, dirname as dirname4, join as join11, resolve as resolve7 } from "path";
|
|
5876
6161
|
function codexHomeDir2(sessionId) {
|
|
5877
|
-
return
|
|
6162
|
+
return join11(sessionDir(sessionId), "codex-home");
|
|
5878
6163
|
}
|
|
5879
6164
|
function realCodexAuthFile(env = process.env) {
|
|
5880
6165
|
return codexAuthFile(env);
|
|
@@ -5883,7 +6168,7 @@ function toml(value) {
|
|
|
5883
6168
|
return JSON.stringify(value);
|
|
5884
6169
|
}
|
|
5885
6170
|
function approvalPolicyFor(permissionMode) {
|
|
5886
|
-
return permissionMode === undefined || permissionMode === "acceptEdits" ? "never" : "
|
|
6171
|
+
return permissionMode === undefined || permissionMode === "acceptEdits" ? "never" : "on-request";
|
|
5887
6172
|
}
|
|
5888
6173
|
function renderCodexConfig(input) {
|
|
5889
6174
|
const { session, config: config2, model, worktreePath, inheritedMcpServers, permissionMode } = input;
|
|
@@ -5922,11 +6207,11 @@ function mcpServerTables(userConfigToml) {
|
|
|
5922
6207
|
`).trim();
|
|
5923
6208
|
}
|
|
5924
6209
|
function readInheritedMcpServers(env = process.env) {
|
|
5925
|
-
const path =
|
|
5926
|
-
if (!
|
|
6210
|
+
const path = join11(dirname4(realCodexAuthFile(env)), "config.toml");
|
|
6211
|
+
if (!existsSync11(path))
|
|
5927
6212
|
return "";
|
|
5928
6213
|
try {
|
|
5929
|
-
return mcpServerTables(
|
|
6214
|
+
return mcpServerTables(readFileSync5(path, "utf8"));
|
|
5930
6215
|
} catch {
|
|
5931
6216
|
return "";
|
|
5932
6217
|
}
|
|
@@ -5947,19 +6232,19 @@ function renderCodexHooks(hookClient, kind, platform = process.platform) {
|
|
|
5947
6232
|
};
|
|
5948
6233
|
}
|
|
5949
6234
|
function writeCodexSkills(worktreePath, packDir) {
|
|
5950
|
-
if (!
|
|
6235
|
+
if (!existsSync11(worktreePath))
|
|
5951
6236
|
return [];
|
|
5952
|
-
const source =
|
|
5953
|
-
if (!
|
|
6237
|
+
const source = join11(packDir, "skills");
|
|
6238
|
+
if (!existsSync11(source))
|
|
5954
6239
|
return [];
|
|
5955
6240
|
const written = [];
|
|
5956
6241
|
for (const name of readdirSync2(source)) {
|
|
5957
|
-
const skill =
|
|
5958
|
-
if (!
|
|
6242
|
+
const skill = join11(source, name, "SKILL.md");
|
|
6243
|
+
if (!existsSync11(skill))
|
|
5959
6244
|
continue;
|
|
5960
|
-
const target =
|
|
6245
|
+
const target = join11(worktreePath, CODEX_SKILLS_REL, name);
|
|
5961
6246
|
mkdirSync4(target, { recursive: true });
|
|
5962
|
-
const path =
|
|
6247
|
+
const path = join11(target, "SKILL.md");
|
|
5963
6248
|
copyFileSync(skill, path);
|
|
5964
6249
|
written.push(path);
|
|
5965
6250
|
}
|
|
@@ -5973,8 +6258,8 @@ function excludeFromGit(worktreePath, entry) {
|
|
|
5973
6258
|
if (!info)
|
|
5974
6259
|
return;
|
|
5975
6260
|
mkdirSync4(info, { recursive: true });
|
|
5976
|
-
const path =
|
|
5977
|
-
const existing =
|
|
6261
|
+
const path = join11(info, "exclude");
|
|
6262
|
+
const existing = existsSync11(path) ? readFileSync5(path, "utf8") : "";
|
|
5978
6263
|
if (existing.split(/\r?\n/).some((line) => line.trim() === entry))
|
|
5979
6264
|
return;
|
|
5980
6265
|
const head = existing.length > 0 && !existing.endsWith(`
|
|
@@ -5985,17 +6270,17 @@ function excludeFromGit(worktreePath, entry) {
|
|
|
5985
6270
|
} catch {}
|
|
5986
6271
|
}
|
|
5987
6272
|
function gitInfoDir(worktreePath) {
|
|
5988
|
-
const dotGit =
|
|
5989
|
-
if (!
|
|
6273
|
+
const dotGit = join11(worktreePath, ".git");
|
|
6274
|
+
if (!existsSync11(dotGit))
|
|
5990
6275
|
return null;
|
|
5991
|
-
if (
|
|
5992
|
-
return
|
|
5993
|
-
const named = /^gitdir:\s*(.+)$/m.exec(
|
|
6276
|
+
if (statSync(dotGit).isDirectory())
|
|
6277
|
+
return join11(dotGit, "info");
|
|
6278
|
+
const named = /^gitdir:\s*(.+)$/m.exec(readFileSync5(dotGit, "utf8"));
|
|
5994
6279
|
if (!named)
|
|
5995
6280
|
return null;
|
|
5996
6281
|
const gitDir = resolve7(worktreePath, named[1].trim());
|
|
5997
6282
|
const parent = dirname4(gitDir);
|
|
5998
|
-
return basename(parent) === "worktrees" ?
|
|
6283
|
+
return basename(parent) === "worktrees" ? join11(dirname4(parent), "info") : join11(gitDir, "info");
|
|
5999
6284
|
}
|
|
6000
6285
|
function buildCodexArgs(input) {
|
|
6001
6286
|
return [
|
|
@@ -6014,7 +6299,7 @@ var init_codex = __esm(() => {
|
|
|
6014
6299
|
init_skills_root();
|
|
6015
6300
|
init_skills();
|
|
6016
6301
|
CODEX = RUNTIME_SPECS.codex;
|
|
6017
|
-
CODEX_SKILLS_REL =
|
|
6302
|
+
CODEX_SKILLS_REL = join11(".agents", "skills");
|
|
6018
6303
|
KICKOFF_LINES2 = kickoffLinesFor("codex");
|
|
6019
6304
|
codexRuntime = {
|
|
6020
6305
|
id: "codex",
|
|
@@ -6030,7 +6315,7 @@ var init_codex = __esm(() => {
|
|
|
6030
6315
|
doctorHint: 'Run `runcastle doctor` and fix its "Codex CLI" probe, then restart runcastle from a ' + "terminal where `codex --version` works."
|
|
6031
6316
|
};
|
|
6032
6317
|
}
|
|
6033
|
-
if (!
|
|
6318
|
+
if (!existsSync11(realCodexAuthFile())) {
|
|
6034
6319
|
return {
|
|
6035
6320
|
ok: false,
|
|
6036
6321
|
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()}`,
|
|
@@ -6042,9 +6327,9 @@ var init_codex = __esm(() => {
|
|
|
6042
6327
|
async writeArtifacts(input) {
|
|
6043
6328
|
const home = codexHomeDir2(input.session.id);
|
|
6044
6329
|
mkdirSync4(home, { recursive: true });
|
|
6045
|
-
const configPath2 =
|
|
6046
|
-
const hooksPath =
|
|
6047
|
-
const agentsPath =
|
|
6330
|
+
const configPath2 = join11(home, "config.toml");
|
|
6331
|
+
const hooksPath = join11(home, "hooks.json");
|
|
6332
|
+
const agentsPath = join11(home, "AGENTS.md");
|
|
6048
6333
|
writeFileSync3(configPath2, renderCodexConfig({
|
|
6049
6334
|
session: input.session,
|
|
6050
6335
|
config: input.config,
|
|
@@ -6057,8 +6342,8 @@ var init_codex = __esm(() => {
|
|
|
6057
6342
|
writeFileSync3(agentsPath, renderSessionPrompt({ ...input, runtime: this.id }), "utf8");
|
|
6058
6343
|
const files = [configPath2, hooksPath, agentsPath];
|
|
6059
6344
|
const auth = realCodexAuthFile();
|
|
6060
|
-
if (
|
|
6061
|
-
const copied =
|
|
6345
|
+
if (existsSync11(auth)) {
|
|
6346
|
+
const copied = join11(home, "auth.json");
|
|
6062
6347
|
copyFileSync(auth, copied);
|
|
6063
6348
|
files.push(copied);
|
|
6064
6349
|
}
|
|
@@ -6100,7 +6385,7 @@ var init_runtimes = __esm(() => {
|
|
|
6100
6385
|
});
|
|
6101
6386
|
|
|
6102
6387
|
// src/launcher/sessions.ts
|
|
6103
|
-
import { statSync as
|
|
6388
|
+
import { statSync as statSync2 } from "fs";
|
|
6104
6389
|
import { and as and8, desc as desc3, eq as eq8, inArray as inArray2, isNotNull as isNotNull2, sql as sql2 } from "drizzle-orm";
|
|
6105
6390
|
function createSessionRow(ctx, input) {
|
|
6106
6391
|
const row = ctx.db.insert(sessions).values({
|
|
@@ -6455,7 +6740,7 @@ function transcriptBytes(session) {
|
|
|
6455
6740
|
if (!session?.transcriptPath)
|
|
6456
6741
|
return;
|
|
6457
6742
|
try {
|
|
6458
|
-
return
|
|
6743
|
+
return statSync2(session.transcriptPath).size;
|
|
6459
6744
|
} catch {
|
|
6460
6745
|
return;
|
|
6461
6746
|
}
|
|
@@ -6533,25 +6818,49 @@ var init_reconcile = __esm(() => {
|
|
|
6533
6818
|
});
|
|
6534
6819
|
|
|
6535
6820
|
// src/launcher/runtime.ts
|
|
6821
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
6536
6822
|
function setRuntimeCtx(ctx) {
|
|
6537
6823
|
current = ctx;
|
|
6538
6824
|
}
|
|
6825
|
+
function followConfigFile(ctx) {
|
|
6826
|
+
followed = { ctx, raw: readConfigFile() };
|
|
6827
|
+
}
|
|
6828
|
+
function readConfigFile() {
|
|
6829
|
+
try {
|
|
6830
|
+
return readFileSync6(configPath(), "utf8");
|
|
6831
|
+
} catch {
|
|
6832
|
+
return "";
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
function syncFollowedConfig() {
|
|
6836
|
+
if (!followed)
|
|
6837
|
+
return;
|
|
6838
|
+
const raw = readConfigFile();
|
|
6839
|
+
if (raw === followed.raw)
|
|
6840
|
+
return;
|
|
6841
|
+
followed.raw = raw;
|
|
6842
|
+
try {
|
|
6843
|
+
followed.ctx.config = loadConfig();
|
|
6844
|
+
} catch {}
|
|
6845
|
+
}
|
|
6539
6846
|
async function getRuntimeCtx() {
|
|
6540
|
-
if (current)
|
|
6847
|
+
if (current) {
|
|
6848
|
+
syncFollowedConfig();
|
|
6541
6849
|
return current;
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
]);
|
|
6547
|
-
const db = createDb2(dbPath2());
|
|
6548
|
-
current = { db, config: loadConfig2() };
|
|
6850
|
+
}
|
|
6851
|
+
const { createDb: createDb2 } = await Promise.resolve().then(() => (init_client(), exports_client));
|
|
6852
|
+
const db = createDb2(dbPath());
|
|
6853
|
+
current = { db, config: loadConfig() };
|
|
6549
6854
|
return current;
|
|
6550
6855
|
}
|
|
6551
|
-
var current = null;
|
|
6856
|
+
var current = null, followed = null;
|
|
6857
|
+
var init_runtime = __esm(() => {
|
|
6858
|
+
init_config_load();
|
|
6859
|
+
init_paths();
|
|
6860
|
+
});
|
|
6552
6861
|
|
|
6553
6862
|
// src/services/gates.ts
|
|
6554
|
-
import { existsSync as
|
|
6863
|
+
import { existsSync as existsSync12 } from "fs";
|
|
6555
6864
|
import { and as and9, desc as desc4, eq as eq9, inArray as inArray4 } from "drizzle-orm";
|
|
6556
6865
|
function checkGate(ctx, check, feature) {
|
|
6557
6866
|
switch (check) {
|
|
@@ -6587,6 +6896,10 @@ function checkGate(ctx, check, feature) {
|
|
|
6587
6896
|
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"
|
|
6588
6897
|
};
|
|
6589
6898
|
}
|
|
6899
|
+
const undispositioned = undispositionedDefects(ctx, feature.id);
|
|
6900
|
+
if (undispositioned.length > 0) {
|
|
6901
|
+
return { satisfied: false, reason: undispositionedReason(undispositioned) };
|
|
6902
|
+
}
|
|
6590
6903
|
return { satisfied: true };
|
|
6591
6904
|
}
|
|
6592
6905
|
case "all-tickets-terminal": {
|
|
@@ -6603,6 +6916,10 @@ function checkGate(ctx, check, feature) {
|
|
|
6603
6916
|
return { satisfied: false, reason: "use the Merge button to ship" };
|
|
6604
6917
|
}
|
|
6605
6918
|
}
|
|
6919
|
+
function undispositionedReason(defects) {
|
|
6920
|
+
const titles = defects.map((defect) => `"${defect.title}"`).join(", ");
|
|
6921
|
+
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.";
|
|
6922
|
+
}
|
|
6606
6923
|
function notYetTerminal(noun, statuses, order) {
|
|
6607
6924
|
const counts = new Map;
|
|
6608
6925
|
for (const s of statuses)
|
|
@@ -6616,7 +6933,7 @@ function notYetTerminal(noun, statuses, order) {
|
|
|
6616
6933
|
}
|
|
6617
6934
|
function fileGate(ctx, feature, fileName, reason) {
|
|
6618
6935
|
const project = projectForFeature(ctx, feature);
|
|
6619
|
-
return
|
|
6936
|
+
return existsSync12(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
|
|
6620
6937
|
}
|
|
6621
6938
|
function lapWorked(ctx, feature) {
|
|
6622
6939
|
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;
|
|
@@ -6676,20 +6993,21 @@ var init_gates = __esm(() => {
|
|
|
6676
6993
|
init_events();
|
|
6677
6994
|
init_feature_docs();
|
|
6678
6995
|
init_repo();
|
|
6996
|
+
init_review_findings();
|
|
6679
6997
|
init_tickets();
|
|
6680
6998
|
init_waypoints();
|
|
6681
6999
|
DOC_WRITING_KINDS = ["ideation", "revisit", "converge"];
|
|
6682
7000
|
});
|
|
6683
7001
|
|
|
6684
7002
|
// src/services/knowledge.ts
|
|
6685
|
-
import { existsSync as
|
|
6686
|
-
import { join as
|
|
7003
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
7004
|
+
import { join as join12, relative as relative2, resolve as resolve8, sep } from "path";
|
|
6687
7005
|
function scaffoldDocs(ctx, feature, opts = {}) {
|
|
6688
7006
|
const project = projectForFeature(ctx, feature);
|
|
6689
7007
|
const dir = featureDocsDir(project, feature);
|
|
6690
7008
|
mkdirSync5(dir, { recursive: true });
|
|
6691
7009
|
const briefPath = featureDocPath(project, feature, "brief.md");
|
|
6692
|
-
if (!
|
|
7010
|
+
if (!existsSync13(briefPath)) {
|
|
6693
7011
|
const override = opts.brief?.trim();
|
|
6694
7012
|
const created = new Date().toISOString();
|
|
6695
7013
|
const brief = override ? `${override}
|
|
@@ -6716,7 +7034,7 @@ function scaffoldDocs(ctx, feature, opts = {}) {
|
|
|
6716
7034
|
function scaffoldMapDoc(project, feature, seed) {
|
|
6717
7035
|
mkdirSync5(featureDocsDir(project, feature), { recursive: true });
|
|
6718
7036
|
const mapPath = featureDocPath(project, feature, "map.md");
|
|
6719
|
-
if (
|
|
7037
|
+
if (existsSync13(mapPath))
|
|
6720
7038
|
return;
|
|
6721
7039
|
writeFileSync4(mapPath, mapDocBody(feature, seed), "utf8");
|
|
6722
7040
|
}
|
|
@@ -6736,18 +7054,18 @@ function mapDocBody(feature, seed) {
|
|
|
6736
7054
|
`);
|
|
6737
7055
|
}
|
|
6738
7056
|
function readCharter(repoRoot) {
|
|
6739
|
-
const path =
|
|
6740
|
-
if (!
|
|
7057
|
+
const path = join12(repoRoot, CHARTER_FILE);
|
|
7058
|
+
if (!existsSync13(path))
|
|
6741
7059
|
return;
|
|
6742
7060
|
try {
|
|
6743
|
-
return
|
|
7061
|
+
return readFileSync7(path, "utf8");
|
|
6744
7062
|
} catch {
|
|
6745
7063
|
return;
|
|
6746
7064
|
}
|
|
6747
7065
|
}
|
|
6748
7066
|
function listLiveAdrs(repoRoot) {
|
|
6749
|
-
const dir =
|
|
6750
|
-
if (!
|
|
7067
|
+
const dir = join12(repoRoot, ADR_DIR_REL);
|
|
7068
|
+
if (!existsSync13(dir))
|
|
6751
7069
|
return [];
|
|
6752
7070
|
const docs2 = [];
|
|
6753
7071
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
@@ -6755,7 +7073,7 @@ function listLiveAdrs(repoRoot) {
|
|
|
6755
7073
|
continue;
|
|
6756
7074
|
let content;
|
|
6757
7075
|
try {
|
|
6758
|
-
content =
|
|
7076
|
+
content = readFileSync7(join12(dir, entry.name), "utf8");
|
|
6759
7077
|
} catch {
|
|
6760
7078
|
continue;
|
|
6761
7079
|
}
|
|
@@ -6768,10 +7086,10 @@ function listLiveAdrs(repoRoot) {
|
|
|
6768
7086
|
function listDocs(ctx, feature) {
|
|
6769
7087
|
const project = projectForFeature(ctx, feature);
|
|
6770
7088
|
const dir = featureDocsDir(project, feature);
|
|
6771
|
-
if (!
|
|
7089
|
+
if (!existsSync13(dir))
|
|
6772
7090
|
return [];
|
|
6773
7091
|
return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => {
|
|
6774
|
-
const path =
|
|
7092
|
+
const path = join12(dir, e.name);
|
|
6775
7093
|
return { relPath: e.name, title: readTitle(path, e.name), ...writtenAt(path) };
|
|
6776
7094
|
}).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
6777
7095
|
}
|
|
@@ -6784,20 +7102,20 @@ function readDoc(ctx, feature, relPath) {
|
|
|
6784
7102
|
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
6785
7103
|
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
6786
7104
|
}
|
|
6787
|
-
if (!
|
|
7105
|
+
if (!existsSync13(target))
|
|
6788
7106
|
throw new NotFoundError(`doc not found: ${relPath}`);
|
|
6789
|
-
return { content:
|
|
7107
|
+
return { content: readFileSync7(target, "utf8") };
|
|
6790
7108
|
}
|
|
6791
7109
|
function writtenAt(path) {
|
|
6792
7110
|
try {
|
|
6793
|
-
return { updatedAt: Math.floor(
|
|
7111
|
+
return { updatedAt: Math.floor(statSync3(path).mtimeMs) };
|
|
6794
7112
|
} catch {
|
|
6795
7113
|
return {};
|
|
6796
7114
|
}
|
|
6797
7115
|
}
|
|
6798
7116
|
function readTitle(path, fallback) {
|
|
6799
7117
|
try {
|
|
6800
|
-
const firstLine =
|
|
7118
|
+
const firstLine = readFileSync7(path, "utf8").split(`
|
|
6801
7119
|
`, 1)[0] ?? "";
|
|
6802
7120
|
const heading = firstLine.match(/^#\s+(.+)$/);
|
|
6803
7121
|
return heading ? heading[1].trim() : fallback;
|
|
@@ -7010,6 +7328,13 @@ var require$1, __create, __defProp2, __getOwnPropDesc, __getOwnPropNames, __getP
|
|
|
7010
7328
|
toString() {
|
|
7011
7329
|
return this.items.join(this.separator);
|
|
7012
7330
|
}
|
|
7331
|
+
}, elidePayloadTokens = (command) => command.replace(/'[A-Za-z0-9+\/=]{120,}'/g, "'\u2026'"), formatExecFailureMessage = (command, result, maxTailChars = MAX_TAIL_CHARS) => {
|
|
7332
|
+
const output = [result.stdout, result.stderr].map((stream) => (stream ?? "").trimEnd()).filter((stream) => stream.length > 0).join(`
|
|
7333
|
+
`);
|
|
7334
|
+
const tail = output.length > maxTailChars ? output.slice(output.length - maxTailChars) : output;
|
|
7335
|
+
const headline = `Command failed (exit ${result.exitCode}): ${elidePayloadTokens(command)}`;
|
|
7336
|
+
return tail ? `${headline}
|
|
7337
|
+
${tail}` : headline;
|
|
7013
7338
|
};
|
|
7014
7339
|
var init_chunk_NGBM7T3E = __esm(() => {
|
|
7015
7340
|
require$1 = createRequire3(import.meta.url);
|
|
@@ -7034,7 +7359,7 @@ import { createRequire as createRequire4 } from "module";
|
|
|
7034
7359
|
import * as clack from "@clack/prompts";
|
|
7035
7360
|
import { join as join$1, basename as basename2, dirname as dirname5, resolve as resolve9, posix as posix2, normalize, isAbsolute as isAbsolute2 } from "path";
|
|
7036
7361
|
import { styleText, promisify } from "util";
|
|
7037
|
-
import { existsSync as
|
|
7362
|
+
import { existsSync as existsSync14, statSync as statSync4 } from "fs";
|
|
7038
7363
|
import { tmpdir as tmpdir2, homedir as homedir3 } from "os";
|
|
7039
7364
|
import { mkdtemp, writeFile, mkdir, readFile, rm, readdir, stat } from "fs/promises";
|
|
7040
7365
|
import { exec, execFile } from "child_process";
|
|
@@ -8113,7 +8438,7 @@ var Effect_exports, isFunction = (input) => typeof input === "function", dual =
|
|
|
8113
8438
|
next4 = b2;
|
|
8114
8439
|
}
|
|
8115
8440
|
return out;
|
|
8116
|
-
}, getEquivalence2, dedupeWith, dedupe = (self) => dedupeWith(self, equivalence()),
|
|
8441
|
+
}, 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) => {
|
|
8117
8442
|
const chunk4 = Object.create(ChunkProto);
|
|
8118
8443
|
chunk4.backing = backing;
|
|
8119
8444
|
switch (backing._tag) {
|
|
@@ -9812,7 +10137,7 @@ ${prefix}}`;
|
|
|
9812
10137
|
Object.defineProperty(error3, "toString", {
|
|
9813
10138
|
enumerable: false,
|
|
9814
10139
|
value() {
|
|
9815
|
-
const path2 = pipe(this.path,
|
|
10140
|
+
const path2 = pipe(this.path, join13(options.pathDelim));
|
|
9816
10141
|
return `(Invalid data at ${path2}: "${this.message}")`;
|
|
9817
10142
|
}
|
|
9818
10143
|
});
|
|
@@ -9827,7 +10152,7 @@ ${prefix}}`;
|
|
|
9827
10152
|
Object.defineProperty(error3, "toString", {
|
|
9828
10153
|
enumerable: false,
|
|
9829
10154
|
value() {
|
|
9830
|
-
const path2 = pipe(this.path,
|
|
10155
|
+
const path2 = pipe(this.path, join13(options.pathDelim));
|
|
9831
10156
|
return `(Missing data at ${path2}: "${this.message}")`;
|
|
9832
10157
|
}
|
|
9833
10158
|
});
|
|
@@ -9843,7 +10168,7 @@ ${prefix}}`;
|
|
|
9843
10168
|
Object.defineProperty(error3, "toString", {
|
|
9844
10169
|
enumerable: false,
|
|
9845
10170
|
value() {
|
|
9846
|
-
const path2 = pipe(this.path,
|
|
10171
|
+
const path2 = pipe(this.path, join13(options.pathDelim));
|
|
9847
10172
|
return `(Source unavailable at ${path2}: "${this.message}")`;
|
|
9848
10173
|
}
|
|
9849
10174
|
});
|
|
@@ -9858,7 +10183,7 @@ ${prefix}}`;
|
|
|
9858
10183
|
Object.defineProperty(error3, "toString", {
|
|
9859
10184
|
enumerable: false,
|
|
9860
10185
|
value() {
|
|
9861
|
-
const path2 = pipe(this.path,
|
|
10186
|
+
const path2 = pipe(this.path, join13(options.pathDelim));
|
|
9862
10187
|
return `(Unsupported operation at ${path2}: "${this.message}")`;
|
|
9863
10188
|
}
|
|
9864
10189
|
});
|
|
@@ -9888,7 +10213,7 @@ ${prefix}}`;
|
|
|
9888
10213
|
pathDelim: "_",
|
|
9889
10214
|
seqDelim: ","
|
|
9890
10215
|
}, options);
|
|
9891
|
-
const makePathString = (path) => pipe(path,
|
|
10216
|
+
const makePathString = (path) => pipe(path, join13(pathDelim));
|
|
9892
10217
|
const unmakePathString = (pathString) => pathString.split(pathDelim);
|
|
9893
10218
|
const getEnv = () => typeof process !== "undefined" && ("env" in process) && typeof process.env === "object" ? process.env : {};
|
|
9894
10219
|
const load = (path, primitive2, split2 = true) => {
|
|
@@ -10009,7 +10334,7 @@ ${prefix}}`;
|
|
|
10009
10334
|
return fail2(right3.left);
|
|
10010
10335
|
}
|
|
10011
10336
|
if (isRight2(left3) && isRight2(right3)) {
|
|
10012
|
-
const path = pipe(prefix,
|
|
10337
|
+
const path = pipe(prefix, join13("."));
|
|
10013
10338
|
const fail19 = fromFlatLoopFail(prefix, path);
|
|
10014
10339
|
const [lefts, rights] = extend(fail19, fail19, pipe(left3.right, map3(right2)), pipe(right3.right, map3(right2)));
|
|
10015
10340
|
return pipe(lefts, zip(rights), forEachSequential(([left4, right4]) => pipe(zip2(left4, right4), map10(([left5, right5]) => op.zip(left5, right5)))));
|
|
@@ -17941,7 +18266,7 @@ schema (${ast._tag}): ${ast}`;
|
|
|
17941
18266
|
const cowFlags = getCopyOnWriteFlags(process.platform);
|
|
17942
18267
|
for (const relativePath of paths) {
|
|
17943
18268
|
const src = join$1(hostRepoDir, relativePath);
|
|
17944
|
-
if (!
|
|
18269
|
+
if (!existsSync14(src)) {
|
|
17945
18270
|
continue;
|
|
17946
18271
|
}
|
|
17947
18272
|
const dest = join$1(worktreePath, relativePath);
|
|
@@ -18083,7 +18408,7 @@ ${e instanceof Error ? e.message : String(e)}`
|
|
|
18083
18408
|
await mkdir(patchesRoot, { recursive: true });
|
|
18084
18409
|
let dirName = base;
|
|
18085
18410
|
let counter6 = 0;
|
|
18086
|
-
while (
|
|
18411
|
+
while (existsSync14(join$1(patchesRoot, dirName))) {
|
|
18087
18412
|
counter6++;
|
|
18088
18413
|
dirName = `${base}-${counter6}`;
|
|
18089
18414
|
}
|
|
@@ -18255,8 +18580,7 @@ ${msg}`);
|
|
|
18255
18580
|
}), 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({
|
|
18256
18581
|
command,
|
|
18257
18582
|
exitCode: result.exitCode,
|
|
18258
|
-
message:
|
|
18259
|
-
${result.stderr}`
|
|
18583
|
+
message: formatExecFailureMessage(command, result, sandbox3.maxOutputTailChars)
|
|
18260
18584
|
})) : Effect_exports.succeed(result)), execOkWithGitTimeout = (sandbox3, command, gitSetupTimeoutMs, options) => execOk2(sandbox3, command, options).pipe(withTimeout(gitSetupTimeoutMs, () => new GitSetupTimeoutError({
|
|
18261
18585
|
message: `Git command timed out after ${gitSetupTimeoutMs}ms: ${command}`,
|
|
18262
18586
|
timeoutMs: gitSetupTimeoutMs,
|
|
@@ -18592,7 +18916,7 @@ ${result.stderr}`
|
|
|
18592
18916
|
yield* Effect_exports.gen(function* () {
|
|
18593
18917
|
for (const relativePath of pathsToCopy) {
|
|
18594
18918
|
const hostPath = join$1(options.hostRepoDir, relativePath);
|
|
18595
|
-
if (!
|
|
18919
|
+
if (!existsSync14(hostPath)) {
|
|
18596
18920
|
continue;
|
|
18597
18921
|
}
|
|
18598
18922
|
const sandboxPath = posix2.join(handle.worktreePath, relativePath);
|
|
@@ -18635,6 +18959,7 @@ ${result.stderr}`
|
|
|
18635
18959
|
message: "copyIn is not supported for this sandbox provider"
|
|
18636
18960
|
}));
|
|
18637
18961
|
}, makeSandboxFromHandle = (handle) => ({
|
|
18962
|
+
maxOutputTailChars: handle.maxOutputTailChars,
|
|
18638
18963
|
exec: (command, options) => Effect_exports.tryPromise({
|
|
18639
18964
|
try: () => handle.exec(command, options),
|
|
18640
18965
|
catch: (e) => new ExecError({
|
|
@@ -18733,7 +19058,7 @@ Worktree removed (no uncommitted changes)`);
|
|
|
18733
19058
|
};
|
|
18734
19059
|
}
|
|
18735
19060
|
const resolvedHostPath = resolveHostPath(m.hostPath);
|
|
18736
|
-
if (!
|
|
19061
|
+
if (!existsSync14(resolvedHostPath)) {
|
|
18737
19062
|
throw new Error(`Mount hostPath does not exist: ${m.hostPath}` + (m.hostPath !== resolvedHostPath ? ` (resolved to ${resolvedHostPath})` : ""));
|
|
18738
19063
|
}
|
|
18739
19064
|
return {
|
|
@@ -18846,7 +19171,7 @@ Worktree removed (no uncommitted changes)`);
|
|
|
18846
19171
|
const selinux = mount.volume !== undefined ? undefined : selinuxLabel || undefined;
|
|
18847
19172
|
const options = [mount.readonly ? "ro" : undefined, selinux].filter((option5) => option5 !== undefined).join(",");
|
|
18848
19173
|
return options ? `${base}:${options}` : base;
|
|
18849
|
-
}, processFileMountParents = (mounts, sandboxHomedir, statFn =
|
|
19174
|
+
}, processFileMountParents = (mounts, sandboxHomedir, statFn = statSync4) => {
|
|
18850
19175
|
const parentDirs = /* @__PURE__ */ new Set;
|
|
18851
19176
|
for (const mount of mounts) {
|
|
18852
19177
|
let isFile2;
|
|
@@ -19847,7 +20172,7 @@ var init_chunk_VOG34SRF = __esm(() => {
|
|
|
19847
20172
|
}
|
|
19848
20173
|
return [];
|
|
19849
20174
|
});
|
|
19850
|
-
|
|
20175
|
+
join13 = /* @__PURE__ */ dual(2, (self, sep2) => fromIterable2(self).join(sep2));
|
|
19851
20176
|
TypeId4 = /* @__PURE__ */ Symbol.for("effect/Chunk");
|
|
19852
20177
|
emptyArray = [];
|
|
19853
20178
|
_equivalence3 = /* @__PURE__ */ getEquivalence3(equals);
|
|
@@ -53504,6 +53829,7 @@ var noSandbox = (options) => ({
|
|
|
53504
53829
|
const maxOutputTailChars = options?.maxOutputTailChars ?? MAX_TAIL_CHARS;
|
|
53505
53830
|
const handle = {
|
|
53506
53831
|
worktreePath,
|
|
53832
|
+
maxOutputTailChars,
|
|
53507
53833
|
exec: (command, opts) => {
|
|
53508
53834
|
const cwd = opts?.cwd ?? worktreePath;
|
|
53509
53835
|
const isWindows = process.platform === "win32";
|
|
@@ -53600,7 +53926,7 @@ var init_chunk_62WN33RK = __esm(() => {
|
|
|
53600
53926
|
// ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/index.js
|
|
53601
53927
|
import { createRequire as createRequire8 } from "module";
|
|
53602
53928
|
import { mkdirSync as mkdirSync6, appendFileSync } from "fs";
|
|
53603
|
-
import path, { join as
|
|
53929
|
+
import path, { join as join15, posix as posix4, dirname as dirname7, relative as relative3 } from "path";
|
|
53604
53930
|
import { styleText as styleText2 } from "util";
|
|
53605
53931
|
import * as clack2 from "@clack/prompts";
|
|
53606
53932
|
import { readdir as readdir2, access as access3, readFile as readFile3, mkdir as mkdir3, writeFile as writeFile3, rm as rm3 } from "fs/promises";
|
|
@@ -53669,7 +53995,7 @@ async function run6(options) {
|
|
|
53669
53995
|
const targetBranch = effectiveBranchType === "merge-to-head" ? currentHostBranch : undefined;
|
|
53670
53996
|
const resolvedLogging = options.logging ?? {
|
|
53671
53997
|
type: "file",
|
|
53672
|
-
path:
|
|
53998
|
+
path: join15(hostRepoDir, ".sandcastle", "logs", buildLogFilename(resolvedBranch, targetBranch, options.name))
|
|
53673
53999
|
};
|
|
53674
54000
|
const displayLayer = resolvedLogging.type === "file" ? (() => {
|
|
53675
54001
|
printFileDisplayStartup({
|
|
@@ -54303,7 +54629,7 @@ ${errorDetail}`
|
|
|
54303
54629
|
}
|
|
54304
54630
|
return vars;
|
|
54305
54631
|
}), resolveEnv = (repoDir) => Effect_exports.gen(function* () {
|
|
54306
|
-
const sandcastleEnv = yield* parseEnvFile2(
|
|
54632
|
+
const sandcastleEnv = yield* parseEnvFile2(join15(repoDir, ".sandcastle", ".env"));
|
|
54307
54633
|
const result = {};
|
|
54308
54634
|
for (const key of Object.keys(sandcastleEnv)) {
|
|
54309
54635
|
const value = sandcastleEnv[key] || process.env[key];
|
|
@@ -54525,11 +54851,11 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54525
54851
|
const normalized = isRoot ? cwd : cwd.replace(/[\\/]+$/, "");
|
|
54526
54852
|
return normalized.replace(/^([A-Za-z]):/, "$1").replace(/[\\/]/g, "-");
|
|
54527
54853
|
}, claudeHostSessionPath = (cwd, id3, projectsDir) => {
|
|
54528
|
-
const base = projectsDir ??
|
|
54529
|
-
return
|
|
54854
|
+
const base = projectsDir ?? join15(process.env.HOME ?? "~", ".claude", "projects");
|
|
54855
|
+
return join15(base, encodeProjectPath(cwd), `${id3}.jsonl`);
|
|
54530
54856
|
}, 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) => {
|
|
54531
|
-
const base = projectsDir ??
|
|
54532
|
-
return
|
|
54857
|
+
const base = projectsDir ?? join15(process.env.HOME ?? "~", ".claude", "projects");
|
|
54858
|
+
return join15(base, encodeProjectPath(cwd), id3, "subagents");
|
|
54533
54859
|
}, listClaudeSubagentSessionsInSandbox = async (cwd, id3, handle, sandboxProjectsDir) => {
|
|
54534
54860
|
const dir = claudeSubagentsDirInSandbox(cwd, id3, sandboxProjectsDir);
|
|
54535
54861
|
const result = await handle.exec(`find ${JSON.stringify(dir)} -type f -name ${JSON.stringify("agent-*.jsonl")} 2>/dev/null`);
|
|
@@ -54541,7 +54867,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54541
54867
|
return stdout.split(`
|
|
54542
54868
|
`).filter((line) => line !== "");
|
|
54543
54869
|
}, findClaudeSessionOnHost = async (id3, projectsDir) => {
|
|
54544
|
-
const root = projectsDir ??
|
|
54870
|
+
const root = projectsDir ?? join15(process.env.HOME ?? "~", ".claude", "projects");
|
|
54545
54871
|
let entries;
|
|
54546
54872
|
try {
|
|
54547
54873
|
entries = await readdir2(root, { withFileTypes: true });
|
|
@@ -54551,7 +54877,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54551
54877
|
for (const entry of entries) {
|
|
54552
54878
|
if (!entry.isDirectory())
|
|
54553
54879
|
continue;
|
|
54554
|
-
const candidate =
|
|
54880
|
+
const candidate = join15(root, entry.name, `${id3}.jsonl`);
|
|
54555
54881
|
if (await pathExists(candidate)) {
|
|
54556
54882
|
return { path: candidate, searchedRoot: root };
|
|
54557
54883
|
}
|
|
@@ -54587,7 +54913,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54587
54913
|
return;
|
|
54588
54914
|
}
|
|
54589
54915
|
for (const entry of entries) {
|
|
54590
|
-
const child =
|
|
54916
|
+
const child = join15(dir, entry.name);
|
|
54591
54917
|
if (entry.isFile() && isCodexSessionFilename(entry.name, id3)) {
|
|
54592
54918
|
return child;
|
|
54593
54919
|
}
|
|
@@ -54601,11 +54927,11 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54601
54927
|
};
|
|
54602
54928
|
return visit(rootDir);
|
|
54603
54929
|
}, findCodexSessionOnHost = async (id3, sessionsDir) => {
|
|
54604
|
-
const root = sessionsDir ??
|
|
54930
|
+
const root = sessionsDir ?? join15(process.env.HOME ?? "~", ".codex", "sessions");
|
|
54605
54931
|
const path2 = await findCodexSessionPath(root, id3);
|
|
54606
54932
|
return { path: path2, searchedRoot: root };
|
|
54607
54933
|
}, locateCodexHostSession = async (id3, sessionsDir) => {
|
|
54608
|
-
const root = sessionsDir ??
|
|
54934
|
+
const root = sessionsDir ?? join15(process.env.HOME ?? "~", ".codex", "sessions");
|
|
54609
54935
|
const path2 = await findCodexSessionPath(root, id3);
|
|
54610
54936
|
if (!path2)
|
|
54611
54937
|
throw new Error(`session ${id3} not found in ${root}`);
|
|
@@ -54681,7 +55007,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54681
55007
|
} catch {}
|
|
54682
55008
|
return [];
|
|
54683
55009
|
}, CURSOR_PRINT_PROMPT_MAX_BYTES, readSandboxFile = async (handle, sandboxPath, tag) => {
|
|
54684
|
-
const tmpPath =
|
|
55010
|
+
const tmpPath = join15(tmpdir4(), `sandcastle-${tag}-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`);
|
|
54685
55011
|
await handle.copyFileOut(sandboxPath, tmpPath);
|
|
54686
55012
|
try {
|
|
54687
55013
|
return await readFile3(tmpPath, "utf-8");
|
|
@@ -54689,7 +55015,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54689
55015
|
await rm3(tmpPath, { force: true }).catch(() => {});
|
|
54690
55016
|
}
|
|
54691
55017
|
}, writeSandboxFile = async (handle, sandboxPath, content, tag) => {
|
|
54692
|
-
const tmpPath =
|
|
55018
|
+
const tmpPath = join15(tmpdir4(), `sandcastle-${tag}-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`);
|
|
54693
55019
|
await writeFile3(tmpPath, content);
|
|
54694
55020
|
try {
|
|
54695
55021
|
await handle.exec(`mkdir -p ${JSON.stringify(posix4.dirname(sandboxPath))}`);
|
|
@@ -54739,7 +55065,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54739
55065
|
sourcePath: sandboxSubagentPath,
|
|
54740
55066
|
fromCwd: sandboxCwd,
|
|
54741
55067
|
toCwd: hostCwd,
|
|
54742
|
-
destPath:
|
|
55068
|
+
destPath: join15(hostSubagentsDir, posix4.basename(sandboxSubagentPath)),
|
|
54743
55069
|
tag: "claude-sub"
|
|
54744
55070
|
});
|
|
54745
55071
|
} catch (err) {
|
|
@@ -54776,8 +55102,8 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
|
|
|
54776
55102
|
const located = await locateCodexSandboxSession(sessionId, handle, sandboxSessionsDir);
|
|
54777
55103
|
const jsonl = await readSandboxFile(handle, located.path, "codex-cap");
|
|
54778
55104
|
const rewritten = transferCodexSession(jsonl, sandboxCwd, hostCwd);
|
|
54779
|
-
const root = hostSessionsDir ??
|
|
54780
|
-
const target =
|
|
55105
|
+
const root = hostSessionsDir ?? join15(process.env.HOME ?? "~", ".codex", "sessions");
|
|
55106
|
+
const target = join15(root, located.relativePath);
|
|
54781
55107
|
await mkdir3(dirname7(target), { recursive: true });
|
|
54782
55108
|
await writeFile3(target, rewritten);
|
|
54783
55109
|
capturedPaths.set(sessionId, target);
|
|
@@ -54938,6 +55264,7 @@ var init_dist = __esm(() => {
|
|
|
54938
55264
|
init_chunk_BIWNFKGV();
|
|
54939
55265
|
init_chunk_62WN33RK();
|
|
54940
55266
|
init_chunk_NGBM7T3E();
|
|
55267
|
+
init_chunk_NGBM7T3E();
|
|
54941
55268
|
createRequire8(import.meta.url);
|
|
54942
55269
|
AgentStreamEmitter = class extends Context_exports.Tag("AgentStreamEmitter")() {
|
|
54943
55270
|
};
|
|
@@ -54983,7 +55310,7 @@ var init_dist = __esm(() => {
|
|
|
54983
55310
|
});
|
|
54984
55311
|
|
|
54985
55312
|
// src/services/settings.ts
|
|
54986
|
-
import { existsSync as
|
|
55313
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
54987
55314
|
import { dirname as dirname8 } from "path";
|
|
54988
55315
|
import * as z3 from "zod";
|
|
54989
55316
|
import { eq as eq10 } from "drizzle-orm";
|
|
@@ -54991,10 +55318,10 @@ function hostDefaults(logicalCpus) {
|
|
|
54991
55318
|
return { ...DEFAULTS, burnConcurrency: resolveDefaultBurnConcurrency(logicalCpus) };
|
|
54992
55319
|
}
|
|
54993
55320
|
function readRawConfig(configFile) {
|
|
54994
|
-
if (!
|
|
55321
|
+
if (!existsSync15(configFile))
|
|
54995
55322
|
return {};
|
|
54996
55323
|
try {
|
|
54997
|
-
const parsed = JSON.parse(
|
|
55324
|
+
const parsed = JSON.parse(readFileSync8(configFile, "utf8"));
|
|
54998
55325
|
if (typeof parsed !== "object" || parsed === null)
|
|
54999
55326
|
return {};
|
|
55000
55327
|
return foldLegacyModelConfig(parsed);
|
|
@@ -55021,6 +55348,7 @@ function projectOverrides(ctx, projectId) {
|
|
|
55021
55348
|
const row = ctx.db.select({
|
|
55022
55349
|
model: projects.model,
|
|
55023
55350
|
sandbox: projects.sandbox,
|
|
55351
|
+
sandboxImage: projects.sandboxImage,
|
|
55024
55352
|
sessionBranch: projects.sessionBranch,
|
|
55025
55353
|
devCommand: projects.devCommand,
|
|
55026
55354
|
setupCommand: projects.setupCommand,
|
|
@@ -55034,6 +55362,7 @@ function projectOverrides(ctx, projectId) {
|
|
|
55034
55362
|
return {
|
|
55035
55363
|
model: row?.model ?? null,
|
|
55036
55364
|
sandbox: row?.sandbox ?? null,
|
|
55365
|
+
sandboxImage: row?.sandboxImage ?? null,
|
|
55037
55366
|
sessionBranch: row?.sessionBranch ?? null,
|
|
55038
55367
|
devCommand: row?.devCommand ?? null,
|
|
55039
55368
|
setupCommand: row?.setupCommand ?? null,
|
|
@@ -55236,6 +55565,7 @@ var init_settings = __esm(() => {
|
|
|
55236
55565
|
key: "sandboxImage",
|
|
55237
55566
|
configKey: "sandboxImage",
|
|
55238
55567
|
envVar: "RUNCASTLE_SANDBOX_IMAGE",
|
|
55568
|
+
projectColumn: "sandboxImage",
|
|
55239
55569
|
restartRequired: false,
|
|
55240
55570
|
valueSchema: z3.string().min(1),
|
|
55241
55571
|
parseEnv: idEnv
|
|
@@ -55363,9 +55693,8 @@ var init_settings = __esm(() => {
|
|
|
55363
55693
|
});
|
|
55364
55694
|
|
|
55365
55695
|
// src/services/setup.ts
|
|
55366
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
55367
|
-
import { dirname as dirname9, join as
|
|
55368
|
-
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
55696
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync9, readdirSync as readdirSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
55697
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
55369
55698
|
async function writeGitIdentity(exec3, identity4) {
|
|
55370
55699
|
const name = identity4.name.trim();
|
|
55371
55700
|
const email = identity4.email.trim();
|
|
@@ -55465,55 +55794,26 @@ async function resolveRuntime(exec3, preferred) {
|
|
|
55465
55794
|
return other;
|
|
55466
55795
|
return preferred;
|
|
55467
55796
|
}
|
|
55468
|
-
function
|
|
55469
|
-
try {
|
|
55470
|
-
const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
|
|
55471
|
-
let dir = dirname9(fileURLToPath6(mainUrl));
|
|
55472
|
-
for (let hops = 0;hops < 6; hops++) {
|
|
55473
|
-
const manifestPath = join15(dir, "package.json");
|
|
55474
|
-
if (existsSync15(manifestPath)) {
|
|
55475
|
-
const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
|
|
55476
|
-
if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
|
|
55477
|
-
return join15(dir, manifest.bin.sandcastle);
|
|
55478
|
-
}
|
|
55479
|
-
}
|
|
55480
|
-
const parent = dirname9(dir);
|
|
55481
|
-
if (parent === dir)
|
|
55482
|
-
break;
|
|
55483
|
-
dir = parent;
|
|
55484
|
-
}
|
|
55485
|
-
return null;
|
|
55486
|
-
} catch {
|
|
55487
|
-
return null;
|
|
55488
|
-
}
|
|
55489
|
-
}
|
|
55490
|
-
function terminalSpec(kind, opts) {
|
|
55797
|
+
function terminalSpec(kind) {
|
|
55491
55798
|
if (kind === "setup-token")
|
|
55492
55799
|
return { cmd: "claude", args: ["setup-token"] };
|
|
55493
55800
|
for (const spec of Object.values(RUNTIME_SPECS)) {
|
|
55494
55801
|
if (kind === LOGIN_TERMINAL_KIND[spec.runtime])
|
|
55495
55802
|
return { cmd: spec.bin, args: spec.loginArgs };
|
|
55496
55803
|
}
|
|
55497
|
-
|
|
55498
|
-
throw new NotFoundError("The bundled sandcastle CLI (@ai-hero/sandcastle) could not be located \u2014 reinstall runcastle.");
|
|
55499
|
-
}
|
|
55500
|
-
return {
|
|
55501
|
-
cmd: "node",
|
|
55502
|
-
args: [opts.sandcastleBin, opts.runtime, "build-image", "--image-name", opts.imageName]
|
|
55503
|
-
};
|
|
55804
|
+
throw new InvalidInputError(`no terminal command for ${kind}`);
|
|
55504
55805
|
}
|
|
55505
55806
|
function refreshSandcastleConfig(templateDir, targetDir) {
|
|
55506
|
-
const dir =
|
|
55807
|
+
const dir = join16(targetDir, ".sandcastle");
|
|
55507
55808
|
mkdirSync8(dir, { recursive: true });
|
|
55508
55809
|
for (const name of readdirSync4(templateDir)) {
|
|
55509
|
-
copyFileSync2(
|
|
55810
|
+
copyFileSync2(join16(templateDir, name), join16(dir, name));
|
|
55510
55811
|
}
|
|
55511
55812
|
return dir;
|
|
55512
55813
|
}
|
|
55513
55814
|
function prepareSandboxBuildContext(templateDir = sandcastleTemplateDir(), target = sandboxBuildDir()) {
|
|
55514
55815
|
mkdirSync8(target, { recursive: true });
|
|
55515
|
-
refreshSandcastleConfig(templateDir, target);
|
|
55516
|
-
return target;
|
|
55816
|
+
return refreshSandcastleConfig(templateDir, target);
|
|
55517
55817
|
}
|
|
55518
55818
|
function createCredentialVerifier(exec3, runtime5 = DEFAULT_RUNTIME) {
|
|
55519
55819
|
const spec = RUNTIME_SPECS[runtime5];
|
|
@@ -55548,7 +55848,7 @@ function createCredentialVerifier(exec3, runtime5 = DEFAULT_RUNTIME) {
|
|
|
55548
55848
|
function fileAfkTokenIo(exec3, runtime5 = DEFAULT_RUNTIME) {
|
|
55549
55849
|
const path2 = envPath();
|
|
55550
55850
|
return {
|
|
55551
|
-
read: () =>
|
|
55851
|
+
read: () => existsSync16(path2) ? readFileSync9(path2, "utf8") : "",
|
|
55552
55852
|
write: (content) => {
|
|
55553
55853
|
mkdirSync8(dirname9(path2), { recursive: true });
|
|
55554
55854
|
writeFileSync6(path2, content, "utf8");
|
|
@@ -55792,13 +56092,13 @@ var init_no_sandbox = __esm(() => {
|
|
|
55792
56092
|
});
|
|
55793
56093
|
|
|
55794
56094
|
// src/workflows/review-ticket.ts
|
|
55795
|
-
import { existsSync as
|
|
55796
|
-
import { delimiter, join as
|
|
56095
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync10, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
56096
|
+
import { delimiter, join as join17 } from "path";
|
|
55797
56097
|
function reviewTemplatePath(ticket) {
|
|
55798
56098
|
return burnerAssetPath(ticket?.passKind === "verification" ? "verify-fixes.md" : "review-ticket.md");
|
|
55799
56099
|
}
|
|
55800
56100
|
function renderReviewPrompt(templateOrTicket, values) {
|
|
55801
|
-
const template = typeof templateOrTicket === "string" ? templateOrTicket :
|
|
56101
|
+
const template = typeof templateOrTicket === "string" ? templateOrTicket : readFileSync10(reviewTemplatePath(templateOrTicket), "utf8");
|
|
55802
56102
|
return renderTemplate(template, values);
|
|
55803
56103
|
}
|
|
55804
56104
|
function findOnPath(bin, env = process.env, platform = process.platform) {
|
|
@@ -55806,8 +56106,8 @@ function findOnPath(bin, env = process.env, platform = process.platform) {
|
|
|
55806
56106
|
const suffixes = platform === "win32" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
55807
56107
|
for (const dir of dirs) {
|
|
55808
56108
|
for (const suffix of suffixes) {
|
|
55809
|
-
const candidate =
|
|
55810
|
-
if (
|
|
56109
|
+
const candidate = join17(dir, `${bin}${suffix}`);
|
|
56110
|
+
if (existsSync17(candidate))
|
|
55811
56111
|
return candidate;
|
|
55812
56112
|
}
|
|
55813
56113
|
}
|
|
@@ -55829,7 +56129,7 @@ function buildDriveAvailability(browserPath, devCommand, inheritedMode) {
|
|
|
55829
56129
|
}
|
|
55830
56130
|
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.";
|
|
55831
56131
|
}
|
|
55832
|
-
function inheritedReviewMode(verifiedTicketId, fileExists2 =
|
|
56132
|
+
function inheritedReviewMode(verifiedTicketId, fileExists2 = existsSync17) {
|
|
55833
56133
|
return verifiedTicketId && fileExists2(reviewWalkthroughPath(verifiedTicketId)) ? "drive" : "gates";
|
|
55834
56134
|
}
|
|
55835
56135
|
function buildDriveInstructions(instructions) {
|
|
@@ -55868,18 +56168,18 @@ function writeReviewArtifacts(ticket, runId, config2) {
|
|
|
55868
56168
|
const dir = reviewDir(ticket.id);
|
|
55869
56169
|
rmSync2(dir, { recursive: true, force: true });
|
|
55870
56170
|
mkdirSync9(dir, { recursive: true });
|
|
55871
|
-
const mcpConfigPath =
|
|
56171
|
+
const mcpConfigPath = join17(dir, "mcp.json");
|
|
55872
56172
|
const mcpConfig = renderRunMcpConfig(runId, config2);
|
|
55873
56173
|
writeFileSync7(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
55874
56174
|
return {
|
|
55875
56175
|
dir,
|
|
55876
56176
|
mcp: { path: mcpConfigPath, config: mcpConfig },
|
|
55877
|
-
digestPath:
|
|
55878
|
-
blockedPath:
|
|
56177
|
+
digestPath: join17(dir, "DIGEST.md"),
|
|
56178
|
+
blockedPath: join17(dir, "BLOCKED.md"),
|
|
55879
56179
|
walkthroughPath: reviewWalkthroughPath(ticket.id)
|
|
55880
56180
|
};
|
|
55881
56181
|
}
|
|
55882
|
-
function shouldStopAfterDigest(iteration, digestPath, fileExists2 =
|
|
56182
|
+
function shouldStopAfterDigest(iteration, digestPath, fileExists2 = existsSync17) {
|
|
55883
56183
|
return iteration >= 2 && fileExists2(digestPath);
|
|
55884
56184
|
}
|
|
55885
56185
|
async function releaseDriveQuietly() {
|
|
@@ -55957,7 +56257,7 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
|
|
|
55957
56257
|
maxIterations: deps.config.burnMaxIterations,
|
|
55958
56258
|
logging: {
|
|
55959
56259
|
type: "file",
|
|
55960
|
-
path:
|
|
56260
|
+
path: join17(logsDir(), `review-${feature.id}-${ticket.seq}.log`),
|
|
55961
56261
|
onAgentStreamEvent: onStreamEvent
|
|
55962
56262
|
}
|
|
55963
56263
|
};
|
|
@@ -56146,6 +56446,7 @@ var dockerExec = (args2) => Effect_exports.async((resume2) => {
|
|
|
56146
56446
|
const unregisterShutdown = registerShutdown(removeContainerSync);
|
|
56147
56447
|
const handle = {
|
|
56148
56448
|
worktreePath,
|
|
56449
|
+
maxOutputTailChars,
|
|
56149
56450
|
exec: (command, opts) => {
|
|
56150
56451
|
const effectiveCommand = opts?.sudo ? `sudo ${command}` : command;
|
|
56151
56452
|
const args2 = ["exec"];
|
|
@@ -56406,6 +56707,7 @@ var podman = (options) => {
|
|
|
56406
56707
|
const unregisterShutdown = registerShutdown(removeContainerSync);
|
|
56407
56708
|
const handle = {
|
|
56408
56709
|
worktreePath,
|
|
56710
|
+
maxOutputTailChars,
|
|
56409
56711
|
exec: (command, opts) => {
|
|
56410
56712
|
const effectiveCommand = opts?.sudo ? `sudo ${command}` : command;
|
|
56411
56713
|
const args2 = ["exec"];
|
|
@@ -56559,13 +56861,47 @@ var init_podman = __esm(() => {
|
|
|
56559
56861
|
});
|
|
56560
56862
|
|
|
56561
56863
|
// src/workflows/ticket-burner.ts
|
|
56562
|
-
import { existsSync as
|
|
56563
|
-
import { dirname as dirname10, join as
|
|
56564
|
-
import { fileURLToPath as
|
|
56864
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync11, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
|
|
56865
|
+
import { dirname as dirname10, join as join18, posix as posixPath, win32 as winPath } from "path";
|
|
56866
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
56565
56867
|
function missingImageRuntimeMessage(runtime5, image) {
|
|
56566
56868
|
const binary = RUNTIME_BINARY[runtime5];
|
|
56567
56869
|
return `${binary} is not installed in image ${image} \u2014 the image predates the burner Dockerfile. Rebuild it from Settings \u2192 Burns (Rebuild image).`;
|
|
56568
56870
|
}
|
|
56871
|
+
function extractCommandNames(command) {
|
|
56872
|
+
const names = [];
|
|
56873
|
+
for (const segment of (command ?? "").split(COMMAND_SEPARATORS)) {
|
|
56874
|
+
const words = segment.replace(/[(){}'"`]/g, " ").split(/\s+/).filter(Boolean);
|
|
56875
|
+
const word = words.find((w) => !ASSIGNMENT_PREFIX.test(w)) ?? "";
|
|
56876
|
+
if (!PLAIN_COMMAND_NAME.test(word) || NON_COMMAND_WORDS.has(word))
|
|
56877
|
+
continue;
|
|
56878
|
+
if (!names.includes(word))
|
|
56879
|
+
names.push(word);
|
|
56880
|
+
}
|
|
56881
|
+
return names;
|
|
56882
|
+
}
|
|
56883
|
+
function preflightCommandNames(input) {
|
|
56884
|
+
return [
|
|
56885
|
+
...new Set([
|
|
56886
|
+
input.agentBinary,
|
|
56887
|
+
...extractCommandNames(input.setupCommand),
|
|
56888
|
+
...extractCommandNames(input.verifyCommands)
|
|
56889
|
+
])
|
|
56890
|
+
].sort();
|
|
56891
|
+
}
|
|
56892
|
+
function buildToolchainProbeArgs(image, names) {
|
|
56893
|
+
const script = `for c in ${names.join(" ")}; do command -v "$c" >/dev/null 2>&1 || echo "$c"; done`;
|
|
56894
|
+
return ["run", "--rm", "--entrypoint", "sh", image, "-c", script];
|
|
56895
|
+
}
|
|
56896
|
+
function parseMissingCommands(stdout, names) {
|
|
56897
|
+
const absent = new Set(stdout.split(`
|
|
56898
|
+
`).map((line) => line.trim()));
|
|
56899
|
+
return names.filter((name) => absent.has(name));
|
|
56900
|
+
}
|
|
56901
|
+
function missingToolchainMessage(names, image) {
|
|
56902
|
+
const one = names.length === 1;
|
|
56903
|
+
return `${names.join(", ")} ${one ? "is" : "are"} not installed in image ${image} \u2014 add ${one ? "it" : "them"} to .runcastle/sandbox/Dockerfile and rebuild.`;
|
|
56904
|
+
}
|
|
56569
56905
|
function burnAuthReady(runtime5, token, loggedIn = codexLoggedIn) {
|
|
56570
56906
|
if (token !== undefined)
|
|
56571
56907
|
return true;
|
|
@@ -56581,6 +56917,9 @@ function verificationDue(tickets2, runTicketIds) {
|
|
|
56581
56917
|
const verifies = [...reviews].reverse().find((ticket) => ticket.status === "done") ?? null;
|
|
56582
56918
|
if (lastReview?.status === "failed")
|
|
56583
56919
|
return { due: false, landed: [], verifies };
|
|
56920
|
+
const failed = inRun.some((ticket) => !isReviewTicket(ticket) && ticket.status === "failed");
|
|
56921
|
+
if (failed)
|
|
56922
|
+
return { due: false, landed: [], verifies };
|
|
56584
56923
|
const cutoff = verifies?.completedAt ?? Number.NEGATIVE_INFINITY;
|
|
56585
56924
|
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));
|
|
56586
56925
|
return { due: landed.length > 0, landed, verifies };
|
|
@@ -56722,10 +57061,10 @@ _(The "${DROPPED_MAP_SECTIONS.join('" and "')}" sections are omitted here: they
|
|
|
56722
57061
|
}
|
|
56723
57062
|
function buildProjectStandards(repoPath) {
|
|
56724
57063
|
const present = [];
|
|
56725
|
-
if (
|
|
57064
|
+
if (existsSync18(join18(repoPath, AGENT_CONVENTIONS_FILE))) {
|
|
56726
57065
|
present.push("`CLAUDE.md` \u2014 this repo's agent-facing conventions, and the highest authority on them");
|
|
56727
57066
|
}
|
|
56728
|
-
if (
|
|
57067
|
+
if (existsSync18(join18(repoPath, CHARTER_FILE))) {
|
|
56729
57068
|
present.push(`\`${CHARTER_FILE}\` \u2014 the project charter: what this codebase is for`);
|
|
56730
57069
|
}
|
|
56731
57070
|
const adrs = listLiveAdrs(repoPath).map((a) => a.relPath);
|
|
@@ -56756,7 +57095,7 @@ function buildDriveNotes(settings) {
|
|
|
56756
57095
|
add5("stop", settings.driveStopCommand);
|
|
56757
57096
|
add5("dev server", settings.devCommand);
|
|
56758
57097
|
add5("db reset", settings.dbResetCommand);
|
|
56759
|
-
const hasDir =
|
|
57098
|
+
const hasDir = existsSync18(join18(settings.repoPath, ".runcastle"));
|
|
56760
57099
|
if (configured.length === 0 && !hasDir) {
|
|
56761
57100
|
return [
|
|
56762
57101
|
"This project has no test-drive machinery configured, so there is nothing here for you to keep in step \u2014 do not invent any.",
|
|
@@ -56976,10 +57315,10 @@ function parseSetupMarker(text3) {
|
|
|
56976
57315
|
return { cold: m[1] === "1", syncMs: Number(m[2]), installMs: Number(m[3]) };
|
|
56977
57316
|
}
|
|
56978
57317
|
function takeSetupMarker(worktreePath) {
|
|
56979
|
-
const file =
|
|
57318
|
+
const file = join18(worktreePath, SETUP_MARKER_FILE);
|
|
56980
57319
|
let text3;
|
|
56981
57320
|
try {
|
|
56982
|
-
text3 =
|
|
57321
|
+
text3 = readFileSync11(file, "utf8");
|
|
56983
57322
|
} catch {
|
|
56984
57323
|
return;
|
|
56985
57324
|
}
|
|
@@ -57052,7 +57391,7 @@ function attachedNoteIds(context5) {
|
|
|
57052
57391
|
return ids3;
|
|
57053
57392
|
}
|
|
57054
57393
|
function attachmentSources(context5) {
|
|
57055
|
-
return attachedNoteIds(context5).map(annotationPath).filter((p) =>
|
|
57394
|
+
return attachedNoteIds(context5).map(annotationPath).filter((p) => existsSync18(p));
|
|
57056
57395
|
}
|
|
57057
57396
|
function buildAttachmentCopyCommands(sources, platform = process.platform) {
|
|
57058
57397
|
if (sources.length === 0)
|
|
@@ -57072,7 +57411,7 @@ function buildAttachmentCopyCommands(sources, platform = process.platform) {
|
|
|
57072
57411
|
];
|
|
57073
57412
|
}
|
|
57074
57413
|
async function clearAttachments(workspacePath, repoPath) {
|
|
57075
|
-
rmSync3(
|
|
57414
|
+
rmSync3(join18(workspacePath, ATTACHMENTS_DIR), { recursive: true, force: true });
|
|
57076
57415
|
await unexcludePath(repoPath, ATTACHMENTS_PREFIX);
|
|
57077
57416
|
}
|
|
57078
57417
|
function buildVerifyNotes(config2) {
|
|
@@ -57094,12 +57433,12 @@ function buildVerifyNotes(config2) {
|
|
|
57094
57433
|
`);
|
|
57095
57434
|
}
|
|
57096
57435
|
function readRepoToolchain(repoPath) {
|
|
57097
|
-
const pkgPath =
|
|
57098
|
-
const hasPackageJson =
|
|
57436
|
+
const pkgPath = join18(repoPath, "package.json");
|
|
57437
|
+
const hasPackageJson = existsSync18(pkgPath);
|
|
57099
57438
|
let packageManagerField;
|
|
57100
57439
|
if (hasPackageJson) {
|
|
57101
57440
|
try {
|
|
57102
|
-
const pkg = JSON.parse(
|
|
57441
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
|
|
57103
57442
|
if (typeof pkg.packageManager === "string")
|
|
57104
57443
|
packageManagerField = pkg.packageManager;
|
|
57105
57444
|
} catch {}
|
|
@@ -57108,10 +57447,10 @@ function readRepoToolchain(repoPath) {
|
|
|
57108
57447
|
hasPackageJson,
|
|
57109
57448
|
...packageManagerField !== undefined ? { packageManagerField } : {},
|
|
57110
57449
|
lockfiles: {
|
|
57111
|
-
bun:
|
|
57112
|
-
pnpm:
|
|
57113
|
-
yarn:
|
|
57114
|
-
npm:
|
|
57450
|
+
bun: existsSync18(join18(repoPath, "bun.lock")) || existsSync18(join18(repoPath, "bun.lockb")),
|
|
57451
|
+
pnpm: existsSync18(join18(repoPath, "pnpm-lock.yaml")),
|
|
57452
|
+
yarn: existsSync18(join18(repoPath, "yarn.lock")),
|
|
57453
|
+
npm: existsSync18(join18(repoPath, "package-lock.json"))
|
|
57115
57454
|
}
|
|
57116
57455
|
};
|
|
57117
57456
|
}
|
|
@@ -57247,19 +57586,36 @@ function isMergeConflictError(err) {
|
|
|
57247
57586
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
57248
57587
|
return /conflict/i.test(msg) || /git branch -D/i.test(msg) || /automatic merge failed/i.test(msg);
|
|
57249
57588
|
}
|
|
57589
|
+
function refusalNamingCredential(refusal) {
|
|
57590
|
+
return new RegExp(`(?:${refusal}[^\\n]*${CREDENTIAL_SUBJECT}|${CREDENTIAL_SUBJECT}[^\\n]*${refusal})`, "i");
|
|
57591
|
+
}
|
|
57592
|
+
function missingCommandRegex(name) {
|
|
57593
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
57594
|
+
return new RegExp(`\\b${escaped}\\b[^\\n]*(?:exited with code 127|command not found|not found|no such file or directory)`, "i");
|
|
57595
|
+
}
|
|
57250
57596
|
function missingAgentBinaryMessage(err, runtime5, image) {
|
|
57251
57597
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
57252
57598
|
const binary = AGENT_BINARY[runtime5];
|
|
57253
|
-
|
|
57254
|
-
if (!missing.test(msg))
|
|
57599
|
+
if (!missingCommandRegex(binary).test(msg))
|
|
57255
57600
|
return;
|
|
57256
57601
|
return `${binary} is not installed in image ${image} \u2014 the image predates the burner Dockerfile. Rebuild it from Settings \u2192 Burns (Rebuild image).`;
|
|
57257
57602
|
}
|
|
57258
|
-
function
|
|
57603
|
+
function missingSetupBinaryMessage(err, setupCommand, image) {
|
|
57604
|
+
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
57605
|
+
const missing = extractCommandNames(setupCommand).find((name) => missingCommandRegex(name).test(msg));
|
|
57606
|
+
return missing ? missingToolchainMessage([missing], image) : undefined;
|
|
57607
|
+
}
|
|
57608
|
+
function classifyTicketRunError(err, runtime5, setupCommand) {
|
|
57259
57609
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
57260
57610
|
const runtimes = runtime5 ? [runtime5] : ["claude-code", "codex"];
|
|
57261
57611
|
const forRuntimes = (table) => runtimes.flatMap((r) => table[r]);
|
|
57262
57612
|
if (runtimes.some((r) => missingAgentBinaryMessage(msg, r, "image")))
|
|
57613
|
+
return "run-fatal";
|
|
57614
|
+
if ([...RUN_FATAL_ERROR_PATTERNS, ...forRuntimes(RUNTIME_RUN_FATAL_ERROR_PATTERNS)].some((p) => p.test(msg)))
|
|
57615
|
+
return "run-fatal";
|
|
57616
|
+
if (missingSetupBinaryMessage(msg, setupCommand, "image"))
|
|
57617
|
+
return "fatal";
|
|
57618
|
+
if (MISSING_COMMAND_PATTERNS.some((p) => p.test(msg)))
|
|
57263
57619
|
return "fatal";
|
|
57264
57620
|
if ([...FATAL_ERROR_PATTERNS, ...forRuntimes(RUNTIME_FATAL_ERROR_PATTERNS)].some((p) => p.test(msg)))
|
|
57265
57621
|
return "fatal";
|
|
@@ -57301,11 +57657,20 @@ function delayUnlessAborted(ms, signal) {
|
|
|
57301
57657
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
57302
57658
|
});
|
|
57303
57659
|
}
|
|
57304
|
-
|
|
57660
|
+
function stopTicketRun(ticketId) {
|
|
57661
|
+
return abortTicketRun(ticketId, new Error("ticket stopped by user"));
|
|
57662
|
+
}
|
|
57663
|
+
function haltTicketRun(ticketId, headline) {
|
|
57664
|
+
return abortTicketRun(ticketId, new RunHaltedAbort(headline));
|
|
57665
|
+
}
|
|
57666
|
+
function ticketStopReason(reason) {
|
|
57667
|
+
return reason instanceof RunHaltedAbort ? `stopped: run halted (${reason.headline})` : "stopped by user";
|
|
57668
|
+
}
|
|
57669
|
+
async function abortTicketRun(ticketId, reason) {
|
|
57305
57670
|
const controller = activeTicketAborts.get(ticketId);
|
|
57306
57671
|
if (!controller)
|
|
57307
57672
|
return { stopped: false, confirmed: true };
|
|
57308
|
-
controller.abort(
|
|
57673
|
+
controller.abort(reason);
|
|
57309
57674
|
const { confirmed } = await killRegistry().killAndWait(ticketId);
|
|
57310
57675
|
return { stopped: true, confirmed };
|
|
57311
57676
|
}
|
|
@@ -57369,11 +57734,7 @@ function errorHeadline(s) {
|
|
|
57369
57734
|
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
57370
57735
|
return causes.at(-1) ?? lines[0] ?? "";
|
|
57371
57736
|
}
|
|
57372
|
-
function
|
|
57373
|
-
const list = [...seqs].sort((a, b) => a - b).join(", ");
|
|
57374
|
-
return `> Reviewed with failed implementation ticket(s): ${list}.`;
|
|
57375
|
-
}
|
|
57376
|
-
async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
57737
|
+
async function burnTickets(ctx, tickets2, execute, concurrency = 1, ticketRuntime) {
|
|
57377
57738
|
const width = Math.max(1, Math.floor(concurrency));
|
|
57378
57739
|
const scheduled = [...tickets2];
|
|
57379
57740
|
const bySeq = indexBySeq(scheduled);
|
|
@@ -57381,10 +57742,23 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
57381
57742
|
const pending3 = new Set(scheduled.filter((t) => t.status === "pending").map((t) => t.seq));
|
|
57382
57743
|
const inFlight = new Map;
|
|
57383
57744
|
const digests = [];
|
|
57745
|
+
const deferred = [];
|
|
57746
|
+
const collapsed = [];
|
|
57384
57747
|
let admitted = 0;
|
|
57385
57748
|
let verificationChecked = false;
|
|
57386
|
-
|
|
57749
|
+
let halted;
|
|
57750
|
+
const satisfied = (s) => s === "done" || s === "cancelled";
|
|
57387
57751
|
const implementationsSettled = () => scheduled.every((t) => isReviewTicket(t) || !pending3.has(t.seq) && !inFlight.has(t.seq));
|
|
57752
|
+
const failedImplementations = () => scheduled.filter((t) => !isReviewTicket(t) && status3.get(t.seq) === "failed").map((t) => t.seq);
|
|
57753
|
+
const reviewGate = () => {
|
|
57754
|
+
if (!implementationsSettled())
|
|
57755
|
+
return "wait";
|
|
57756
|
+
if (failedImplementations().length > 0)
|
|
57757
|
+
return "defer";
|
|
57758
|
+
const implementations = scheduled.filter((t) => !isReviewTicket(t));
|
|
57759
|
+
const landed = implementations.some((t) => status3.get(t.seq) === "done");
|
|
57760
|
+
return implementations.length > 0 && !landed ? "collapsed" : "ready";
|
|
57761
|
+
};
|
|
57388
57762
|
const admitNewTickets = () => {
|
|
57389
57763
|
const added = [];
|
|
57390
57764
|
for (const t of ctx.listTickets?.() ?? []) {
|
|
@@ -57459,20 +57833,18 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
57459
57833
|
for (const b of t.blockedBy) {
|
|
57460
57834
|
const present = bySeq.has(b);
|
|
57461
57835
|
const bs = present ? status3.get(b) : undefined;
|
|
57462
|
-
if (!present
|
|
57836
|
+
if (!present)
|
|
57837
|
+
return { blockedBy: b, present };
|
|
57838
|
+
if (bs === "failed" && !isReviewTicket(t))
|
|
57463
57839
|
return { blockedBy: b, present };
|
|
57464
57840
|
}
|
|
57465
|
-
if (isReviewTicket(t)
|
|
57466
|
-
return
|
|
57467
|
-
return t.blockedBy.every((b) => satisfied(
|
|
57841
|
+
if (isReviewTicket(t))
|
|
57842
|
+
return reviewGate();
|
|
57843
|
+
return t.blockedBy.every((b) => satisfied(status3.get(b))) ? "ready" : "wait";
|
|
57468
57844
|
};
|
|
57469
57845
|
const harvestDigest2 = (t, digest) => {
|
|
57470
|
-
|
|
57471
|
-
|
|
57472
|
-
|
|
57473
|
-
`);
|
|
57474
|
-
if (body)
|
|
57475
|
-
digests.push({ seq: t.seq, title: t.title, digest: body });
|
|
57846
|
+
if (digest)
|
|
57847
|
+
digests.push({ seq: t.seq, title: t.title, digest });
|
|
57476
57848
|
};
|
|
57477
57849
|
const failTicket = (seq2, error, extra, digest) => {
|
|
57478
57850
|
const t = bySeq.get(seq2);
|
|
@@ -57485,10 +57857,34 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
57485
57857
|
if (extra)
|
|
57486
57858
|
ctx.emitEvent({ ...extra, ticketId: t.id });
|
|
57487
57859
|
};
|
|
57860
|
+
const haltRun = async (t, outcome) => {
|
|
57861
|
+
const halt = {
|
|
57862
|
+
runtime: outcome.runtime,
|
|
57863
|
+
ticketSeq: t.seq,
|
|
57864
|
+
headline: errorHeadline(outcome.error)
|
|
57865
|
+
};
|
|
57866
|
+
halted = halt;
|
|
57867
|
+
ctx.emitEvent({
|
|
57868
|
+
type: "run.halted",
|
|
57869
|
+
message: `run halted: ${halt.headline}`,
|
|
57870
|
+
ticketId: t.id,
|
|
57871
|
+
data: { runtime: halt.runtime, ticketSeq: halt.ticketSeq }
|
|
57872
|
+
});
|
|
57873
|
+
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);
|
|
57874
|
+
await Promise.all(doomed.map((x) => haltTicketRun(x.id, halt.headline)));
|
|
57875
|
+
};
|
|
57876
|
+
const refreshedForLaunch = (t) => {
|
|
57877
|
+
const fresh3 = ctx.listTickets?.().find((row) => row.id === t.id);
|
|
57878
|
+
if (!fresh3)
|
|
57879
|
+
return t;
|
|
57880
|
+
bySeq.set(t.seq, fresh3);
|
|
57881
|
+
return fresh3;
|
|
57882
|
+
};
|
|
57488
57883
|
const runOne = async (seq2) => {
|
|
57489
|
-
const
|
|
57490
|
-
if (!
|
|
57884
|
+
const snapshot = bySeq.get(seq2);
|
|
57885
|
+
if (!snapshot)
|
|
57491
57886
|
return;
|
|
57887
|
+
const t = refreshedForLaunch(snapshot);
|
|
57492
57888
|
status3.set(seq2, "burning");
|
|
57493
57889
|
ctx.updateTicket(t.id, { status: "burning" });
|
|
57494
57890
|
mirrorFinding(t, "fixing");
|
|
@@ -57531,35 +57927,67 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
57531
57927
|
ticketId: t.id,
|
|
57532
57928
|
data: { error: outcome.error }
|
|
57533
57929
|
});
|
|
57930
|
+
if (outcome.runFatal && !halted) {
|
|
57931
|
+
await haltRun(t, { error: outcome.error, runtime: outcome.runFatal.runtime });
|
|
57932
|
+
}
|
|
57534
57933
|
}
|
|
57535
57934
|
if (isReviewTicket(t))
|
|
57536
57935
|
admitNewTickets();
|
|
57537
57936
|
};
|
|
57538
57937
|
while (pending3.size > 0 || inFlight.size > 0 || !verificationChecked) {
|
|
57539
57938
|
ctx.signal.throwIfAborted();
|
|
57540
|
-
if (
|
|
57939
|
+
if (halted && inFlight.size === 0)
|
|
57940
|
+
break;
|
|
57941
|
+
if (!halted && pending3.size === 0 && inFlight.size === 0) {
|
|
57541
57942
|
appendVerificationIfDue();
|
|
57542
57943
|
if (pending3.size === 0)
|
|
57543
57944
|
break;
|
|
57544
57945
|
}
|
|
57545
|
-
for (const seq2 of [...pending3]) {
|
|
57946
|
+
for (const seq2 of halted ? [] : [...pending3]) {
|
|
57546
57947
|
const st = readyState(seq2);
|
|
57547
|
-
if (
|
|
57548
|
-
|
|
57549
|
-
|
|
57550
|
-
|
|
57551
|
-
|
|
57948
|
+
if (st === "ready" || st === "wait")
|
|
57949
|
+
continue;
|
|
57950
|
+
pending3.delete(seq2);
|
|
57951
|
+
const t = bySeq.get(seq2);
|
|
57952
|
+
if (st === "defer") {
|
|
57953
|
+
deferred.push(seq2);
|
|
57552
57954
|
if (t) {
|
|
57955
|
+
const failed = failedImplementations();
|
|
57553
57956
|
ctx.emitEvent({
|
|
57554
|
-
type: "ticket.
|
|
57555
|
-
message: `ticket ${t.seq} ${
|
|
57957
|
+
type: "ticket.deferred",
|
|
57958
|
+
message: `ticket ${t.seq} deferred to the next burn: implementation ticket(s) ${failed.join(", ")} failed \u2014 retry or cancel them, then burn again`,
|
|
57556
57959
|
ticketId: t.id,
|
|
57557
|
-
data: {
|
|
57960
|
+
data: { failed }
|
|
57558
57961
|
});
|
|
57559
57962
|
}
|
|
57963
|
+
continue;
|
|
57964
|
+
}
|
|
57965
|
+
if (st === "collapsed") {
|
|
57966
|
+
collapsed.push(seq2);
|
|
57967
|
+
status3.set(seq2, "cancelled");
|
|
57968
|
+
if (t) {
|
|
57969
|
+
ctx.updateTicket(t.id, { status: "cancelled", error: NOTHING_LANDED });
|
|
57970
|
+
ctx.emitEvent({
|
|
57971
|
+
type: "ticket.cancelled",
|
|
57972
|
+
message: `ticket ${t.seq} cancelled: ${NOTHING_LANDED}`,
|
|
57973
|
+
ticketId: t.id,
|
|
57974
|
+
data: { reason: NOTHING_LANDED }
|
|
57975
|
+
});
|
|
57976
|
+
}
|
|
57977
|
+
continue;
|
|
57978
|
+
}
|
|
57979
|
+
const reason = st.present ? `blocked by failed ticket ${st.blockedBy}` : `blocked by missing ticket ${st.blockedBy}`;
|
|
57980
|
+
failTicket(seq2, reason);
|
|
57981
|
+
if (t) {
|
|
57982
|
+
ctx.emitEvent({
|
|
57983
|
+
type: "ticket.blocked",
|
|
57984
|
+
message: `ticket ${t.seq} ${reason}`,
|
|
57985
|
+
ticketId: t.id,
|
|
57986
|
+
data: { reason, blockedBy: st.blockedBy }
|
|
57987
|
+
});
|
|
57560
57988
|
}
|
|
57561
57989
|
}
|
|
57562
|
-
while (inFlight.size < width) {
|
|
57990
|
+
while (!halted && inFlight.size < width) {
|
|
57563
57991
|
const readySeq = [...pending3].find((seq2) => readyState(seq2) === "ready");
|
|
57564
57992
|
if (readySeq === undefined)
|
|
57565
57993
|
break;
|
|
@@ -57595,7 +58023,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
57595
58023
|
for (const s of status3.values())
|
|
57596
58024
|
if (s === "done")
|
|
57597
58025
|
done10 += 1;
|
|
57598
|
-
return { done: done10, admitted, digests };
|
|
58026
|
+
return { done: done10, admitted, digests, deferred, collapsed, ...halted ? { halted } : {} };
|
|
57599
58027
|
}
|
|
57600
58028
|
function composeRunDigest(entries) {
|
|
57601
58029
|
if (entries.length === 0)
|
|
@@ -57628,18 +58056,18 @@ async function burnRun(ctx, deps) {
|
|
|
57628
58056
|
return { status: "failed", summary: "burn aborted: auth token missing" };
|
|
57629
58057
|
}
|
|
57630
58058
|
if (deps.config.sandbox !== "noSandbox" && deps.exec) {
|
|
57631
|
-
const image = resolveSandboxImage(deps.config);
|
|
58059
|
+
const image = resolveSandboxImage(deps.config, ctx.project);
|
|
57632
58060
|
const binary = RUNTIME_BINARY[deps.runtime];
|
|
57633
|
-
const
|
|
57634
|
-
|
|
57635
|
-
|
|
57636
|
-
|
|
57637
|
-
|
|
57638
|
-
|
|
57639
|
-
|
|
57640
|
-
]
|
|
57641
|
-
if (
|
|
57642
|
-
const message = missingImageRuntimeMessage(deps.runtime, image);
|
|
58061
|
+
const prepared = resolvePreparedSettings(deps.config, ctx.project);
|
|
58062
|
+
const names = preflightCommandNames({
|
|
58063
|
+
agentBinary: binary,
|
|
58064
|
+
setupCommand: resolveSetupCommand(readRepoToolchain(ctx.project.repoPath), prepared.setupCommand),
|
|
58065
|
+
verifyCommands: prepared.verifyCommands
|
|
58066
|
+
});
|
|
58067
|
+
const probe = await deps.exec(deps.config.sandbox, buildToolchainProbeArgs(image, names));
|
|
58068
|
+
const missing = probe.ok && probe.code === 0 ? parseMissingCommands(probe.stdout, names) : [binary];
|
|
58069
|
+
if (missing.length > 0) {
|
|
58070
|
+
const message = missing.includes(binary) ? missingImageRuntimeMessage(deps.runtime, image) : missingToolchainMessage(missing, image);
|
|
57643
58071
|
ctx.emitEvent({ type: IMAGE_RUNTIME_MISSING_EVENT, message });
|
|
57644
58072
|
return { status: "failed", summary: message };
|
|
57645
58073
|
}
|
|
@@ -57654,12 +58082,22 @@ async function burnRun(ctx, deps) {
|
|
|
57654
58082
|
});
|
|
57655
58083
|
return { status: "failed", summary: `dependency cycle: ${path2}` };
|
|
57656
58084
|
}
|
|
57657
|
-
const { done: done10, admitted, digests } = await burnTickets(ctx, tickets2, gateTicketAuth(deps), deps.concurrency);
|
|
57658
|
-
const total = burnable.length + admitted;
|
|
57659
|
-
const
|
|
57660
|
-
|
|
58085
|
+
const { done: done10, admitted, digests, halted, deferred, collapsed } = await burnTickets(ctx, tickets2, gateTicketAuth(deps), deps.concurrency, deps.ticketRuntime);
|
|
58086
|
+
const total = burnable.length + admitted - collapsed.length;
|
|
58087
|
+
const cancelledCount = cancelled + collapsed.length;
|
|
58088
|
+
const counts = cancelledCount > 0 ? `${done10}/${total} tickets done (${cancelledCount} cancelled)` : `${done10}/${total} tickets done`;
|
|
58089
|
+
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;
|
|
58090
|
+
ctx.emitEvent({
|
|
58091
|
+
type: "burn.summary",
|
|
58092
|
+
message: summary,
|
|
58093
|
+
data: { done: done10, total, cancelled: cancelledCount }
|
|
58094
|
+
});
|
|
57661
58095
|
const digest = composeRunDigest(digests);
|
|
57662
|
-
return {
|
|
58096
|
+
return {
|
|
58097
|
+
status: !halted && done10 === total ? "succeeded" : "failed",
|
|
58098
|
+
summary,
|
|
58099
|
+
...digest ? { digest } : {}
|
|
58100
|
+
};
|
|
57663
58101
|
}
|
|
57664
58102
|
function burnerTemplatePath() {
|
|
57665
58103
|
return burnerAssetPath("implement-ticket.md");
|
|
@@ -57668,8 +58106,8 @@ function resolverTemplatePath() {
|
|
|
57668
58106
|
return burnerAssetPath("resolve-conflict.md");
|
|
57669
58107
|
}
|
|
57670
58108
|
function burnerAssetPath(file) {
|
|
57671
|
-
const here = dirname10(
|
|
57672
|
-
return
|
|
58109
|
+
const here = dirname10(fileURLToPath6(import.meta.url));
|
|
58110
|
+
return join18(resolveSkillsRoot(here), "burner", file);
|
|
57673
58111
|
}
|
|
57674
58112
|
function listFeatureDocs(docsDir, prefix = "") {
|
|
57675
58113
|
const out = [];
|
|
@@ -57678,7 +58116,7 @@ function listFeatureDocs(docsDir, prefix = "") {
|
|
|
57678
58116
|
if (e.isFile() && e.name.endsWith(".md"))
|
|
57679
58117
|
out.push(rel);
|
|
57680
58118
|
else if (e.isDirectory() && !prefix)
|
|
57681
|
-
out.push(...listFeatureDocs(
|
|
58119
|
+
out.push(...listFeatureDocs(join18(docsDir, e.name), rel));
|
|
57682
58120
|
}
|
|
57683
58121
|
return out;
|
|
57684
58122
|
}
|
|
@@ -57692,11 +58130,11 @@ function readDocsDigest(projectId, slug) {
|
|
|
57692
58130
|
...missing ? { missing } : {}
|
|
57693
58131
|
});
|
|
57694
58132
|
const worktree = worktreeDir(projectId, slug);
|
|
57695
|
-
if (!
|
|
58133
|
+
if (!existsSync18(worktree)) {
|
|
57696
58134
|
return done10("_No talk worktree on disk \u2014 docs digest skipped._", "no-worktree");
|
|
57697
58135
|
}
|
|
57698
|
-
const docsDir =
|
|
57699
|
-
if (!
|
|
58136
|
+
const docsDir = join18(worktree, ...docsRel.split("/"));
|
|
58137
|
+
if (!existsSync18(docsDir)) {
|
|
57700
58138
|
return done10("_No docs/features dir in the talk worktree \u2014 docs digest skipped._", "no-docs-dir");
|
|
57701
58139
|
}
|
|
57702
58140
|
const names = listFeatureDocs(docsDir).sort((a, b) => agentDigestDocOrder(a) - agentDigestDocOrder(b) || a.localeCompare(b));
|
|
@@ -57712,7 +58150,7 @@ function readDocsDigest(projectId, slug) {
|
|
|
57712
58150
|
}
|
|
57713
58151
|
let content;
|
|
57714
58152
|
try {
|
|
57715
|
-
content =
|
|
58153
|
+
content = readFileSync11(join18(docsDir, ...name.split("/")), "utf8");
|
|
57716
58154
|
} catch {
|
|
57717
58155
|
continue;
|
|
57718
58156
|
}
|
|
@@ -57758,10 +58196,10 @@ function readAgentFile(dirs, file) {
|
|
|
57758
58196
|
for (const dir of dirs) {
|
|
57759
58197
|
if (!dir)
|
|
57760
58198
|
continue;
|
|
57761
|
-
const p =
|
|
57762
|
-
if (
|
|
58199
|
+
const p = join18(dir, file);
|
|
58200
|
+
if (existsSync18(p)) {
|
|
57763
58201
|
try {
|
|
57764
|
-
return
|
|
58202
|
+
return readFileSync11(p, "utf8");
|
|
57765
58203
|
} catch {}
|
|
57766
58204
|
}
|
|
57767
58205
|
}
|
|
@@ -57829,8 +58267,8 @@ function buildAgentExtraArgs(runtime5, options) {
|
|
|
57829
58267
|
function quoteArg(value) {
|
|
57830
58268
|
return `"${value}"`;
|
|
57831
58269
|
}
|
|
57832
|
-
function selectSandbox(config2, mounts = [], env = {}, kill = {}) {
|
|
57833
|
-
const imageOpts = buildSandboxOptions(config2, mounts, env, kill.containerName);
|
|
58270
|
+
function selectSandbox(config2, project3, mounts = [], env = {}, kill = {}) {
|
|
58271
|
+
const imageOpts = buildSandboxOptions(config2, project3, mounts, env, kill.containerName);
|
|
57834
58272
|
switch (config2.sandbox) {
|
|
57835
58273
|
case "docker":
|
|
57836
58274
|
return docker(imageOpts);
|
|
@@ -57842,9 +58280,9 @@ function selectSandbox(config2, mounts = [], env = {}, kill = {}) {
|
|
|
57842
58280
|
throw new Error(`no sandbox provider for sandbox "${config2.sandbox}" \u2014 refusing to run the agent unsandboxed`);
|
|
57843
58281
|
}
|
|
57844
58282
|
}
|
|
57845
|
-
function buildSandboxOptions(config2, mounts = [], env = {}, containerName) {
|
|
58283
|
+
function buildSandboxOptions(config2, project3, mounts = [], env = {}, containerName) {
|
|
57846
58284
|
return {
|
|
57847
|
-
imageName: resolveSandboxImage(config2),
|
|
58285
|
+
imageName: resolveSandboxImage(config2, project3),
|
|
57848
58286
|
...mounts.length > 0 ? { mounts } : {},
|
|
57849
58287
|
...config2.burnCpus !== undefined ? { cpus: config2.burnCpus } : {},
|
|
57850
58288
|
...Object.keys(env).length > 0 ? { env } : {},
|
|
@@ -57905,7 +58343,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
|
|
|
57905
58343
|
const guardInstalled = config2.burnGuard && config2.sandbox !== "noSandbox";
|
|
57906
58344
|
const prepared = resolvePreparedSettings(config2, project3);
|
|
57907
58345
|
const verifyNotes = buildVerifyNotes(prepared);
|
|
57908
|
-
const basePrompt = renderTicketPrompt(
|
|
58346
|
+
const basePrompt = renderTicketPrompt(readFileSync11(burnerTemplatePath(), "utf8"), {
|
|
57909
58347
|
WORKSPACE_NOTES: workspaceNotes,
|
|
57910
58348
|
PROJECT_STANDARDS: blocks.projectStandards,
|
|
57911
58349
|
FEATURE_BRIEF: featureBrief,
|
|
@@ -57933,10 +58371,10 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
|
|
|
57933
58371
|
const guardInstall = guardInstalled ? buildGuardInstallCommand(model.runtime) : undefined;
|
|
57934
58372
|
const withPrelude = (setup) => chainSetupCommands(codexAuthMount ? buildCodexAuthCopyCommand() : undefined, guardInstall, setup);
|
|
57935
58373
|
const agentOptions = guardInstalled ? { bypassHookTrust: true } : {};
|
|
57936
|
-
const slotStamp = buildSlotStamp(resolveSandboxImage(config2), toolchain.packageManagerField);
|
|
58374
|
+
const slotStamp = buildSlotStamp(resolveSandboxImage(config2, project3), toolchain.packageManagerField);
|
|
57937
58375
|
const setupHookFor = (branch) => withPrelude(slot !== undefined ? buildSlotSetupCommand(slot, branch, setupCommand, pm, slotStamp) : workspaceMode === "isolated" ? buildIsolatedSetupCommand(branch, setupCommand, pm) : setupCommand);
|
|
57938
58376
|
mkdirSync10(logsDir(), { recursive: true });
|
|
57939
|
-
const logFilePath =
|
|
58377
|
+
const logFilePath = join18(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
|
|
57940
58378
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
57941
58379
|
beginTranscript(ticket.id);
|
|
57942
58380
|
const timer = createToolTimer();
|
|
@@ -58008,6 +58446,17 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
|
|
|
58008
58446
|
const preserveChain = (branch) => {
|
|
58009
58447
|
ctx.updateTicket(ticket.id, { attemptBranch: branch });
|
|
58010
58448
|
};
|
|
58449
|
+
const stoppedOutcome = (salvaged) => {
|
|
58450
|
+
const reason = ticketStopReason(ticketAbort.signal.reason);
|
|
58451
|
+
return {
|
|
58452
|
+
status: "failed",
|
|
58453
|
+
error: `${reason}${salvaged > 0 ? ` \u2014 ${salvaged} commit(s) preserved; retry to continue from them` : ""}`,
|
|
58454
|
+
event: {
|
|
58455
|
+
type: "ticket.stopped",
|
|
58456
|
+
message: `ticket ${ticket.seq} ${reason}${salvaged > 0 ? ` \u2014 ${salvaged} commit(s) preserved for retry` : ""}`
|
|
58457
|
+
}
|
|
58458
|
+
};
|
|
58459
|
+
};
|
|
58011
58460
|
const discards = new Map;
|
|
58012
58461
|
const discardWorktree = (branch) => {
|
|
58013
58462
|
const pending3 = discards.get(branch) ?? cleanupBurnWorktree(project3.repoPath, branch);
|
|
@@ -58018,7 +58467,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
|
|
|
58018
58467
|
const resolveBranch = ticketBranchName(feature.slug, ticket.seq, newId("r").slice(2, 10));
|
|
58019
58468
|
const containerName = burnContainerName(ctx.runId, ticket.seq, "resolve");
|
|
58020
58469
|
const otherSide = await commitSummaries(project3.repoPath, input.branch, feature.branch);
|
|
58021
|
-
const prompt = renderTemplate(
|
|
58470
|
+
const prompt = renderTemplate(readFileSync11(resolverTemplatePath(), "utf8"), {
|
|
58022
58471
|
TICKET_JSON: ticketJson,
|
|
58023
58472
|
FEATURE_BRIEF: featureBrief,
|
|
58024
58473
|
DOCS_DIGEST: docsDigest,
|
|
@@ -58047,7 +58496,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land, ensureIsolat
|
|
|
58047
58496
|
makeKillable(containerName);
|
|
58048
58497
|
result = await run6({
|
|
58049
58498
|
agent: buildBurnAgent(config2, token, model, agentOptions),
|
|
58050
|
-
sandbox: selectSandbox(config2, mounts, sandboxEnv, killHandles(containerName)),
|
|
58499
|
+
sandbox: selectSandbox(config2, project3, mounts, sandboxEnv, killHandles(containerName)),
|
|
58051
58500
|
cwd: project3.repoPath,
|
|
58052
58501
|
prompt,
|
|
58053
58502
|
branchStrategy: { type: "branch", branch: resolveBranch, baseBranch: input.branch },
|
|
@@ -58180,7 +58629,7 @@ ${blocked3.trim()}`
|
|
|
58180
58629
|
}
|
|
58181
58630
|
const runOptions = {
|
|
58182
58631
|
agent: buildBurnAgent(config2, token, model, agentOptions),
|
|
58183
|
-
sandbox: selectSandbox(config2, mounts, sandboxEnv, killHandles(containerName)),
|
|
58632
|
+
sandbox: selectSandbox(config2, project3, mounts, sandboxEnv, killHandles(containerName)),
|
|
58184
58633
|
cwd: project3.repoPath,
|
|
58185
58634
|
prompt: retryNotes ? `${basePrompt}
|
|
58186
58635
|
|
|
@@ -58216,21 +58665,15 @@ ${retryNotes}` : basePrompt,
|
|
|
58216
58665
|
if (ctx.signal.aborted)
|
|
58217
58666
|
throw err;
|
|
58218
58667
|
const msg = err instanceof Error ? err.message : String(err);
|
|
58219
|
-
const
|
|
58668
|
+
const sandboxImage = resolveSandboxImage(config2, project3);
|
|
58669
|
+
const missingBinary = missingAgentBinaryMessage(err, model.runtime, sandboxImage) ?? missingSetupBinaryMessage(err, setupCommand, sandboxImage);
|
|
58220
58670
|
const salvaged = await branchCommitsAhead(project3.repoPath, feature.branch, tempBranch);
|
|
58221
58671
|
if (salvaged.length > 0)
|
|
58222
58672
|
baseBranch = tempBranch;
|
|
58223
58673
|
if (ticketAbort.signal.aborted) {
|
|
58224
58674
|
if (salvaged.length > 0)
|
|
58225
58675
|
preserveChain(tempBranch);
|
|
58226
|
-
return
|
|
58227
|
-
status: "failed",
|
|
58228
|
-
error: `stopped by user${salvaged.length > 0 ? ` \u2014 ${salvaged.length} commit(s) preserved; retry to continue from them` : ""}`,
|
|
58229
|
-
event: {
|
|
58230
|
-
type: "ticket.stopped",
|
|
58231
|
-
message: `ticket ${ticket.seq} stopped by user${salvaged.length > 0 ? ` \u2014 ${salvaged.length} commit(s) preserved for retry` : ""}`
|
|
58232
|
-
}
|
|
58233
|
-
};
|
|
58676
|
+
return stoppedOutcome(salvaged.length);
|
|
58234
58677
|
}
|
|
58235
58678
|
if (isWorktreeTeardownError(err)) {
|
|
58236
58679
|
const removed = await discardWorktree(tempBranch);
|
|
@@ -58261,9 +58704,10 @@ ${retryNotes}` : basePrompt,
|
|
|
58261
58704
|
if (missingBinary) {
|
|
58262
58705
|
if (salvaged.length > 0)
|
|
58263
58706
|
preserveChain(tempBranch);
|
|
58264
|
-
return { status: "failed", error: missingBinary };
|
|
58707
|
+
return { status: "failed", error: missingBinary, runFatal: { runtime: model.runtime } };
|
|
58265
58708
|
}
|
|
58266
|
-
|
|
58709
|
+
const verdict = classifyTicketRunError(err, model.runtime, setupCommand);
|
|
58710
|
+
if (verdict === "retryable" && attempt < maxAttempts) {
|
|
58267
58711
|
const headline = errorHeadline(msg);
|
|
58268
58712
|
retryNotes = buildRetryNotes({ error: headline, commitCount: salvaged.length });
|
|
58269
58713
|
ctx.emitEvent({
|
|
@@ -58283,20 +58727,17 @@ ${retryNotes}` : basePrompt,
|
|
|
58283
58727
|
if (ticketAbort.signal.aborted) {
|
|
58284
58728
|
if (salvaged.length > 0)
|
|
58285
58729
|
preserveChain(tempBranch);
|
|
58286
|
-
return
|
|
58287
|
-
status: "failed",
|
|
58288
|
-
error: `stopped by user${salvaged.length > 0 ? ` \u2014 ${salvaged.length} commit(s) preserved; retry to continue from them` : ""}`,
|
|
58289
|
-
event: {
|
|
58290
|
-
type: "ticket.stopped",
|
|
58291
|
-
message: `ticket ${ticket.seq} stopped by user${salvaged.length > 0 ? ` \u2014 ${salvaged.length} commit(s) preserved for retry` : ""}`
|
|
58292
|
-
}
|
|
58293
|
-
};
|
|
58730
|
+
return stoppedOutcome(salvaged.length);
|
|
58294
58731
|
}
|
|
58295
58732
|
continue;
|
|
58296
58733
|
}
|
|
58297
58734
|
if (salvaged.length > 0)
|
|
58298
58735
|
preserveChain(tempBranch);
|
|
58299
|
-
return {
|
|
58736
|
+
return {
|
|
58737
|
+
status: "failed",
|
|
58738
|
+
error: msg,
|
|
58739
|
+
...verdict === "run-fatal" ? { runFatal: { runtime: model.runtime } } : {}
|
|
58740
|
+
};
|
|
58300
58741
|
}
|
|
58301
58742
|
}
|
|
58302
58743
|
if (!result)
|
|
@@ -58340,7 +58781,7 @@ function resolveBurnDeps(ctx) {
|
|
|
58340
58781
|
const exec3 = createSystemExec({ cwd: ctx.project.repoPath });
|
|
58341
58782
|
const imageProbeCache = new Map;
|
|
58342
58783
|
const cachedExec = (command, args2) => {
|
|
58343
|
-
const key =
|
|
58784
|
+
const key = [resolveSandboxImage(config2, ctx.project), model.runtime, command, ...args2].join("\x00");
|
|
58344
58785
|
let result = imageProbeCache.get(key);
|
|
58345
58786
|
if (!result) {
|
|
58346
58787
|
result = exec3(command, args2);
|
|
@@ -58358,7 +58799,7 @@ function resolveBurnDeps(ctx) {
|
|
|
58358
58799
|
const engine = config2.sandbox === "podman" ? "podman" : "docker";
|
|
58359
58800
|
return cacheVolumeReady ??= ensureBurnCacheVolume({
|
|
58360
58801
|
engine,
|
|
58361
|
-
imageName: resolveSandboxImage(config2),
|
|
58802
|
+
imageName: resolveSandboxImage(config2, ctx.project),
|
|
58362
58803
|
projectId: ctx.project.id,
|
|
58363
58804
|
exec: exec3
|
|
58364
58805
|
});
|
|
@@ -58392,6 +58833,7 @@ function resolveBurnDeps(ctx) {
|
|
|
58392
58833
|
const { model: ticketModel, token: ticketToken } = ticketCredentials(ticket);
|
|
58393
58834
|
return burnAuthReady(ticketModel.runtime, ticketToken) ? undefined : ticketModel.runtime;
|
|
58394
58835
|
},
|
|
58836
|
+
ticketRuntime: (ticket) => ticketCredentials(ticket).model.runtime,
|
|
58395
58837
|
concurrency: config2.burnConcurrency,
|
|
58396
58838
|
executeTicketRun: (c, ticket, run7) => {
|
|
58397
58839
|
const { model: ticketModel, token: ticketToken } = ticketCredentials(ticket);
|
|
@@ -58408,9 +58850,9 @@ function resolveBurnDeps(ctx) {
|
|
|
58408
58850
|
function readTokenFromEnvFile(path2, runtime5) {
|
|
58409
58851
|
const key = RUNTIME_AUTH_KEY[runtime5];
|
|
58410
58852
|
let fromFile;
|
|
58411
|
-
if (
|
|
58853
|
+
if (existsSync18(path2)) {
|
|
58412
58854
|
try {
|
|
58413
|
-
fromFile = parseEnvFile3(
|
|
58855
|
+
fromFile = parseEnvFile3(readFileSync11(path2, "utf8"))[key];
|
|
58414
58856
|
} catch {
|
|
58415
58857
|
fromFile = undefined;
|
|
58416
58858
|
}
|
|
@@ -58418,7 +58860,7 @@ function readTokenFromEnvFile(path2, runtime5) {
|
|
|
58418
58860
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env[key];
|
|
58419
58861
|
return token && token.length > 0 ? token : undefined;
|
|
58420
58862
|
}
|
|
58421
|
-
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;
|
|
58863
|
+
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;
|
|
58422
58864
|
var init_ticket_burner = __esm(() => {
|
|
58423
58865
|
init_src();
|
|
58424
58866
|
init_config_load();
|
|
@@ -58443,6 +58885,26 @@ var init_ticket_burner = __esm(() => {
|
|
|
58443
58885
|
"claude-code": "claude",
|
|
58444
58886
|
codex: "codex"
|
|
58445
58887
|
};
|
|
58888
|
+
COMMAND_SEPARATORS = /&&|\|\||[;|\n]/;
|
|
58889
|
+
ASSIGNMENT_PREFIX = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
58890
|
+
NON_COMMAND_WORDS = new Set([
|
|
58891
|
+
"cd",
|
|
58892
|
+
"export",
|
|
58893
|
+
"set",
|
|
58894
|
+
"unset",
|
|
58895
|
+
"source",
|
|
58896
|
+
".",
|
|
58897
|
+
"exec",
|
|
58898
|
+
"env",
|
|
58899
|
+
"eval",
|
|
58900
|
+
"command",
|
|
58901
|
+
"time",
|
|
58902
|
+
"sudo",
|
|
58903
|
+
"echo",
|
|
58904
|
+
"true",
|
|
58905
|
+
"false"
|
|
58906
|
+
]);
|
|
58907
|
+
PLAIN_COMMAND_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
58446
58908
|
DROPPED_MAP_SECTIONS = ["Not yet specified", "Out of scope"];
|
|
58447
58909
|
PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
58448
58910
|
PM_CACHE_SANDBOX_PATHS = {
|
|
@@ -58486,27 +58948,42 @@ var init_ticket_burner = __esm(() => {
|
|
|
58486
58948
|
/is not a working tree/i,
|
|
58487
58949
|
/resource busy|ebusy|eperm|eacces/i
|
|
58488
58950
|
];
|
|
58489
|
-
|
|
58951
|
+
REFUSAL_STATUS = String.raw`\b(?:401|403)\b`;
|
|
58952
|
+
DETACHED_FORBIDDEN = String.raw`(?<!${REFUSAL_STATUS}[\W_]*)forbidden(?![\W_]*${REFUSAL_STATUS})`;
|
|
58953
|
+
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)`;
|
|
58954
|
+
ACCOUNT_PERMISSION_DENIED = refusalNamingCredential(String.raw`permission denied`);
|
|
58955
|
+
RUN_FATAL_ERROR_PATTERNS = [
|
|
58490
58956
|
/invalid (api key|x-api-key)/i,
|
|
58491
|
-
/authentication|unauthorized
|
|
58957
|
+
/authentication|unauthorized/i,
|
|
58958
|
+
ACCOUNT_PERMISSION_DENIED,
|
|
58492
58959
|
/credit balance|billing/i,
|
|
58493
58960
|
/oauth token|setup-token/i,
|
|
58494
|
-
/
|
|
58495
|
-
/does not support resumeSession|resumeSession .* not found/i
|
|
58961
|
+
/usage limit/i
|
|
58496
58962
|
];
|
|
58497
|
-
|
|
58963
|
+
REFUSED_CREDENTIAL_STATUS = refusalNamingCredential(String.raw`(?:${REFUSAL_STATUS}|forbidden)`);
|
|
58964
|
+
RUNTIME_RUN_FATAL_ERROR_PATTERNS = {
|
|
58498
58965
|
"claude-code": [],
|
|
58499
58966
|
codex: [
|
|
58500
|
-
/invalid_api_key
|
|
58501
|
-
|
|
58967
|
+
/invalid_api_key/i,
|
|
58968
|
+
REFUSED_CREDENTIAL_STATUS,
|
|
58502
58969
|
/insufficient_quota|exceeded your current quota/i,
|
|
58503
|
-
/model_not_found|does not exist or you do not have access/i,
|
|
58504
58970
|
/CODEX_API_KEY/,
|
|
58505
58971
|
/not logged in/i,
|
|
58506
58972
|
/\bauth (failed|required)\b/i,
|
|
58507
58973
|
/refresh token/i
|
|
58508
58974
|
]
|
|
58509
58975
|
};
|
|
58976
|
+
FATAL_ERROR_PATTERNS = [
|
|
58977
|
+
/issue with the selected model|model not found|unknown model/i,
|
|
58978
|
+
/does not support resumeSession|resumeSession .* not found/i
|
|
58979
|
+
];
|
|
58980
|
+
RUNTIME_FATAL_ERROR_PATTERNS = {
|
|
58981
|
+
"claude-code": [],
|
|
58982
|
+
codex: [
|
|
58983
|
+
/invalid_request_error/i,
|
|
58984
|
+
/model_not_found|does not exist or you do not have access/i
|
|
58985
|
+
]
|
|
58986
|
+
};
|
|
58510
58987
|
RETRYABLE_ERROR_PATTERNS = [
|
|
58511
58988
|
/exited with code \d+/i,
|
|
58512
58989
|
/idle timeout|AgentIdleTimeout/i,
|
|
@@ -58525,7 +59002,19 @@ var init_ticket_burner = __esm(() => {
|
|
|
58525
59002
|
"claude-code": "claude",
|
|
58526
59003
|
codex: "codex"
|
|
58527
59004
|
};
|
|
59005
|
+
MISSING_COMMAND_PATTERNS = [
|
|
59006
|
+
/\bexit(?:ed with)?(?: code)? 127\b/i,
|
|
59007
|
+
/command not found/i
|
|
59008
|
+
];
|
|
58528
59009
|
activeTicketAborts = new Map;
|
|
59010
|
+
RunHaltedAbort = class RunHaltedAbort extends Error {
|
|
59011
|
+
headline;
|
|
59012
|
+
constructor(headline) {
|
|
59013
|
+
super(`ticket stopped: run halted (${headline})`);
|
|
59014
|
+
this.headline = headline;
|
|
59015
|
+
this.name = "RunHaltedAbort";
|
|
59016
|
+
}
|
|
59017
|
+
};
|
|
58529
59018
|
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
58530
59019
|
ticketBurner = {
|
|
58531
59020
|
id: "ticket-burner",
|
|
@@ -58534,9 +59023,9 @@ var init_ticket_burner = __esm(() => {
|
|
|
58534
59023
|
});
|
|
58535
59024
|
|
|
58536
59025
|
// src/workflows/research.ts
|
|
58537
|
-
import { mkdirSync as mkdirSync11, readFileSync as
|
|
58538
|
-
import { dirname as dirname11, join as
|
|
58539
|
-
import { fileURLToPath as
|
|
59026
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync12 } from "fs";
|
|
59027
|
+
import { dirname as dirname11, join as join19 } from "path";
|
|
59028
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
58540
59029
|
function waypointSlug(waypoint) {
|
|
58541
59030
|
const base = waypoint.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
58542
59031
|
return `${waypoint.seq}-${base || "waypoint"}`;
|
|
@@ -58603,8 +59092,8 @@ function interpretResearchResult(result, docRelPath) {
|
|
|
58603
59092
|
return { status: "failed", error: "agent produced no commits \u2014 no research summary landed" };
|
|
58604
59093
|
}
|
|
58605
59094
|
function researchTemplatePath() {
|
|
58606
|
-
const here = dirname11(
|
|
58607
|
-
return
|
|
59095
|
+
const here = dirname11(fileURLToPath7(import.meta.url));
|
|
59096
|
+
return join19(resolveSkillsRoot(here), "burner", "research-waypoint.md");
|
|
58608
59097
|
}
|
|
58609
59098
|
function researchContainerName(runId, seq2) {
|
|
58610
59099
|
return `runcastle-${runId}-w${seq2}`;
|
|
@@ -58620,7 +59109,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
58620
59109
|
const { project: project3, feature } = ctx;
|
|
58621
59110
|
const docRelPath = researchDocRel(feature.slug, waypoint);
|
|
58622
59111
|
const tempBranch = researchBranchName(feature.slug, waypoint.seq, newId("b").slice(2, 10));
|
|
58623
|
-
const template =
|
|
59112
|
+
const template = readFileSync12(researchTemplatePath(), "utf8");
|
|
58624
59113
|
const prompt = renderResearchPrompt(template, {
|
|
58625
59114
|
WAYPOINT_JSON: buildWaypointJson(waypoint),
|
|
58626
59115
|
FEATURE_BRIEF: buildFeatureBrief(feature),
|
|
@@ -58628,12 +59117,12 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
58628
59117
|
RESEARCH_DOC_PATH: docRelPath
|
|
58629
59118
|
});
|
|
58630
59119
|
mkdirSync11(logsDir(), { recursive: true });
|
|
58631
|
-
const logFilePath =
|
|
59120
|
+
const logFilePath = join19(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
|
|
58632
59121
|
const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
|
|
58633
59122
|
const codexAuthMount = codexAuthMountFor(model.runtime, config2.sandbox);
|
|
58634
59123
|
const runOptions = {
|
|
58635
59124
|
agent: buildBurnAgent(config2, token, model),
|
|
58636
|
-
sandbox: selectSandbox(config2, codexAuthMount ? [codexAuthMount] : [], {}, registerResearchKill(config2, ctx.runId, waypoint.seq)),
|
|
59125
|
+
sandbox: selectSandbox(config2, project3, codexAuthMount ? [codexAuthMount] : [], {}, registerResearchKill(config2, ctx.runId, waypoint.seq)),
|
|
58637
59126
|
cwd: project3.repoPath,
|
|
58638
59127
|
prompt,
|
|
58639
59128
|
branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
|
|
@@ -59234,16 +59723,30 @@ async function rethinkAndLaunch(ctx, featureId, launch3) {
|
|
|
59234
59723
|
function errMsg2(e) {
|
|
59235
59724
|
return e instanceof Error ? e.message : String(e);
|
|
59236
59725
|
}
|
|
59726
|
+
function latestRunDeniedDirty(ctx, featureId) {
|
|
59727
|
+
const latestRun = listRunsByFeature(ctx, featureId)[0];
|
|
59728
|
+
if (!latestRun)
|
|
59729
|
+
return false;
|
|
59730
|
+
const deniedAt = latestEventTs(ctx, featureId, "reviewdrive.denied");
|
|
59731
|
+
return deniedAt !== undefined && deniedAt >= latestRun.startedAt;
|
|
59732
|
+
}
|
|
59237
59733
|
async function retryTicket(ctx, ticketId, opts = {}) {
|
|
59238
59734
|
const ticket = getTicket(ctx, ticketId);
|
|
59239
59735
|
const feature = getFeatureRow(ctx, ticket.featureId);
|
|
59240
|
-
|
|
59736
|
+
const retryingDeniedReview = ticket.kind === "review" && ticket.status === "done" && latestRunDeniedDirty(ctx, feature.id);
|
|
59737
|
+
if (ticket.status !== "failed" && !retryingDeniedReview) {
|
|
59241
59738
|
throw new GateError(`only failed tickets can be retried \u2014 ticket ${ticket.seq} is ${ticket.status}`);
|
|
59242
59739
|
}
|
|
59243
59740
|
if (hasActiveRun(ctx, feature.id)) {
|
|
59244
59741
|
throw new GateError("a run is live for this feature \u2014 retry after it finishes, or cancel it first");
|
|
59245
59742
|
}
|
|
59246
59743
|
const project3 = projectForFeature(ctx, feature);
|
|
59744
|
+
if (retryingDeniedReview) {
|
|
59745
|
+
const stillDirty = await driveBlockingPaths(project3.repoPath);
|
|
59746
|
+
if (stillDirty.length > 0) {
|
|
59747
|
+
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)}`);
|
|
59748
|
+
}
|
|
59749
|
+
}
|
|
59247
59750
|
const all8 = listByFeature2(ctx, feature.id);
|
|
59248
59751
|
const bySeq = new Map(all8.map((t) => [t.seq, t]));
|
|
59249
59752
|
const toReset = new Map([[ticket.seq, ticket]]);
|
|
@@ -59425,7 +59928,7 @@ var init_features = __esm(() => {
|
|
|
59425
59928
|
});
|
|
59426
59929
|
|
|
59427
59930
|
// src/services/test-notes.ts
|
|
59428
|
-
import { existsSync as
|
|
59931
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync12, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
59429
59932
|
import { dirname as dirname12 } from "path";
|
|
59430
59933
|
import { and as and10, asc as asc5, eq as eq13, lt } from "drizzle-orm";
|
|
59431
59934
|
function rowToNote(row) {
|
|
@@ -59440,7 +59943,7 @@ function rowToNote(row) {
|
|
|
59440
59943
|
reviewTicketId: row.reviewTicketId ?? undefined,
|
|
59441
59944
|
carriedLap: row.carriedLap ?? undefined,
|
|
59442
59945
|
videoTimestamp: row.videoTimestamp ?? undefined,
|
|
59443
|
-
screenshotUrl:
|
|
59946
|
+
screenshotUrl: existsSync20(annotationPath(row.id)) ? noteScreenshotUrl(row.id) : undefined,
|
|
59444
59947
|
createdAt: row.createdAt,
|
|
59445
59948
|
updatedAt: row.updatedAt
|
|
59446
59949
|
});
|
|
@@ -59710,8 +60213,8 @@ var init_test_notes = __esm(() => {
|
|
|
59710
60213
|
});
|
|
59711
60214
|
|
|
59712
60215
|
// src/mcp/server.ts
|
|
59713
|
-
import { existsSync as
|
|
59714
|
-
import { join as
|
|
60216
|
+
import { existsSync as existsSync21, readFileSync as readFileSync13 } from "fs";
|
|
60217
|
+
import { join as join20, relative as relative4, resolve as resolve10, sep as sep2 } from "path";
|
|
59715
60218
|
import { StreamableHTTPTransport } from "@hono/mcp";
|
|
59716
60219
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
59717
60220
|
import { Hono } from "hono";
|
|
@@ -59796,6 +60299,7 @@ function featureContext(ctx, reader) {
|
|
|
59796
60299
|
moreDocs,
|
|
59797
60300
|
docsNote: DOCS_NOTE,
|
|
59798
60301
|
openDefects: carried.openDefects,
|
|
60302
|
+
carriedDefects: carried.carriedDefects,
|
|
59799
60303
|
tickets: listByFeature2(ctx, feature.id).map(stripDigest),
|
|
59800
60304
|
annotatedModels: annotatedModels(ctx)
|
|
59801
60305
|
};
|
|
@@ -59852,7 +60356,13 @@ function toolEmitTickets(ctx, session, input) {
|
|
|
59852
60356
|
const feature = getFeatureRow(ctx, requireFeatureId(session));
|
|
59853
60357
|
refuseIfReadOnly(session, "emitting tickets");
|
|
59854
60358
|
refuseMisKindedReview(input.tickets);
|
|
60359
|
+
const linkedFindingIds = input.tickets.flatMap((t) => t.originFindingId ?? []);
|
|
60360
|
+
requireLinkableFindings(ctx, feature.id, linkedFindingIds);
|
|
59855
60361
|
const stored = storeTickets(ctx, feature.id, input.tickets);
|
|
60362
|
+
for (const ticket of stored) {
|
|
60363
|
+
if (ticket.originFindingId)
|
|
60364
|
+
linkFixTicket(ctx, feature.id, ticket.originFindingId, ticket.id);
|
|
60365
|
+
}
|
|
59856
60366
|
return {
|
|
59857
60367
|
stored: stored.length,
|
|
59858
60368
|
tickets: stored.map((t) => ({ id: t.id, seq: t.seq, title: t.title }))
|
|
@@ -59877,6 +60387,13 @@ function toolCancelTicket(ctx, session, input) {
|
|
|
59877
60387
|
refuseIfReadOnly(session, "cancelling a ticket");
|
|
59878
60388
|
return { ok: true, ticket: cancelTicket(ctx, input.id, input.reason) };
|
|
59879
60389
|
}
|
|
60390
|
+
function toolResolveFinding(ctx, session, input) {
|
|
60391
|
+
const featureId = requireFeatureId(session);
|
|
60392
|
+
refuseIfReadOnly(session, "resolving a finding");
|
|
60393
|
+
const { findingId, disposition, note: note2 } = ResolveFindingInput.parse(input);
|
|
60394
|
+
const finding = disposition === "carry" ? carryFinding(ctx, featureId, findingId, note2) : closeAsAddressed(ctx, featureId, findingId, note2 ?? "");
|
|
60395
|
+
return { ok: true, finding };
|
|
60396
|
+
}
|
|
59880
60397
|
function toolEscalateToMap(ctx, session, input) {
|
|
59881
60398
|
return escalateToMap(ctx, requireFeatureId(session), input);
|
|
59882
60399
|
}
|
|
@@ -60143,16 +60660,16 @@ function adrRef(adr) {
|
|
|
60143
60660
|
}
|
|
60144
60661
|
function toolReadAdr(ctx, session, input) {
|
|
60145
60662
|
requireProject(ctx, session);
|
|
60146
|
-
const root = resolve10(
|
|
60663
|
+
const root = resolve10(join20(session.worktreePath, ADR_DIR_REL));
|
|
60147
60664
|
const name = input.relPath.replace(/\\/g, "/").replace(/^\.?\/*docs\/adr\//i, "");
|
|
60148
60665
|
const target = resolve10(root, name);
|
|
60149
60666
|
const rel = relative4(root, target);
|
|
60150
60667
|
if (rel === "" || rel.startsWith("..") || rel.split(sep2).includes("..")) {
|
|
60151
60668
|
throw new InvalidInputError(`adr path escapes ${ADR_DIR_REL}: ${input.relPath}`);
|
|
60152
60669
|
}
|
|
60153
|
-
if (!
|
|
60670
|
+
if (!existsSync21(target))
|
|
60154
60671
|
throw new NotFoundError(`adr not found: ${input.relPath}`);
|
|
60155
|
-
return { relPath: `${ADR_DIR_REL}/${name}`, content:
|
|
60672
|
+
return { relPath: `${ADR_DIR_REL}/${name}`, content: readFileSync13(target, "utf8") };
|
|
60156
60673
|
}
|
|
60157
60674
|
function featureIndexLine(feature) {
|
|
60158
60675
|
if (feature.status === "shipped") {
|
|
@@ -60291,7 +60808,7 @@ function buildMcpServer(audience) {
|
|
|
60291
60808
|
if (wants("review_drive")) {
|
|
60292
60809
|
server.registerTool("review_drive", {
|
|
60293
60810
|
title: "Drive the feature branch for review",
|
|
60294
|
-
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.
|
|
60811
|
+
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.",
|
|
60295
60812
|
inputSchema: {
|
|
60296
60813
|
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.")
|
|
60297
60814
|
}
|
|
@@ -60511,6 +61028,18 @@ function buildMcpServer(audience) {
|
|
|
60511
61028
|
return ok(result);
|
|
60512
61029
|
});
|
|
60513
61030
|
}
|
|
61031
|
+
if (wants("resolve_finding")) {
|
|
61032
|
+
server.registerTool("resolve_finding", {
|
|
61033
|
+
title: "Resolve a review finding",
|
|
61034
|
+
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.',
|
|
61035
|
+
inputSchema: ResolveFindingShape
|
|
61036
|
+
}, async (args2, extra) => {
|
|
61037
|
+
const rs = await resolveCtxSession(extra);
|
|
61038
|
+
if (!rs)
|
|
61039
|
+
return noSession();
|
|
61040
|
+
return ok(toolResolveFinding(rs.ctx, rs.session, args2));
|
|
61041
|
+
});
|
|
61042
|
+
}
|
|
60514
61043
|
if (wants("escalate_to_map")) {
|
|
60515
61044
|
server.registerTool("escalate_to_map", {
|
|
60516
61045
|
title: "Escalate to map",
|
|
@@ -60607,12 +61136,29 @@ async function resolveAudience(sessionId, runId) {
|
|
|
60607
61136
|
return;
|
|
60608
61137
|
}
|
|
60609
61138
|
}
|
|
60610
|
-
|
|
61139
|
+
async function readMcpBody(c) {
|
|
61140
|
+
if (c.req.method !== "POST")
|
|
61141
|
+
return;
|
|
61142
|
+
let text3;
|
|
61143
|
+
try {
|
|
61144
|
+
text3 = await c.req.text();
|
|
61145
|
+
} catch (e) {
|
|
61146
|
+
console.error(`[mcp] request body read failed after ${c.req.header("content-length") ?? "unknown"} ` + `declared bytes: ${e instanceof Error ? e.message : String(e)}`);
|
|
61147
|
+
return;
|
|
61148
|
+
}
|
|
61149
|
+
try {
|
|
61150
|
+
return JSON.parse(text3);
|
|
61151
|
+
} catch {
|
|
61152
|
+
return;
|
|
61153
|
+
}
|
|
61154
|
+
}
|
|
61155
|
+
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;
|
|
60611
61156
|
var init_server = __esm(() => {
|
|
60612
61157
|
init_src();
|
|
60613
61158
|
init_paths();
|
|
60614
61159
|
init_errors();
|
|
60615
61160
|
init_artifacts();
|
|
61161
|
+
init_runtime();
|
|
60616
61162
|
init_sessions();
|
|
60617
61163
|
init_features();
|
|
60618
61164
|
init_carried_work();
|
|
@@ -60628,6 +61174,20 @@ var init_server = __esm(() => {
|
|
|
60628
61174
|
init_waypoints();
|
|
60629
61175
|
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.";
|
|
60630
61176
|
REVIEW_TITLE = /^review[: ]/i;
|
|
61177
|
+
ResolveFindingShape = {
|
|
61178
|
+
findingId: z4.string().min(1).describe("The finding id, from `get_feature_context`\u2019s openDefects or carriedDefects."),
|
|
61179
|
+
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."),
|
|
61180
|
+
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.")
|
|
61181
|
+
};
|
|
61182
|
+
ResolveFindingInput = z4.object(ResolveFindingShape).superRefine((input, refinement) => {
|
|
61183
|
+
if (input.disposition === "addressed" && !input.note?.trim()) {
|
|
61184
|
+
refinement.addIssue({
|
|
61185
|
+
code: "custom",
|
|
61186
|
+
path: ["note"],
|
|
61187
|
+
message: "closing a defect as addressed requires a note saying what addressed it"
|
|
61188
|
+
});
|
|
61189
|
+
}
|
|
61190
|
+
});
|
|
60631
61191
|
DRAFTING_KINDS = ["ideation", "revisit", "waypoint", "converge"];
|
|
60632
61192
|
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/.";
|
|
60633
61193
|
FEATURE_KINDS = [
|
|
@@ -60649,6 +61209,7 @@ var init_server = __esm(() => {
|
|
|
60649
61209
|
update_ticket: FEATURE_WRITE_KINDS,
|
|
60650
61210
|
cancel_ticket: FEATURE_WRITE_KINDS,
|
|
60651
61211
|
complete_phase: FEATURE_WRITE_KINDS,
|
|
61212
|
+
resolve_finding: FEATURE_WRITE_KINDS,
|
|
60652
61213
|
escalate_to_map: FEATURE_KINDS,
|
|
60653
61214
|
emit_waypoints: FEATURE_KINDS,
|
|
60654
61215
|
resolve_waypoint: FEATURE_KINDS,
|
|
@@ -60674,11 +61235,12 @@ var init_server = __esm(() => {
|
|
|
60674
61235
|
}
|
|
60675
61236
|
});
|
|
60676
61237
|
mcp.all("*", async (c) => {
|
|
61238
|
+
const body = await readMcpBody(c);
|
|
60677
61239
|
const audience = await resolveAudience(c.req.header("X-Runcastle-Session"), c.req.header(RUN_HEADER));
|
|
60678
61240
|
const server = buildMcpServer(audience);
|
|
60679
61241
|
const transport = new StreamableHTTPTransport({ enableJsonResponse: true });
|
|
60680
61242
|
await server.connect(transport);
|
|
60681
|
-
const res = await transport.handleRequest(c);
|
|
61243
|
+
const res = await transport.handleRequest(c, body);
|
|
60682
61244
|
return res ?? c.body(null, 202);
|
|
60683
61245
|
});
|
|
60684
61246
|
server_default = mcp;
|
|
@@ -60951,6 +61513,7 @@ function sessionStartContext(ctx, feature) {
|
|
|
60951
61513
|
var hooks, hooks_default;
|
|
60952
61514
|
var init_hooks = __esm(() => {
|
|
60953
61515
|
init_edit_guard();
|
|
61516
|
+
init_runtime();
|
|
60954
61517
|
init_sessions();
|
|
60955
61518
|
init_events();
|
|
60956
61519
|
init_git();
|
|
@@ -61019,12 +61582,12 @@ var init_hooks = __esm(() => {
|
|
|
61019
61582
|
});
|
|
61020
61583
|
|
|
61021
61584
|
// src/routes/reviews.ts
|
|
61022
|
-
import { createReadStream, statSync as
|
|
61585
|
+
import { createReadStream, statSync as statSync5 } from "fs";
|
|
61023
61586
|
import { Readable } from "stream";
|
|
61024
61587
|
import { Hono as Hono3 } from "hono";
|
|
61025
61588
|
function fileSize(path2) {
|
|
61026
61589
|
try {
|
|
61027
|
-
const stat3 =
|
|
61590
|
+
const stat3 = statSync5(path2);
|
|
61028
61591
|
return stat3.isFile() ? stat3.size : undefined;
|
|
61029
61592
|
} catch {
|
|
61030
61593
|
return;
|
|
@@ -61075,6 +61638,7 @@ var init_reviews = __esm(() => {
|
|
|
61075
61638
|
init_src();
|
|
61076
61639
|
init_paths();
|
|
61077
61640
|
init_errors();
|
|
61641
|
+
init_runtime();
|
|
61078
61642
|
init_test_notes();
|
|
61079
61643
|
init_tickets();
|
|
61080
61644
|
reviews = new Hono3;
|
|
@@ -61211,15 +61775,15 @@ var init_stream = __esm(() => {
|
|
|
61211
61775
|
});
|
|
61212
61776
|
|
|
61213
61777
|
// src/routes/web.ts
|
|
61214
|
-
import { existsSync as
|
|
61778
|
+
import { existsSync as existsSync22, statSync as statSync6 } from "fs";
|
|
61215
61779
|
import { readFile as readFile4 } from "fs/promises";
|
|
61216
|
-
import { extname, join as
|
|
61780
|
+
import { extname, join as join21, normalize as normalize2, resolve as resolve11, sep as sep3 } from "path";
|
|
61217
61781
|
function contentType(path2) {
|
|
61218
61782
|
return MIME[extname(path2).toLowerCase()] ?? "application/octet-stream";
|
|
61219
61783
|
}
|
|
61220
61784
|
function isFile2(path2) {
|
|
61221
61785
|
try {
|
|
61222
|
-
return
|
|
61786
|
+
return statSync6(path2).isFile();
|
|
61223
61787
|
} catch {
|
|
61224
61788
|
return false;
|
|
61225
61789
|
}
|
|
@@ -61231,7 +61795,7 @@ function safeJoin(root, pathname) {
|
|
|
61231
61795
|
} catch {
|
|
61232
61796
|
return null;
|
|
61233
61797
|
}
|
|
61234
|
-
const joined = normalize2(
|
|
61798
|
+
const joined = normalize2(join21(root, decoded));
|
|
61235
61799
|
if (joined !== root && !joined.startsWith(root + sep3))
|
|
61236
61800
|
return null;
|
|
61237
61801
|
return joined;
|
|
@@ -61244,7 +61808,7 @@ function resolveWebDist() {
|
|
|
61244
61808
|
}
|
|
61245
61809
|
function mountWebApp(app, distDir) {
|
|
61246
61810
|
const root = resolve11(distDir);
|
|
61247
|
-
const indexHtml =
|
|
61811
|
+
const indexHtml = join21(root, "index.html");
|
|
61248
61812
|
app.get("*", async (c, next4) => {
|
|
61249
61813
|
const { pathname } = new URL(c.req.url);
|
|
61250
61814
|
if (/^\/(api|mcp|ws|health)(\/|$)/.test(pathname))
|
|
@@ -61264,7 +61828,7 @@ function mountWebApp(app, distDir) {
|
|
|
61264
61828
|
}
|
|
61265
61829
|
function mountWebAppIfBuilt(app) {
|
|
61266
61830
|
const dist = resolveWebDist();
|
|
61267
|
-
if (
|
|
61831
|
+
if (existsSync22(join21(dist, "index.html")))
|
|
61268
61832
|
mountWebApp(app, dist);
|
|
61269
61833
|
}
|
|
61270
61834
|
var MIME;
|
|
@@ -61291,16 +61855,16 @@ var init_web = __esm(() => {
|
|
|
61291
61855
|
});
|
|
61292
61856
|
|
|
61293
61857
|
// src/version.ts
|
|
61294
|
-
import { readFileSync as
|
|
61295
|
-
import { dirname as dirname13, join as
|
|
61296
|
-
import { fileURLToPath as
|
|
61858
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
61859
|
+
import { dirname as dirname13, join as join23 } from "path";
|
|
61860
|
+
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
61297
61861
|
function runcastleVersion() {
|
|
61298
61862
|
if (cached4)
|
|
61299
61863
|
return cached4;
|
|
61300
|
-
const here = dirname13(
|
|
61301
|
-
for (const pkgPath of [
|
|
61864
|
+
const here = dirname13(fileURLToPath8(import.meta.url));
|
|
61865
|
+
for (const pkgPath of [join23(here, "..", "package.json"), join23(here, "package.json")]) {
|
|
61302
61866
|
try {
|
|
61303
|
-
const pkg = JSON.parse(
|
|
61867
|
+
const pkg = JSON.parse(readFileSync14(pkgPath, "utf8"));
|
|
61304
61868
|
if (typeof pkg.version === "string") {
|
|
61305
61869
|
cached4 = pkg.version;
|
|
61306
61870
|
return cached4;
|
|
@@ -61315,13 +61879,13 @@ var init_version = () => {};
|
|
|
61315
61879
|
|
|
61316
61880
|
// src/services/install-id.ts
|
|
61317
61881
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
61318
|
-
import { existsSync as
|
|
61882
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync13, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
|
|
61319
61883
|
import { dirname as dirname14 } from "path";
|
|
61320
61884
|
function getInstallId() {
|
|
61321
61885
|
const file = installIdPath();
|
|
61322
61886
|
try {
|
|
61323
|
-
if (
|
|
61324
|
-
const existing =
|
|
61887
|
+
if (existsSync23(file)) {
|
|
61888
|
+
const existing = readFileSync15(file, "utf8").trim();
|
|
61325
61889
|
if (UUID_RE.test(existing))
|
|
61326
61890
|
return existing;
|
|
61327
61891
|
}
|
|
@@ -61520,8 +62084,8 @@ var init_events2 = __esm(() => {
|
|
|
61520
62084
|
});
|
|
61521
62085
|
|
|
61522
62086
|
// src/launcher/launcher.ts
|
|
61523
|
-
import { existsSync as
|
|
61524
|
-
import { join as
|
|
62087
|
+
import { existsSync as existsSync24 } from "fs";
|
|
62088
|
+
import { join as join25 } from "path";
|
|
61525
62089
|
import { and as and12, eq as eq15 } from "drizzle-orm";
|
|
61526
62090
|
function ptyExitMessage(exitCode) {
|
|
61527
62091
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
@@ -61996,7 +62560,7 @@ async function buildPrepareBrief(ctx, project3) {
|
|
|
61996
62560
|
};
|
|
61997
62561
|
}
|
|
61998
62562
|
function probePrepareHost(repoPath) {
|
|
61999
|
-
const has4 = (rel) =>
|
|
62563
|
+
const has4 = (rel) => existsSync24(join25(repoPath, rel));
|
|
62000
62564
|
const lock = LOCKFILES.find(([file]) => has4(file));
|
|
62001
62565
|
return {
|
|
62002
62566
|
platform: process.platform,
|
|
@@ -62189,7 +62753,7 @@ var init_launcher = __esm(() => {
|
|
|
62189
62753
|
|
|
62190
62754
|
// src/services/outcome.ts
|
|
62191
62755
|
import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
62192
|
-
import { join as
|
|
62756
|
+
import { join as join26 } from "path";
|
|
62193
62757
|
function digestHasSubstance(text3) {
|
|
62194
62758
|
return text3.split(`
|
|
62195
62759
|
`).some((line) => {
|
|
@@ -62289,9 +62853,9 @@ async function promoteOutcomeDoc(ctx, project3, feature, target, delta) {
|
|
|
62289
62853
|
delta: { commits: delta.commits ?? 0, files: delta.files ?? 0 }
|
|
62290
62854
|
});
|
|
62291
62855
|
await onBranch(project3.repoPath, target, async () => {
|
|
62292
|
-
const docsDir =
|
|
62856
|
+
const docsDir = join26(project3.repoPath, ...featureDocsRel(feature.slug).split("/"));
|
|
62293
62857
|
mkdirSync14(docsDir, { recursive: true });
|
|
62294
|
-
writeFileSync10(
|
|
62858
|
+
writeFileSync10(join26(docsDir, "outcome.md"), doc, "utf8");
|
|
62295
62859
|
await commitDocs(project3.repoPath, `runcastle: outcome for ${feature.slug}`);
|
|
62296
62860
|
});
|
|
62297
62861
|
} catch (error) {
|
|
@@ -62418,9 +62982,9 @@ var init_feature = __esm(() => {
|
|
|
62418
62982
|
});
|
|
62419
62983
|
|
|
62420
62984
|
// src/services/fsbrowse.ts
|
|
62421
|
-
import { existsSync as
|
|
62985
|
+
import { existsSync as existsSync25, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
|
|
62422
62986
|
import { homedir as homedir4 } from "os";
|
|
62423
|
-
import { basename as basename3, dirname as dirname15, isAbsolute as isAbsolute3, join as
|
|
62987
|
+
import { basename as basename3, dirname as dirname15, isAbsolute as isAbsolute3, join as join27, resolve as resolve12, sep as sep4 } from "path";
|
|
62424
62988
|
function expandPath(input, home = homedir4()) {
|
|
62425
62989
|
const trimmed = input.trim();
|
|
62426
62990
|
if (trimmed === "")
|
|
@@ -62429,12 +62993,12 @@ function expandPath(input, home = homedir4()) {
|
|
|
62429
62993
|
if (out === "~")
|
|
62430
62994
|
out = home;
|
|
62431
62995
|
else if (out.startsWith("~/") || out.startsWith("~\\"))
|
|
62432
|
-
out =
|
|
62996
|
+
out = join27(home, out.slice(2));
|
|
62433
62997
|
return resolve12(out);
|
|
62434
62998
|
}
|
|
62435
62999
|
function looksLikeRepo(dir) {
|
|
62436
63000
|
try {
|
|
62437
|
-
return
|
|
63001
|
+
return existsSync25(join27(dir, ".git"));
|
|
62438
63002
|
} catch {
|
|
62439
63003
|
return false;
|
|
62440
63004
|
}
|
|
@@ -62445,20 +63009,20 @@ function listRoots(home = homedir4()) {
|
|
|
62445
63009
|
for (let code = 67;code <= 90; code++) {
|
|
62446
63010
|
const drive = `${String.fromCharCode(code)}:${sep4}`;
|
|
62447
63011
|
try {
|
|
62448
|
-
if (
|
|
63012
|
+
if (existsSync25(drive))
|
|
62449
63013
|
roots3.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
|
|
62450
63014
|
} catch {}
|
|
62451
63015
|
}
|
|
62452
63016
|
} else {
|
|
62453
63017
|
roots3.push({ label: "/", path: "/", kind: "drive" });
|
|
62454
63018
|
}
|
|
62455
|
-
if (
|
|
63019
|
+
if (existsSync25(home))
|
|
62456
63020
|
roots3.push({ label: "Home", path: home, kind: "home" });
|
|
62457
63021
|
const seen = new Set(roots3.map(pathKey));
|
|
62458
63022
|
for (const name of COMMON_DIRS) {
|
|
62459
|
-
const candidate =
|
|
63023
|
+
const candidate = join27(home, name);
|
|
62460
63024
|
const key = pathKey({ path: candidate });
|
|
62461
|
-
if (seen.has(key) || !
|
|
63025
|
+
if (seen.has(key) || !existsSync25(candidate))
|
|
62462
63026
|
continue;
|
|
62463
63027
|
seen.add(key);
|
|
62464
63028
|
roots3.push({ label: name, path: candidate, kind: "common" });
|
|
@@ -62489,12 +63053,12 @@ function browseDir(input, showHidden = false, home = homedir4()) {
|
|
|
62489
63053
|
if (!isAbsolute3(target)) {
|
|
62490
63054
|
throw new InvalidInputError(`path is not absolute: ${target}`);
|
|
62491
63055
|
}
|
|
62492
|
-
if (!
|
|
63056
|
+
if (!existsSync25(target)) {
|
|
62493
63057
|
throw new InvalidInputError(`path does not exist: ${target}`);
|
|
62494
63058
|
}
|
|
62495
63059
|
let dir = target;
|
|
62496
63060
|
try {
|
|
62497
|
-
if (!
|
|
63061
|
+
if (!statSync7(target).isDirectory())
|
|
62498
63062
|
dir = dirname15(target);
|
|
62499
63063
|
} catch (e) {
|
|
62500
63064
|
throw new InvalidInputError(`cannot read path: ${target} (${errMsg4(e)})`);
|
|
@@ -62517,14 +63081,14 @@ function browseDir(input, showHidden = false, home = homedir4()) {
|
|
|
62517
63081
|
continue;
|
|
62518
63082
|
if (!showHidden && name.startsWith("."))
|
|
62519
63083
|
continue;
|
|
62520
|
-
const path2 =
|
|
63084
|
+
const path2 = join27(dir, name);
|
|
62521
63085
|
const isSymlink = dirent.isSymbolicLink();
|
|
62522
63086
|
if (isSymlink && !showHidden)
|
|
62523
63087
|
continue;
|
|
62524
63088
|
let isDirectory = dirent.isDirectory();
|
|
62525
63089
|
if (isSymlink) {
|
|
62526
63090
|
try {
|
|
62527
|
-
isDirectory =
|
|
63091
|
+
isDirectory = statSync7(path2).isDirectory();
|
|
62528
63092
|
} catch {
|
|
62529
63093
|
continue;
|
|
62530
63094
|
}
|
|
@@ -62554,7 +63118,7 @@ var init_fsbrowse = __esm(() => {
|
|
|
62554
63118
|
});
|
|
62555
63119
|
|
|
62556
63120
|
// src/services/transcripts.ts
|
|
62557
|
-
import { readFileSync as
|
|
63121
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
62558
63122
|
function* entries(jsonl) {
|
|
62559
63123
|
for (const line of jsonl.split(`
|
|
62560
63124
|
`)) {
|
|
@@ -62635,7 +63199,7 @@ function readTranscript2(path2, runtime5) {
|
|
|
62635
63199
|
if (!path2)
|
|
62636
63200
|
return { status: "ok", turns: [] };
|
|
62637
63201
|
try {
|
|
62638
|
-
return parseTranscript(
|
|
63202
|
+
return parseTranscript(readFileSync16(path2, "utf8"), runtime5);
|
|
62639
63203
|
} catch {
|
|
62640
63204
|
return { status: "ok", turns: [] };
|
|
62641
63205
|
}
|
|
@@ -62748,8 +63312,8 @@ var init_conversations = __esm(() => {
|
|
|
62748
63312
|
});
|
|
62749
63313
|
|
|
62750
63314
|
// src/services/projects.ts
|
|
62751
|
-
import { existsSync as
|
|
62752
|
-
import { basename as basename4, join as
|
|
63315
|
+
import { existsSync as existsSync26 } from "fs";
|
|
63316
|
+
import { basename as basename4, join as join28 } from "path";
|
|
62753
63317
|
import { eq as eq16, isNull } from "drizzle-orm";
|
|
62754
63318
|
function listProjects(ctx) {
|
|
62755
63319
|
return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
|
|
@@ -62828,7 +63392,7 @@ async function assertRepoTolerant(ctx, repoPath) {
|
|
|
62828
63392
|
} catch (e) {
|
|
62829
63393
|
if (!isNotImplemented(e))
|
|
62830
63394
|
throw e;
|
|
62831
|
-
if (!
|
|
63395
|
+
if (!existsSync26(join28(repoPath, ".git"))) {
|
|
62832
63396
|
throw new InvalidInputError(`not a git repository: ${repoPath}`);
|
|
62833
63397
|
}
|
|
62834
63398
|
}
|
|
@@ -62893,6 +63457,7 @@ var init_review_findings2 = __esm(() => {
|
|
|
62893
63457
|
reviewFindingsRouter = router({
|
|
62894
63458
|
listByFeature: publicProcedure.input(z9.object({ featureId: z9.string() })).query(({ ctx, input }) => viewByFeature(ctx, input.featureId)),
|
|
62895
63459
|
dismiss: publicProcedure.input(z9.object({ findingId: z9.string() })).mutation(({ ctx, input }) => dismiss(ctx, input.findingId)),
|
|
63460
|
+
reopen: publicProcedure.input(z9.object({ findingId: z9.string() })).mutation(({ ctx, input }) => reopenFinding(ctx, input.findingId)),
|
|
62896
63461
|
fixOpenDefects: publicProcedure.input(z9.object({ featureId: z9.string() })).mutation(async ({ ctx, input }) => {
|
|
62897
63462
|
const promoted = promoteOpenDefects(ctx, input.featureId);
|
|
62898
63463
|
const { runId } = await burn(ctx, input.featureId);
|
|
@@ -62978,13 +63543,13 @@ var init_settings2 = __esm(() => {
|
|
|
62978
63543
|
});
|
|
62979
63544
|
|
|
62980
63545
|
// src/doctor/afk-env.ts
|
|
62981
|
-
import { existsSync as
|
|
63546
|
+
import { existsSync as existsSync27, readFileSync as readFileSync17 } from "fs";
|
|
62982
63547
|
function envWithAfkCredentials() {
|
|
62983
63548
|
const merged = { ...process.env };
|
|
62984
63549
|
try {
|
|
62985
63550
|
const path2 = envPath();
|
|
62986
|
-
if (
|
|
62987
|
-
const fromFile = parseEnvFile3(
|
|
63551
|
+
if (existsSync27(path2)) {
|
|
63552
|
+
const fromFile = parseEnvFile3(readFileSync17(path2, "utf8"));
|
|
62988
63553
|
for (const runtime5 of AGENT_RUNTIMES) {
|
|
62989
63554
|
const key = RUNTIME_SPECS[runtime5].afkKey;
|
|
62990
63555
|
const value = fromFile[key];
|
|
@@ -63002,9 +63567,84 @@ var init_afk_env = __esm(() => {
|
|
|
63002
63567
|
init_doctor();
|
|
63003
63568
|
});
|
|
63004
63569
|
|
|
63570
|
+
// src/services/project-image.ts
|
|
63571
|
+
function adoptProjectImage(ctx, projectId, tag) {
|
|
63572
|
+
if (!isOverwritable(ctx, projectId, "sandboxImage"))
|
|
63573
|
+
return false;
|
|
63574
|
+
recordFinding(ctx, projectId, {
|
|
63575
|
+
key: "sandboxImage",
|
|
63576
|
+
value: tag,
|
|
63577
|
+
source: "build",
|
|
63578
|
+
evidence: `Built from .runcastle/sandbox/Dockerfile as ${tag}.`
|
|
63579
|
+
});
|
|
63580
|
+
emitProject(ctx, projectId, {
|
|
63581
|
+
type: "settings.updated",
|
|
63582
|
+
message: `sandboxImage set to ${tag} by the image build`,
|
|
63583
|
+
data: { key: "sandboxImage", scope: "project", value: tag }
|
|
63584
|
+
});
|
|
63585
|
+
return true;
|
|
63586
|
+
}
|
|
63587
|
+
function releaseProjectImage(ctx, projectId) {
|
|
63588
|
+
if (!isOverwritable(ctx, projectId, "sandboxImage"))
|
|
63589
|
+
return false;
|
|
63590
|
+
recordFinding(ctx, projectId, { key: "sandboxImage", value: null, source: "build" });
|
|
63591
|
+
emitProject(ctx, projectId, {
|
|
63592
|
+
type: "settings.updated",
|
|
63593
|
+
message: "sandboxImage cleared \u2014 .runcastle/sandbox/Dockerfile is gone",
|
|
63594
|
+
data: { key: "sandboxImage", scope: "project", value: null }
|
|
63595
|
+
});
|
|
63596
|
+
return true;
|
|
63597
|
+
}
|
|
63598
|
+
var init_project_image = __esm(() => {
|
|
63599
|
+
init_events();
|
|
63600
|
+
init_findings();
|
|
63601
|
+
});
|
|
63602
|
+
|
|
63005
63603
|
// src/trpc/routers/setup.ts
|
|
63604
|
+
import { join as join29 } from "path";
|
|
63006
63605
|
import { isNotNull as isNotNull4 } from "drizzle-orm";
|
|
63007
63606
|
import * as z12 from "zod";
|
|
63607
|
+
function projectImageEnv(ctx, project3) {
|
|
63608
|
+
return {
|
|
63609
|
+
id: project3.id,
|
|
63610
|
+
repoPath: project3.repoPath,
|
|
63611
|
+
stored: project3.sandboxImage ?? null,
|
|
63612
|
+
overwritable: isOverwritable(ctx, project3.id, "sandboxImage"),
|
|
63613
|
+
clearStored: () => releaseProjectImage(ctx, project3.id)
|
|
63614
|
+
};
|
|
63615
|
+
}
|
|
63616
|
+
async function buildImageTerminal(ctx, projectId) {
|
|
63617
|
+
const exec3 = createSystemExec();
|
|
63618
|
+
const runtime5 = await resolveRuntime(exec3, ctx.config.sandbox === "podman" ? "podman" : "docker");
|
|
63619
|
+
const project3 = projectId ? requireProjectById(ctx, projectId) : null;
|
|
63620
|
+
const stockContext = prepareSandboxBuildContext();
|
|
63621
|
+
const stockHash = hashDockerfile(join29(stockContext, "Dockerfile"));
|
|
63622
|
+
const plan = planImageBuild({
|
|
63623
|
+
config: ctx.config,
|
|
63624
|
+
project: project3 ? {
|
|
63625
|
+
id: project3.id,
|
|
63626
|
+
repoPath: project3.repoPath,
|
|
63627
|
+
sandboxImage: project3.sandboxImage,
|
|
63628
|
+
sandboxImageOverwritable: isOverwritable(ctx, project3.id, "sandboxImage")
|
|
63629
|
+
} : null,
|
|
63630
|
+
stockContext,
|
|
63631
|
+
stockFresh: stockHash !== null && (await inspectBuiltImage(exec3, runtime5, DEFAULT_SANDBOX_IMAGE)).hash === stockHash,
|
|
63632
|
+
buildArgs: stockBuildArgs(runtime5)
|
|
63633
|
+
});
|
|
63634
|
+
if (plan.kind === "refused")
|
|
63635
|
+
throw new InvalidInputError(plan.reason);
|
|
63636
|
+
const spec = imageBuildTerminal(runtime5, plan);
|
|
63637
|
+
if (plan.kind !== "chain" || !project3)
|
|
63638
|
+
return { spec };
|
|
63639
|
+
const { projectTag } = plan;
|
|
63640
|
+
return {
|
|
63641
|
+
spec,
|
|
63642
|
+
onExit: ({ exitCode }) => {
|
|
63643
|
+
if (exitCode === 0)
|
|
63644
|
+
adoptProjectImage(ctx, project3.id, projectTag);
|
|
63645
|
+
}
|
|
63646
|
+
};
|
|
63647
|
+
}
|
|
63008
63648
|
var setupRouter;
|
|
63009
63649
|
var init_setup2 = __esm(() => {
|
|
63010
63650
|
init_src();
|
|
@@ -63012,41 +63652,48 @@ var init_setup2 = __esm(() => {
|
|
|
63012
63652
|
init_afk_env();
|
|
63013
63653
|
init_doctor();
|
|
63014
63654
|
init_system_exec();
|
|
63655
|
+
init_errors();
|
|
63015
63656
|
init_asset_paths();
|
|
63016
63657
|
init_registry();
|
|
63658
|
+
init_findings();
|
|
63659
|
+
init_project_image();
|
|
63660
|
+
init_repo();
|
|
63661
|
+
init_sandbox_image();
|
|
63017
63662
|
init_setup();
|
|
63018
63663
|
init_resolve_executable();
|
|
63019
63664
|
init_context();
|
|
63020
63665
|
setupRouter = router({
|
|
63021
|
-
doctor: publicProcedure.
|
|
63666
|
+
doctor: publicProcedure.input(z12.object({
|
|
63667
|
+
projectId: z12.string().optional()
|
|
63668
|
+
}).optional()).query(({ ctx, input }) => {
|
|
63022
63669
|
const projectModels = ctx.db.select({ model: projects.model }).from(projects).where(isNotNull4(projects.model)).all().map((p) => p.model);
|
|
63670
|
+
const project3 = input?.projectId ? requireProjectById(ctx, input.projectId) : null;
|
|
63023
63671
|
return runDoctor({
|
|
63024
63672
|
exec: createSystemExec(),
|
|
63025
63673
|
burnerDockerfile: burnerDockerfilePath(),
|
|
63026
63674
|
env: envWithAfkCredentials(),
|
|
63027
63675
|
runtimes: configuredRuntimes(ctx.config, projectModels),
|
|
63028
|
-
|
|
63676
|
+
imageName: resolveSandboxImage(ctx.config),
|
|
63677
|
+
...project3 ? { projectImage: projectImageEnv(ctx, project3) } : {}
|
|
63029
63678
|
});
|
|
63030
63679
|
}),
|
|
63031
63680
|
runtimeGuide: publicProcedure.query(() => runtimeInstallGuide(process.platform)),
|
|
63032
63681
|
gitIdentity: publicProcedure.input(z12.object({ name: z12.string(), email: z12.string() })).mutation(({ input }) => writeGitIdentity(createSystemExec(), input)),
|
|
63033
63682
|
afkToken: publicProcedure.input(z12.object({ token: z12.string(), runtime: AgentRuntime.default("claude-code") })).mutation(({ input }) => saveAfkCredential(fileAfkTokenIo(createSystemExec(), input.runtime), input.token, input.runtime)),
|
|
63034
63683
|
seedModelDefaults: publicProcedure.input(z12.object({ runtimes: z12.array(AgentRuntime) })).mutation(({ ctx, input }) => seedModelDefaults(ctx, input.runtimes)),
|
|
63035
|
-
startTerminal: publicProcedure.input(z12.object({
|
|
63036
|
-
|
|
63037
|
-
|
|
63038
|
-
|
|
63039
|
-
const imageName = resolveSandboxImage(ctx.config);
|
|
63040
|
-
const sandcastleBin = input.kind === "build-image" ? resolveSandcastleBin() ?? undefined : undefined;
|
|
63041
|
-
const spec = terminalSpec(input.kind, { runtime: runtime5, imageName, sandcastleBin });
|
|
63684
|
+
startTerminal: publicProcedure.input(z12.object({
|
|
63685
|
+
kind: z12.enum(["setup-token", "build-image", "claude-login", "codex-login"]),
|
|
63686
|
+
projectId: z12.string().optional()
|
|
63687
|
+
})).mutation(async ({ ctx, input }) => {
|
|
63042
63688
|
const sessionId = newId("setup");
|
|
63043
|
-
const
|
|
63689
|
+
const { spec, onExit: onExit4 } = input.kind === "build-image" ? await buildImageTerminal(ctx, input.projectId) : { spec: { ...terminalSpec(input.kind), cwd: process.cwd() }, onExit: undefined };
|
|
63044
63690
|
const { file, args: args2 } = resolveSpawnTarget(spec.cmd, spec.args);
|
|
63045
63691
|
ptyRegistry().create({
|
|
63046
63692
|
sessionId,
|
|
63047
63693
|
cmd: file,
|
|
63048
63694
|
args: args2,
|
|
63049
|
-
opts: { cwd, env: process.env }
|
|
63695
|
+
opts: { cwd: spec.cwd, env: process.env },
|
|
63696
|
+
...onExit4 ? { onExit: onExit4 } : {}
|
|
63050
63697
|
});
|
|
63051
63698
|
return { sessionId };
|
|
63052
63699
|
})
|
|
@@ -63308,6 +63955,7 @@ async function startServer() {
|
|
|
63308
63955
|
runMigrations(db);
|
|
63309
63956
|
const ctx = { db, config: config2 };
|
|
63310
63957
|
const app = buildApp(ctx);
|
|
63958
|
+
followConfigFile(ctx);
|
|
63311
63959
|
const reconciled = reconcileStaleSessions(ctx);
|
|
63312
63960
|
if (reconciled.length > 0) {
|
|
63313
63961
|
console.log(`reconciled ${reconciled.length} stale session(s) from a previous server run`);
|
|
@@ -63349,6 +63997,7 @@ var init_src2 = __esm(() => {
|
|
|
63349
63997
|
init_client();
|
|
63350
63998
|
init_migrate();
|
|
63351
63999
|
init_reconcile();
|
|
64000
|
+
init_runtime();
|
|
63352
64001
|
init_server();
|
|
63353
64002
|
init_registry();
|
|
63354
64003
|
init_ws();
|
|
@@ -63405,6 +64054,8 @@ var init_report = __esm(() => {
|
|
|
63405
64054
|
ok: "\u2713",
|
|
63406
64055
|
stale: "\u2717",
|
|
63407
64056
|
missing: "\u2717",
|
|
64057
|
+
"not-built-yet": "\u2717",
|
|
64058
|
+
custom: "\xB7",
|
|
63408
64059
|
"daemon-dead": "\u2717",
|
|
63409
64060
|
"machine-stopped": "\u2717",
|
|
63410
64061
|
unhealthy: "\u2717",
|
|
@@ -63464,7 +64115,7 @@ var init_cli = __esm(() => {
|
|
|
63464
64115
|
init_asset_paths();
|
|
63465
64116
|
init_version();
|
|
63466
64117
|
import { dirname as dirname16, resolve as resolve13 } from "path";
|
|
63467
|
-
import { fileURLToPath as
|
|
64118
|
+
import { fileURLToPath as fileURLToPath9 } from "url";
|
|
63468
64119
|
function parseCommand(argv) {
|
|
63469
64120
|
const [first, ...rest] = argv;
|
|
63470
64121
|
if (first === undefined)
|
|
@@ -63488,7 +64139,7 @@ Usage:
|
|
|
63488
64139
|
runcastle --help Show this help
|
|
63489
64140
|
`;
|
|
63490
64141
|
async function main(argv) {
|
|
63491
|
-
applyInstalledAssetEnv(resolve13(dirname16(
|
|
64142
|
+
applyInstalledAssetEnv(resolve13(dirname16(fileURLToPath9(import.meta.url)), ".."));
|
|
63492
64143
|
const { command, args: args2 } = parseCommand(argv);
|
|
63493
64144
|
switch (command) {
|
|
63494
64145
|
case "version":
|