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