runcastle 1.0.23 → 1.0.25
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 +2167 -923
- package/drizzle/0010_misty_ken_ellis.sql +24 -0
- package/drizzle/meta/0010_snapshot.json +757 -0
- package/drizzle/meta/_journal.json +7 -0
- package/index.js +2161 -917
- package/package.json +1 -1
- package/pty-host.cjs +5 -1
- package/skills/burner/implement-ticket.md +32 -3
- package/skills/burner/prepare-project.md +92 -0
- package/skills/burner/resolve-conflict.md +10 -1
- package/skills/packs/README.md +1 -1
- package/skills/packs/runcastle/skills/tickets/SKILL.md +22 -6
- package/web/assets/index-B4LHcg8h.css +1 -0
- package/web/assets/{index-BcWKL2hj.js → index-DrjQZmZ4.js} +21 -21
- package/web/index.html +2 -2
- package/web/assets/index-fWdYT4eC.css +0 -1
package/bin/runcastle.js
CHANGED
|
@@ -69,6 +69,20 @@ import * as z from "zod";
|
|
|
69
69
|
function resolveSandboxImage(config) {
|
|
70
70
|
return config.sandboxImage ?? DEFAULT_SANDBOX_IMAGE;
|
|
71
71
|
}
|
|
72
|
+
function resolvePreparedSettings(config, project) {
|
|
73
|
+
const pick = (a, b) => {
|
|
74
|
+
const own = a?.trim();
|
|
75
|
+
if (own)
|
|
76
|
+
return own;
|
|
77
|
+
const inherited = b?.trim();
|
|
78
|
+
return inherited || undefined;
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
setupCommand: pick(project?.setupCommand, config.setupCommand),
|
|
82
|
+
verifyCommands: pick(project?.verifyCommands, config.verifyCommands),
|
|
83
|
+
knownFailures: pick(project?.knownFailures, config.knownFailures)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
72
86
|
function resolveModel(step, config, project, runOverride) {
|
|
73
87
|
return runOverride ?? config.stepModels[step] ?? project?.model ?? config.model;
|
|
74
88
|
}
|
|
@@ -94,6 +108,7 @@ var init_config = __esm(() => {
|
|
|
94
108
|
"revisit",
|
|
95
109
|
"research",
|
|
96
110
|
"implement",
|
|
111
|
+
"prepare",
|
|
97
112
|
"smoke"
|
|
98
113
|
];
|
|
99
114
|
ModelStep = z.enum(MODEL_STEPS);
|
|
@@ -105,10 +120,15 @@ var init_config = __esm(() => {
|
|
|
105
120
|
mainBranch: z.string().default("main"),
|
|
106
121
|
sandboxImage: z.string().optional(),
|
|
107
122
|
burnConcurrency: z.number().int().min(1).max(8).default(3),
|
|
123
|
+
burnCpus: z.number().positive().max(256).optional(),
|
|
124
|
+
burnGuard: z.boolean().default(true),
|
|
108
125
|
burnMaxIterations: z.number().int().min(1).max(10).default(3),
|
|
109
126
|
burnAttempts: z.number().int().min(1).max(5).default(3),
|
|
110
127
|
burnConflictAttempts: z.number().int().min(0).max(3).default(2),
|
|
128
|
+
autoPrepare: z.boolean().default(true),
|
|
111
129
|
setupCommand: z.string().optional(),
|
|
130
|
+
verifyCommands: z.string().optional(),
|
|
131
|
+
knownFailures: z.string().optional(),
|
|
112
132
|
burnWorkspace: z.enum(["auto", "mounted", "isolated"]).default("auto")
|
|
113
133
|
}));
|
|
114
134
|
});
|
|
@@ -154,8 +174,17 @@ function loadConfig(env = process.env) {
|
|
|
154
174
|
if (env.RUNCASTLE_BURN_CONFLICT_ATTEMPTS !== undefined) {
|
|
155
175
|
overrides.burnConflictAttempts = Number(env.RUNCASTLE_BURN_CONFLICT_ATTEMPTS);
|
|
156
176
|
}
|
|
177
|
+
if (env.RUNCASTLE_BURN_CPUS)
|
|
178
|
+
overrides.burnCpus = Number(env.RUNCASTLE_BURN_CPUS);
|
|
179
|
+
if (env.RUNCASTLE_BURN_GUARD !== undefined) {
|
|
180
|
+
overrides.burnGuard = env.RUNCASTLE_BURN_GUARD !== "0" && env.RUNCASTLE_BURN_GUARD !== "false";
|
|
181
|
+
}
|
|
157
182
|
if (env.RUNCASTLE_SETUP_COMMAND)
|
|
158
183
|
overrides.setupCommand = env.RUNCASTLE_SETUP_COMMAND;
|
|
184
|
+
if (env.RUNCASTLE_VERIFY_COMMANDS)
|
|
185
|
+
overrides.verifyCommands = env.RUNCASTLE_VERIFY_COMMANDS;
|
|
186
|
+
if (env.RUNCASTLE_KNOWN_FAILURES)
|
|
187
|
+
overrides.knownFailures = env.RUNCASTLE_KNOWN_FAILURES;
|
|
159
188
|
if (env.RUNCASTLE_BURN_WORKSPACE)
|
|
160
189
|
overrides.burnWorkspace = env.RUNCASTLE_BURN_WORKSPACE;
|
|
161
190
|
const base = typeof fileConfig === "object" && fileConfig !== null ? fileConfig : {};
|
|
@@ -190,7 +219,7 @@ var init_ids = () => {};
|
|
|
190
219
|
|
|
191
220
|
// ../core/src/schemas.ts
|
|
192
221
|
import * as z2 from "zod";
|
|
193
|
-
var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, Project, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
222
|
+
var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, PREPARED_KEYS, PreparedKey, FindingSource, Project, ProjectFinding, PrepStatus, PrepRun, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
194
223
|
var init_schemas = __esm(() => {
|
|
195
224
|
Phase = z2.enum([
|
|
196
225
|
"ideation",
|
|
@@ -243,13 +272,44 @@ var init_schemas = __esm(() => {
|
|
|
243
272
|
lastSessionId: z2.string().optional(),
|
|
244
273
|
summary: z2.string().optional()
|
|
245
274
|
});
|
|
275
|
+
PREPARED_KEYS = [
|
|
276
|
+
"setupCommand",
|
|
277
|
+
"verifyCommands",
|
|
278
|
+
"knownFailures",
|
|
279
|
+
"devCommand",
|
|
280
|
+
"dbResetCommand"
|
|
281
|
+
];
|
|
282
|
+
PreparedKey = z2.enum(PREPARED_KEYS);
|
|
283
|
+
FindingSource = z2.enum(["prep", "human"]);
|
|
246
284
|
Project = z2.object({
|
|
247
285
|
id: z2.string(),
|
|
248
286
|
name: z2.string(),
|
|
249
287
|
repoPath: z2.string(),
|
|
250
288
|
mainBranch: z2.string(),
|
|
251
289
|
devCommand: z2.string().optional(),
|
|
252
|
-
model: z2.string().optional()
|
|
290
|
+
model: z2.string().optional(),
|
|
291
|
+
setupCommand: z2.string().optional(),
|
|
292
|
+
verifyCommands: z2.string().optional(),
|
|
293
|
+
knownFailures: z2.string().optional(),
|
|
294
|
+
dbResetCommand: z2.string().optional()
|
|
295
|
+
});
|
|
296
|
+
ProjectFinding = z2.object({
|
|
297
|
+
key: PreparedKey,
|
|
298
|
+
source: FindingSource,
|
|
299
|
+
evidence: z2.string().optional(),
|
|
300
|
+
establishedAt: z2.number(),
|
|
301
|
+
establishedSha: z2.string().optional(),
|
|
302
|
+
staleCommits: z2.number().optional()
|
|
303
|
+
});
|
|
304
|
+
PrepStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
|
|
305
|
+
PrepRun = z2.object({
|
|
306
|
+
id: z2.string(),
|
|
307
|
+
projectId: z2.string(),
|
|
308
|
+
status: PrepStatus,
|
|
309
|
+
startedAt: z2.number(),
|
|
310
|
+
endedAt: z2.number().optional(),
|
|
311
|
+
summary: z2.string().optional(),
|
|
312
|
+
headSha: z2.string().optional()
|
|
253
313
|
});
|
|
254
314
|
Feature = z2.object({
|
|
255
315
|
id: z2.string(),
|
|
@@ -448,8 +508,8 @@ var init_pipeline = __esm(() => {
|
|
|
448
508
|
});
|
|
449
509
|
|
|
450
510
|
// ../core/src/db-schema.ts
|
|
451
|
-
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
452
|
-
var projects, features, sessions, tickets, waypoints, runs, events, gateOverrides;
|
|
511
|
+
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
512
|
+
var projects, projectFindings, projectPreps, features, sessions, tickets, waypoints, runs, events, gateOverrides;
|
|
453
513
|
var init_db_schema = __esm(() => {
|
|
454
514
|
projects = sqliteTable("projects", {
|
|
455
515
|
id: text("id").primaryKey(),
|
|
@@ -459,7 +519,28 @@ var init_db_schema = __esm(() => {
|
|
|
459
519
|
devCommand: text("dev_command"),
|
|
460
520
|
model: text("model"),
|
|
461
521
|
sandbox: text("sandbox"),
|
|
462
|
-
closedAt: integer("closed_at")
|
|
522
|
+
closedAt: integer("closed_at"),
|
|
523
|
+
setupCommand: text("setup_command"),
|
|
524
|
+
verifyCommands: text("verify_commands"),
|
|
525
|
+
knownFailures: text("known_failures"),
|
|
526
|
+
dbResetCommand: text("db_reset_command")
|
|
527
|
+
});
|
|
528
|
+
projectFindings = sqliteTable("project_findings", {
|
|
529
|
+
projectId: text("project_id").notNull(),
|
|
530
|
+
key: text("key").notNull(),
|
|
531
|
+
source: text("source").notNull().$type(),
|
|
532
|
+
evidence: text("evidence"),
|
|
533
|
+
establishedAt: integer("established_at").notNull(),
|
|
534
|
+
establishedSha: text("established_sha")
|
|
535
|
+
}, (t) => [primaryKey({ columns: [t.projectId, t.key] })]);
|
|
536
|
+
projectPreps = sqliteTable("project_preps", {
|
|
537
|
+
id: text("id").primaryKey(),
|
|
538
|
+
projectId: text("project_id").notNull(),
|
|
539
|
+
status: text("status").notNull().$type(),
|
|
540
|
+
startedAt: integer("started_at").notNull(),
|
|
541
|
+
endedAt: integer("ended_at"),
|
|
542
|
+
summary: text("summary"),
|
|
543
|
+
headSha: text("head_sha")
|
|
463
544
|
});
|
|
464
545
|
features = sqliteTable("features", {
|
|
465
546
|
id: text("id").primaryKey(),
|
|
@@ -563,7 +644,9 @@ var init_schema = __esm(() => {
|
|
|
563
644
|
waypoints,
|
|
564
645
|
runs,
|
|
565
646
|
events,
|
|
566
|
-
gateOverrides
|
|
647
|
+
gateOverrides,
|
|
648
|
+
projectFindings,
|
|
649
|
+
projectPreps
|
|
567
650
|
};
|
|
568
651
|
});
|
|
569
652
|
|
|
@@ -660,6 +743,72 @@ var init_migrate = __esm(() => {
|
|
|
660
743
|
// src/util/resolve-executable.ts
|
|
661
744
|
import { existsSync as existsSync3 } from "fs";
|
|
662
745
|
import { win32, posix } from "path";
|
|
746
|
+
function wellKnownBinDirs(opts = {}) {
|
|
747
|
+
const env = opts.env ?? process.env;
|
|
748
|
+
const isWin = (opts.platform ?? process.platform) === "win32";
|
|
749
|
+
const dirs = [];
|
|
750
|
+
if (isWin) {
|
|
751
|
+
const home2 = env.USERPROFILE;
|
|
752
|
+
if (home2) {
|
|
753
|
+
dirs.push(win32.join(home2, ".local", "bin"), win32.join(home2, ".bun", "bin"));
|
|
754
|
+
}
|
|
755
|
+
if (env.APPDATA)
|
|
756
|
+
dirs.push(win32.join(env.APPDATA, "npm"));
|
|
757
|
+
if (env.LOCALAPPDATA)
|
|
758
|
+
dirs.push(win32.join(env.LOCALAPPDATA, "Programs", "claude"));
|
|
759
|
+
return dirs;
|
|
760
|
+
}
|
|
761
|
+
const home = env.HOME;
|
|
762
|
+
if (home) {
|
|
763
|
+
dirs.push(posix.join(home, ".local", "bin"), posix.join(home, ".bun", "bin"), posix.join(home, ".npm-global", "bin"));
|
|
764
|
+
}
|
|
765
|
+
dirs.push("/usr/local/bin", "/opt/homebrew/bin");
|
|
766
|
+
return dirs;
|
|
767
|
+
}
|
|
768
|
+
function resolveTool(name, opts = {}) {
|
|
769
|
+
const env = opts.env ?? process.env;
|
|
770
|
+
const overrideKey = BIN_OVERRIDE_ENV[name];
|
|
771
|
+
const override = opts.override ?? (overrideKey ? env[overrideKey] : undefined);
|
|
772
|
+
const extraDirs = opts.extraDirs ?? wellKnownBinDirs({ ...opts.platform ? { platform: opts.platform } : {}, env });
|
|
773
|
+
return resolveExecutable(name, { ...opts, extraDirs, ...override ? { override } : {} });
|
|
774
|
+
}
|
|
775
|
+
function spawnTargetFor(resolved, args) {
|
|
776
|
+
if (/\.(cmd|bat)$/i.test(resolved)) {
|
|
777
|
+
return { file: process.env.ComSpec ?? "cmd.exe", args: ["/c", resolved, ...args] };
|
|
778
|
+
}
|
|
779
|
+
if (/\.ps1$/i.test(resolved)) {
|
|
780
|
+
return {
|
|
781
|
+
file: "powershell.exe",
|
|
782
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", resolved, ...args]
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
return { file: resolved, args };
|
|
786
|
+
}
|
|
787
|
+
function resolveSpawnTarget(name, args) {
|
|
788
|
+
return spawnTargetFor(resolveTool(name), args);
|
|
789
|
+
}
|
|
790
|
+
function isBareName(cmd) {
|
|
791
|
+
return !cmd.includes("/") && !cmd.includes("\\");
|
|
792
|
+
}
|
|
793
|
+
function explainSpawnFailure(cmd, raw) {
|
|
794
|
+
const detail = raw.trim();
|
|
795
|
+
if (!isBareName(cmd)) {
|
|
796
|
+
return `Could not launch ${cmd}${detail ? ` \u2014 ${detail}` : ""}`;
|
|
797
|
+
}
|
|
798
|
+
const overrideKey = BIN_OVERRIDE_ENV[cmd];
|
|
799
|
+
const lines = [
|
|
800
|
+
`Could not launch \`${cmd}\`: not on the PATH this runcastle server inherited, nor in the usual install locations (~/.local/bin, npm global, ~/.bun/bin).`,
|
|
801
|
+
`A terminal where \`${cmd}\` works does not prove the server can see it: the server's PATH was captured when it started.`,
|
|
802
|
+
`Fix: quit runcastle and start it again from a terminal where \`${cmd} --version\` works.`
|
|
803
|
+
];
|
|
804
|
+
if (overrideKey) {
|
|
805
|
+
lines.push(`If that fails, set ${overrideKey} to the full path (\`where.exe ${cmd}\` / \`which ${cmd}\`) and restart.`);
|
|
806
|
+
}
|
|
807
|
+
if (detail)
|
|
808
|
+
lines.push(`(underlying error: ${detail})`);
|
|
809
|
+
return lines.join(`
|
|
810
|
+
`);
|
|
811
|
+
}
|
|
663
812
|
function resolveExecutable(name, opts = {}) {
|
|
664
813
|
const exists = opts.exists ?? existsSync3;
|
|
665
814
|
if (opts.override && exists(opts.override))
|
|
@@ -667,22 +816,28 @@ function resolveExecutable(name, opts = {}) {
|
|
|
667
816
|
const isWin = (opts.platform ?? process.platform) === "win32";
|
|
668
817
|
const exts = opts.exts ?? (isWin ? WIN_EXTS : [""]);
|
|
669
818
|
const pathEnv = opts.pathEnv ?? process.env.PATH ?? "";
|
|
670
|
-
const dirs = pathEnv.split(isWin ? ";" : ":");
|
|
671
819
|
const join5 = isWin ? win32.join : posix.join;
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
820
|
+
const scan = (dirs) => {
|
|
821
|
+
for (const dir of dirs) {
|
|
822
|
+
if (!dir)
|
|
823
|
+
continue;
|
|
824
|
+
for (const ext of exts) {
|
|
825
|
+
const candidate = join5(dir, `${name}${ext}`);
|
|
826
|
+
if (exists(candidate))
|
|
827
|
+
return candidate;
|
|
828
|
+
}
|
|
679
829
|
}
|
|
680
|
-
|
|
681
|
-
|
|
830
|
+
return null;
|
|
831
|
+
};
|
|
832
|
+
return scan(pathEnv.split(isWin ? ";" : ":")) ?? scan(opts.extraDirs ?? []) ?? name;
|
|
682
833
|
}
|
|
683
|
-
var WIN_EXTS;
|
|
834
|
+
var WIN_EXTS, BIN_OVERRIDE_ENV;
|
|
684
835
|
var init_resolve_executable = __esm(() => {
|
|
685
|
-
WIN_EXTS = [".exe", ".cmd", ".bat", ""];
|
|
836
|
+
WIN_EXTS = [".exe", ".cmd", ".bat", ".ps1", ""];
|
|
837
|
+
BIN_OVERRIDE_ENV = {
|
|
838
|
+
claude: "RUNCASTLE_CLAUDE_BIN",
|
|
839
|
+
node: "RUNCASTLE_NODE_BIN"
|
|
840
|
+
};
|
|
686
841
|
});
|
|
687
842
|
|
|
688
843
|
// src/pty/pty-sidecar.ts
|
|
@@ -699,7 +854,7 @@ function resolveNodeExecutable() {
|
|
|
699
854
|
return override;
|
|
700
855
|
if (!isBun())
|
|
701
856
|
return process.execPath;
|
|
702
|
-
return
|
|
857
|
+
return resolveTool("node", { exts: process.platform === "win32" ? [".exe", ".cmd", ""] : [""] });
|
|
703
858
|
}
|
|
704
859
|
function resolveNodePtyEntry() {
|
|
705
860
|
try {
|
|
@@ -767,11 +922,17 @@ function createSidecarPtySession(cmd, args, opts) {
|
|
|
767
922
|
case "exit":
|
|
768
923
|
fireExit(typeof msg.code === "number" ? msg.code : 0, msg.signal ?? undefined);
|
|
769
924
|
break;
|
|
770
|
-
case "error":
|
|
771
|
-
|
|
925
|
+
case "error": {
|
|
926
|
+
const why = explainSpawnFailure(cmd, msg.message ?? "unknown");
|
|
927
|
+
process.stderr.write(`[pty-sidecar] host error: ${why}
|
|
772
928
|
`);
|
|
929
|
+
emitData(Buffer.from(`\r
|
|
930
|
+
${why.replace(/\n/g, `\r
|
|
931
|
+
`)}\r
|
|
932
|
+
`, "utf8"));
|
|
773
933
|
fireExit(1);
|
|
774
934
|
break;
|
|
935
|
+
}
|
|
775
936
|
}
|
|
776
937
|
}
|
|
777
938
|
});
|
|
@@ -780,8 +941,13 @@ function createSidecarPtySession(cmd, args, opts) {
|
|
|
780
941
|
process.stderr.write(chunk);
|
|
781
942
|
});
|
|
782
943
|
child.on("error", (err) => {
|
|
783
|
-
|
|
944
|
+
const why = explainSpawnFailure(nodeExe, err.message);
|
|
945
|
+
process.stderr.write(`[pty-sidecar] failed to spawn node host: ${why}
|
|
784
946
|
`);
|
|
947
|
+
emitData(Buffer.from(`\r
|
|
948
|
+
${why.replace(/\n/g, `\r
|
|
949
|
+
`)}\r
|
|
950
|
+
`, "utf8"));
|
|
785
951
|
fireExit(1);
|
|
786
952
|
});
|
|
787
953
|
child.on("exit", (code, signal) => {
|
|
@@ -916,14 +1082,19 @@ function createPtySession(cmd, args, opts) {
|
|
|
916
1082
|
}
|
|
917
1083
|
function createNativePtySession(cmd, args, opts) {
|
|
918
1084
|
const pty = loadNodePty();
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1085
|
+
let proc;
|
|
1086
|
+
try {
|
|
1087
|
+
proc = pty.spawn(cmd, args, {
|
|
1088
|
+
name: "xterm-256color",
|
|
1089
|
+
cols: opts.cols ?? 80,
|
|
1090
|
+
rows: opts.rows ?? 24,
|
|
1091
|
+
cwd: opts.cwd,
|
|
1092
|
+
env: cleanEnv(opts.env),
|
|
1093
|
+
useConpty: opts.useConpty ?? true
|
|
1094
|
+
});
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
throw new Error(explainSpawnFailure(cmd, err instanceof Error ? err.message : String(err)));
|
|
1097
|
+
}
|
|
927
1098
|
let killed = false;
|
|
928
1099
|
proc.onExit(() => {
|
|
929
1100
|
killed = true;
|
|
@@ -958,6 +1129,7 @@ function createNativePtySession(cmd, args, opts) {
|
|
|
958
1129
|
}
|
|
959
1130
|
var cached = null, BACKEND_LOGGED;
|
|
960
1131
|
var init_pty = __esm(() => {
|
|
1132
|
+
init_resolve_executable();
|
|
961
1133
|
init_pty_sidecar();
|
|
962
1134
|
BACKEND_LOGGED = Symbol.for("runcastle.pty.backend.logged");
|
|
963
1135
|
});
|
|
@@ -1239,7 +1411,11 @@ function rowToProject(row) {
|
|
|
1239
1411
|
repoPath: row.repoPath,
|
|
1240
1412
|
mainBranch: row.mainBranch,
|
|
1241
1413
|
devCommand: row.devCommand ?? undefined,
|
|
1242
|
-
model: row.model ?? undefined
|
|
1414
|
+
model: row.model ?? undefined,
|
|
1415
|
+
setupCommand: row.setupCommand ?? undefined,
|
|
1416
|
+
verifyCommands: row.verifyCommands ?? undefined,
|
|
1417
|
+
knownFailures: row.knownFailures ?? undefined,
|
|
1418
|
+
dbResetCommand: row.dbResetCommand ?? undefined
|
|
1243
1419
|
};
|
|
1244
1420
|
}
|
|
1245
1421
|
function rowToRun(row) {
|
|
@@ -1571,34 +1747,159 @@ function writeKickoffSequence(line, io, submitDelayMs = KICKOFF_SUBMIT_DELAY_MS)
|
|
|
1571
1747
|
}, submitDelayMs);
|
|
1572
1748
|
submit.unref?.();
|
|
1573
1749
|
}
|
|
1574
|
-
function
|
|
1575
|
-
const
|
|
1576
|
-
|
|
1577
|
-
|
|
1750
|
+
function stopTimers(d) {
|
|
1751
|
+
for (const t of d.timers)
|
|
1752
|
+
clearTimeout(t);
|
|
1753
|
+
d.timers.clear();
|
|
1754
|
+
}
|
|
1755
|
+
function forgetKickoff(sessionId) {
|
|
1756
|
+
const d = deliveries.get(sessionId);
|
|
1757
|
+
if (d)
|
|
1758
|
+
stopTimers(d);
|
|
1759
|
+
deliveries.delete(sessionId);
|
|
1760
|
+
pendingKickoffOverrides.delete(sessionId);
|
|
1761
|
+
}
|
|
1762
|
+
function ptyIo(sessionId) {
|
|
1763
|
+
return {
|
|
1764
|
+
write: (data) => ptyRegistry().get(sessionId)?.pty.write(data),
|
|
1765
|
+
alive: () => {
|
|
1766
|
+
const entry = ptyRegistry().get(sessionId);
|
|
1767
|
+
return !!entry && !entry.exited;
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
function track(d, timer) {
|
|
1772
|
+
timer.unref?.();
|
|
1773
|
+
d.timers.add(timer);
|
|
1774
|
+
}
|
|
1775
|
+
function attemptKickoff(ctx, session, d, delayMs, clearFirst = false) {
|
|
1776
|
+
track(d, setTimeout(() => {
|
|
1777
|
+
if (d.settled)
|
|
1778
|
+
return;
|
|
1779
|
+
const io = ptyIo(session.id);
|
|
1780
|
+
if (!io.alive()) {
|
|
1781
|
+
d.settled = true;
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
d.attempts += 1;
|
|
1785
|
+
const attempt = d.attempts;
|
|
1578
1786
|
try {
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
data: { sessionId: session.id, kind: session.kind }
|
|
1590
|
-
});
|
|
1591
|
-
}
|
|
1787
|
+
if (clearFirst)
|
|
1788
|
+
io.write(CLEAR_INPUT);
|
|
1789
|
+
writeKickoffSequence(d.line, {
|
|
1790
|
+
write: io.write,
|
|
1791
|
+
alive: io.alive,
|
|
1792
|
+
onSubmitted: () => emit(ctx, session.featureId, {
|
|
1793
|
+
type: "session.kickoff",
|
|
1794
|
+
message: attempt === 1 ? `${session.kind} session kicked off automatically` : `${session.kind} kickoff re-sent (attempt ${attempt}) \u2014 the first was never acknowledged`,
|
|
1795
|
+
data: { sessionId: session.id, kind: session.kind, attempt }
|
|
1796
|
+
})
|
|
1592
1797
|
});
|
|
1593
1798
|
} catch {}
|
|
1594
|
-
|
|
1799
|
+
armConfirmation(ctx, session, d);
|
|
1800
|
+
}, delayMs));
|
|
1801
|
+
}
|
|
1802
|
+
function armConfirmation(ctx, session, d) {
|
|
1803
|
+
track(d, setTimeout(() => {
|
|
1804
|
+
if (d.settled || d.confirmed)
|
|
1805
|
+
return;
|
|
1806
|
+
if (d.attempts < KICKOFF_MAX_ATTEMPTS && ptyIo(session.id).alive()) {
|
|
1807
|
+
attemptKickoff(ctx, session, d, 0, true);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
settleUndelivered(ctx, session, d, "unacknowledged");
|
|
1811
|
+
}, KICKOFF_CONFIRM_MS));
|
|
1812
|
+
}
|
|
1813
|
+
function settleUndelivered(ctx, session, d, reason) {
|
|
1814
|
+
d.settled = true;
|
|
1815
|
+
stopTimers(d);
|
|
1816
|
+
emit(ctx, session.featureId, {
|
|
1817
|
+
type: "session.kickoff_undelivered",
|
|
1818
|
+
message: reason === "superseded" ? `the ${session.kind} briefing was never delivered \u2014 you typed first, so runcastle stopped injecting it` : `the ${session.kind} briefing was typed ${d.attempts}\xD7 but Claude Code never acknowledged it \u2014 send it again from the session strip`,
|
|
1819
|
+
data: { sessionId: session.id, kind: session.kind, reason, attempts: d.attempts, line: d.line }
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
function noteKickoffPrompt(ctx, sessionId, prompt) {
|
|
1823
|
+
const d = deliveries.get(sessionId);
|
|
1824
|
+
if (!d || d.confirmed)
|
|
1825
|
+
return;
|
|
1826
|
+
const session = getSessionRow(ctx, sessionId);
|
|
1827
|
+
if (!session)
|
|
1828
|
+
return;
|
|
1829
|
+
if (promptMatchesKickoff(d.line, prompt)) {
|
|
1830
|
+
d.confirmed = true;
|
|
1831
|
+
d.settled = true;
|
|
1832
|
+
stopTimers(d);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if (d.settled)
|
|
1836
|
+
return;
|
|
1837
|
+
settleUndelivered(ctx, session, d, "superseded");
|
|
1838
|
+
}
|
|
1839
|
+
function promptMatchesKickoff(line, prompt) {
|
|
1840
|
+
if (!prompt)
|
|
1841
|
+
return false;
|
|
1842
|
+
const norm = (s) => s.replace(/\s+/g, " ").trim().toLowerCase();
|
|
1843
|
+
const want = norm(line);
|
|
1844
|
+
const got = norm(prompt);
|
|
1845
|
+
if (!want || !got)
|
|
1846
|
+
return false;
|
|
1847
|
+
return got.includes(want.slice(0, MATCH_PREFIX));
|
|
1848
|
+
}
|
|
1849
|
+
function resendKickoff(ctx, sessionId) {
|
|
1850
|
+
const session = getSessionRow(ctx, sessionId);
|
|
1851
|
+
if (!session)
|
|
1852
|
+
throw new Error(`unknown session ${sessionId}`);
|
|
1853
|
+
if (session.status === "ended")
|
|
1854
|
+
throw new Error("that session has ended \u2014 open a new terminal");
|
|
1855
|
+
if (!ptyIo(sessionId).alive())
|
|
1856
|
+
throw new Error("the terminal for that session is no longer running");
|
|
1857
|
+
const existing = deliveries.get(sessionId);
|
|
1858
|
+
const d = existing ?? {
|
|
1859
|
+
line: kickoffLineFor(session.kind, pendingKickoffOverrides.get(sessionId)),
|
|
1860
|
+
attempts: 0,
|
|
1861
|
+
confirmed: false,
|
|
1862
|
+
settled: false,
|
|
1863
|
+
timers: new Set
|
|
1864
|
+
};
|
|
1865
|
+
stopTimers(d);
|
|
1866
|
+
d.attempts = 0;
|
|
1867
|
+
d.confirmed = false;
|
|
1868
|
+
d.settled = false;
|
|
1869
|
+
deliveries.set(sessionId, d);
|
|
1870
|
+
attemptKickoff(ctx, session, d, 0, true);
|
|
1871
|
+
return { line: d.line };
|
|
1872
|
+
}
|
|
1873
|
+
function armSessionReadyWatchdog(ctx, session) {
|
|
1874
|
+
const timer = setTimeout(() => {
|
|
1875
|
+
const row = getSessionRow(ctx, session.id);
|
|
1876
|
+
if (!row || row.status !== "launching")
|
|
1877
|
+
return;
|
|
1878
|
+
if (!ptyIo(session.id).alive())
|
|
1879
|
+
return;
|
|
1880
|
+
emit(ctx, session.featureId, {
|
|
1881
|
+
type: "session.not_ready",
|
|
1882
|
+
message: `the ${session.kind} terminal is open but Claude Code has not reported ready \u2014 check it for a prompt or dialog waiting on you, then send the briefing`,
|
|
1883
|
+
data: { sessionId: session.id, kind: session.kind }
|
|
1884
|
+
});
|
|
1885
|
+
}, SESSION_READY_TIMEOUT_MS);
|
|
1595
1886
|
timer.unref?.();
|
|
1596
1887
|
}
|
|
1888
|
+
function scheduleKickoff(ctx, session) {
|
|
1889
|
+
const line = kickoffLineFor(session.kind, pendingKickoffOverrides.get(session.id));
|
|
1890
|
+
pendingKickoffOverrides.delete(session.id);
|
|
1891
|
+
const existing = deliveries.get(session.id);
|
|
1892
|
+
if (existing)
|
|
1893
|
+
stopTimers(existing);
|
|
1894
|
+
const d = { line, attempts: 0, confirmed: false, settled: false, timers: new Set };
|
|
1895
|
+
deliveries.set(session.id, d);
|
|
1896
|
+
attemptKickoff(ctx, session, d, KICKOFF_DELAY_MS);
|
|
1897
|
+
}
|
|
1597
1898
|
function markSessionEnded(ctx, id) {
|
|
1598
1899
|
const existing = getSessionRow(ctx, id);
|
|
1599
1900
|
if (!existing)
|
|
1600
1901
|
return null;
|
|
1601
|
-
|
|
1902
|
+
forgetKickoff(id);
|
|
1602
1903
|
ctx.db.update(sessions).set({ status: "ended" }).where(eq4(sessions.id, id)).run();
|
|
1603
1904
|
return getSessionRow(ctx, id);
|
|
1604
1905
|
}
|
|
@@ -1610,7 +1911,7 @@ function mostRecentLiveSession(ctx) {
|
|
|
1610
1911
|
const row = ctx.db.select().from(sessions).where(eq4(sessions.status, "live")).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1611
1912
|
return row ? rowToSession(row) : null;
|
|
1612
1913
|
}
|
|
1613
|
-
var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides;
|
|
1914
|
+
var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, KICKOFF_CONFIRM_MS = 12000, KICKOFF_MAX_ATTEMPTS = 3, CLEAR_INPUT = "\x15", SESSION_READY_TIMEOUT_MS = 25000, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides, deliveries, MATCH_PREFIX = 40;
|
|
1614
1915
|
var init_sessions = __esm(() => {
|
|
1615
1916
|
init_src();
|
|
1616
1917
|
init_schema();
|
|
@@ -1628,6 +1929,7 @@ var init_sessions = __esm(() => {
|
|
|
1628
1929
|
};
|
|
1629
1930
|
RESUME_KICKOFF_PREFIX = "We are picking this conversation back up \u2014 runcastle restarted and closed the terminal, " + "but this conversation is intact. Do NOT start over: tell me in one or two lines where we " + "left off and what is next, then carry on. Your original instruction was: ";
|
|
1630
1931
|
pendingKickoffOverrides = new Map;
|
|
1932
|
+
deliveries = new Map;
|
|
1631
1933
|
});
|
|
1632
1934
|
|
|
1633
1935
|
// src/launcher/reconcile.ts
|
|
@@ -1663,315 +1965,93 @@ var init_reconcile = __esm(() => {
|
|
|
1663
1965
|
init_sessions();
|
|
1664
1966
|
});
|
|
1665
1967
|
|
|
1666
|
-
// src/launcher/
|
|
1667
|
-
function setRuntimeCtx(ctx) {
|
|
1668
|
-
current = ctx;
|
|
1669
|
-
}
|
|
1670
|
-
async function getRuntimeCtx() {
|
|
1671
|
-
if (current)
|
|
1672
|
-
return current;
|
|
1673
|
-
const [{ createDb: createDb2 }, { dbPath: dbPath2 }, { loadConfig: loadConfig2 }] = await Promise.all([
|
|
1674
|
-
Promise.resolve().then(() => (init_client(), exports_client)),
|
|
1675
|
-
Promise.resolve().then(() => (init_paths(), exports_paths)),
|
|
1676
|
-
Promise.resolve().then(() => (init_config_load(), exports_config_load))
|
|
1677
|
-
]);
|
|
1678
|
-
const db = createDb2(dbPath2());
|
|
1679
|
-
current = { db, config: loadConfig2() };
|
|
1680
|
-
return current;
|
|
1681
|
-
}
|
|
1682
|
-
var current = null;
|
|
1683
|
-
|
|
1684
|
-
// src/services/feature-docs.ts
|
|
1968
|
+
// src/launcher/skills-root.ts
|
|
1685
1969
|
import { existsSync as existsSync5 } from "fs";
|
|
1686
|
-
import { join as join5 } from "path";
|
|
1687
|
-
function
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1970
|
+
import { dirname, join as join5, resolve as resolve3 } from "path";
|
|
1971
|
+
function resolveSkillsRoot(fromDir) {
|
|
1972
|
+
const override = process.env[SKILLS_DIR_ENV];
|
|
1973
|
+
if (override) {
|
|
1974
|
+
const root = resolve3(override);
|
|
1975
|
+
if (existsSync5(join5(root, SKILLS_MARKER)))
|
|
1976
|
+
return root;
|
|
1977
|
+
throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
|
|
1978
|
+
}
|
|
1979
|
+
const searched = [];
|
|
1980
|
+
let dir = fromDir;
|
|
1981
|
+
for (let i = 0;i < 8; i += 1) {
|
|
1982
|
+
const candidate = join5(dir, "packages", "skills");
|
|
1983
|
+
searched.push(candidate);
|
|
1984
|
+
if (existsSync5(join5(candidate, SKILLS_MARKER)))
|
|
1985
|
+
return candidate;
|
|
1986
|
+
const parent = dirname(dir);
|
|
1987
|
+
if (parent === dir)
|
|
1988
|
+
break;
|
|
1989
|
+
dir = parent;
|
|
1990
|
+
}
|
|
1991
|
+
throw new Error(`runcastle skills root not found; searched:
|
|
1992
|
+
${searched.join(`
|
|
1993
|
+
`)}`);
|
|
1695
1994
|
}
|
|
1696
|
-
var
|
|
1697
|
-
|
|
1995
|
+
var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
|
|
1996
|
+
var init_skills_root = __esm(() => {
|
|
1997
|
+
SKILLS_MARKER = join5("packs", "runcastle");
|
|
1698
1998
|
});
|
|
1699
1999
|
|
|
1700
|
-
// src/
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
return {
|
|
1704
|
-
id: row.id,
|
|
1705
|
-
featureId: row.featureId,
|
|
1706
|
-
seq: row.seq,
|
|
1707
|
-
title: row.title,
|
|
1708
|
-
goal: row.goal,
|
|
1709
|
-
context: row.context,
|
|
1710
|
-
acceptanceCriteria: row.acceptanceCriteria,
|
|
1711
|
-
seams: row.seams,
|
|
1712
|
-
blockedBy: row.blockedBy,
|
|
1713
|
-
status: row.status,
|
|
1714
|
-
commits: row.commits,
|
|
1715
|
-
error: row.error ?? undefined,
|
|
1716
|
-
attemptBranch: row.attemptBranch ?? undefined,
|
|
1717
|
-
conflictFiles: row.conflictFiles ?? undefined
|
|
1718
|
-
};
|
|
2000
|
+
// src/pty/dev-pane.ts
|
|
2001
|
+
function drivePaneId(featureId) {
|
|
2002
|
+
return `${DRIVE_PREFIX}${featureId}`;
|
|
1719
2003
|
}
|
|
1720
|
-
function
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
} catch (e) {
|
|
1724
|
-
if (e instanceof BlockingEdgeError)
|
|
1725
|
-
throw new InvalidInputError(e.message);
|
|
1726
|
-
throw e;
|
|
1727
|
-
}
|
|
2004
|
+
function sniffDevUrl(text2) {
|
|
2005
|
+
const m = text2.match(/https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/[^\s"'<>`\x1b]*)?/i);
|
|
2006
|
+
return m ? m[0] : undefined;
|
|
1728
2007
|
}
|
|
1729
|
-
function
|
|
1730
|
-
|
|
2008
|
+
function devSpawnTarget(devCommand) {
|
|
2009
|
+
if (process.platform === "win32") {
|
|
2010
|
+
return { file: process.env.ComSpec ?? "cmd.exe", args: ["/d", "/s", "/c", devCommand] };
|
|
2011
|
+
}
|
|
2012
|
+
return { file: "/bin/sh", args: ["-c", devCommand] };
|
|
1731
2013
|
}
|
|
1732
|
-
function
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
const
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
2014
|
+
function startDevPane(input) {
|
|
2015
|
+
const { ctx, featureId, repoPath, devCommand, onUrl } = input;
|
|
2016
|
+
const paneId = drivePaneId(featureId);
|
|
2017
|
+
const { file, args } = devSpawnTarget(devCommand);
|
|
2018
|
+
try {
|
|
2019
|
+
ptyRegistry().create({
|
|
2020
|
+
sessionId: paneId,
|
|
2021
|
+
cmd: file,
|
|
2022
|
+
args,
|
|
2023
|
+
opts: { cwd: repoPath, env: process.env, cols: 80, rows: 24, useConpty: true }
|
|
2024
|
+
});
|
|
2025
|
+
} catch (err) {
|
|
2026
|
+
emit(ctx, featureId, {
|
|
2027
|
+
type: "testdrive.dev_failed",
|
|
2028
|
+
message: `dev server failed to start: ${err instanceof Error ? err.message : String(err)}`,
|
|
2029
|
+
data: { paneId }
|
|
2030
|
+
});
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
let scanned = "";
|
|
2034
|
+
let found = false;
|
|
2035
|
+
const sniffer = {
|
|
2036
|
+
sendData(chunk) {
|
|
2037
|
+
if (found)
|
|
2038
|
+
return;
|
|
2039
|
+
scanned = (scanned + chunk.toString("utf8")).slice(-SNIFF_WINDOW);
|
|
2040
|
+
const url = sniffDevUrl(scanned);
|
|
2041
|
+
if (url) {
|
|
2042
|
+
found = true;
|
|
2043
|
+
onUrl(url);
|
|
2044
|
+
}
|
|
2045
|
+
},
|
|
2046
|
+
sendControl() {}
|
|
2047
|
+
};
|
|
2048
|
+
ptyRegistry().attach(paneId, sniffer);
|
|
1755
2049
|
emit(ctx, featureId, {
|
|
1756
|
-
type: "
|
|
1757
|
-
message:
|
|
1758
|
-
data: {
|
|
2050
|
+
type: "testdrive.dev_started",
|
|
2051
|
+
message: `dev server spawned: ${devCommand}`,
|
|
2052
|
+
data: { paneId }
|
|
1759
2053
|
});
|
|
1760
|
-
return
|
|
1761
|
-
}
|
|
1762
|
-
function getTicket(ctx, id) {
|
|
1763
|
-
const row = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
1764
|
-
if (!row)
|
|
1765
|
-
throw new NotFoundError(`ticket ${id} not found`);
|
|
1766
|
-
return rowToTicket(row);
|
|
1767
|
-
}
|
|
1768
|
-
function assertMutable(t, verb) {
|
|
1769
|
-
if (!MUTABLE_STATUSES.has(t.status)) {
|
|
1770
|
-
throw new InvalidInputError(`cannot ${verb} ticket ${t.seq} \u2014 it is ${t.status}; only pending or failed tickets can be ${verb}${verb.endsWith("l") ? "led" : "ed"}`);
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
function editTicket(ctx, id, patch) {
|
|
1774
|
-
const current2 = getTicket(ctx, id);
|
|
1775
|
-
assertMutable(current2, "edit");
|
|
1776
|
-
const set = {};
|
|
1777
|
-
if (patch.title !== undefined)
|
|
1778
|
-
set.title = patch.title;
|
|
1779
|
-
if (patch.goal !== undefined)
|
|
1780
|
-
set.goal = patch.goal;
|
|
1781
|
-
if (patch.context !== undefined)
|
|
1782
|
-
set.context = patch.context;
|
|
1783
|
-
if (patch.acceptanceCriteria !== undefined)
|
|
1784
|
-
set.acceptanceCriteria = patch.acceptanceCriteria;
|
|
1785
|
-
if (patch.seams !== undefined)
|
|
1786
|
-
set.seams = patch.seams;
|
|
1787
|
-
if (Object.keys(set).length === 0) {
|
|
1788
|
-
throw new InvalidInputError("nothing to edit \u2014 pass at least one content field");
|
|
1789
|
-
}
|
|
1790
|
-
ctx.db.update(tickets).set(set).where(eq5(tickets.id, id)).run();
|
|
1791
|
-
emit(ctx, current2.featureId, {
|
|
1792
|
-
type: "ticket.edited",
|
|
1793
|
-
message: `ticket ${current2.seq} content edited (${Object.keys(set).join(", ")})`,
|
|
1794
|
-
ticketId: id,
|
|
1795
|
-
data: { fields: Object.keys(set) }
|
|
1796
|
-
});
|
|
1797
|
-
return getTicket(ctx, id);
|
|
1798
|
-
}
|
|
1799
|
-
function cancelTicket(ctx, id, reason) {
|
|
1800
|
-
const current2 = getTicket(ctx, id);
|
|
1801
|
-
assertMutable(current2, "cancel");
|
|
1802
|
-
ctx.db.update(tickets).set({ status: "cancelled", error: reason?.trim() ? reason.trim() : null }).where(eq5(tickets.id, id)).run();
|
|
1803
|
-
emit(ctx, current2.featureId, {
|
|
1804
|
-
type: "ticket.cancelled",
|
|
1805
|
-
message: `ticket ${current2.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
|
|
1806
|
-
ticketId: id,
|
|
1807
|
-
data: { reason: reason ?? null }
|
|
1808
|
-
});
|
|
1809
|
-
return getTicket(ctx, id);
|
|
1810
|
-
}
|
|
1811
|
-
function updateTicket(ctx, id, patch) {
|
|
1812
|
-
const current2 = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
1813
|
-
if (!current2)
|
|
1814
|
-
throw new NotFoundError(`ticket ${id} not found`);
|
|
1815
|
-
const set = {};
|
|
1816
|
-
if (patch.status !== undefined)
|
|
1817
|
-
set.status = patch.status;
|
|
1818
|
-
if (patch.commits !== undefined)
|
|
1819
|
-
set.commits = patch.commits;
|
|
1820
|
-
if (patch.error !== undefined)
|
|
1821
|
-
set.error = patch.error;
|
|
1822
|
-
if (patch.attemptBranch !== undefined)
|
|
1823
|
-
set.attemptBranch = patch.attemptBranch;
|
|
1824
|
-
if (patch.conflictFiles !== undefined)
|
|
1825
|
-
set.conflictFiles = patch.conflictFiles;
|
|
1826
|
-
ctx.db.update(tickets).set(set).where(eq5(tickets.id, id)).run();
|
|
1827
|
-
emit(ctx, current2.featureId, {
|
|
1828
|
-
type: "ticket.updated",
|
|
1829
|
-
message: `ticket ${current2.seq} \u2192 ${patch.status ?? current2.status}`,
|
|
1830
|
-
ticketId: id,
|
|
1831
|
-
data: patch
|
|
1832
|
-
});
|
|
1833
|
-
const updated = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
1834
|
-
return rowToTicket(updated);
|
|
1835
|
-
}
|
|
1836
|
-
var MUTABLE_STATUSES;
|
|
1837
|
-
var init_tickets = __esm(() => {
|
|
1838
|
-
init_src();
|
|
1839
|
-
init_schema();
|
|
1840
|
-
init_errors();
|
|
1841
|
-
init_events();
|
|
1842
|
-
MUTABLE_STATUSES = new Set(["pending", "failed"]);
|
|
1843
|
-
});
|
|
1844
|
-
|
|
1845
|
-
// src/services/gates.ts
|
|
1846
|
-
import { existsSync as existsSync6 } from "fs";
|
|
1847
|
-
function checkGate(ctx, check, feature) {
|
|
1848
|
-
switch (check) {
|
|
1849
|
-
case "decisions-file-exists":
|
|
1850
|
-
return fileGate(ctx, feature, "decisions.md", "run the ideation session to capture decisions first");
|
|
1851
|
-
case "all-waypoints-terminal": {
|
|
1852
|
-
const wps = listByFeature(ctx, feature.id);
|
|
1853
|
-
if (wps.length === 0) {
|
|
1854
|
-
return { satisfied: false, reason: "no waypoints charted yet \u2014 chart the map before converging" };
|
|
1855
|
-
}
|
|
1856
|
-
const open = wps.filter((w) => w.status !== "resolved" && w.status !== "dropped");
|
|
1857
|
-
return open.length === 0 ? { satisfied: true } : {
|
|
1858
|
-
satisfied: false,
|
|
1859
|
-
reason: notYetTerminal("waypoint", open.map((w) => w.status), ["open", "claimed"])
|
|
1860
|
-
};
|
|
1861
|
-
}
|
|
1862
|
-
case "spec-file-exists":
|
|
1863
|
-
return fileGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
|
|
1864
|
-
case "tickets-approved": {
|
|
1865
|
-
const count = listByFeature2(ctx, feature.id).filter((t) => t.status !== "cancelled").length;
|
|
1866
|
-
return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
|
|
1867
|
-
}
|
|
1868
|
-
case "all-tickets-terminal": {
|
|
1869
|
-
const tickets2 = listByFeature2(ctx, feature.id);
|
|
1870
|
-
if (tickets2.length === 0)
|
|
1871
|
-
return { satisfied: false, reason: "no tickets have run yet" };
|
|
1872
|
-
const open = tickets2.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
1873
|
-
return open.length === 0 ? { satisfied: true } : {
|
|
1874
|
-
satisfied: false,
|
|
1875
|
-
reason: notYetTerminal("ticket", open.map((t) => t.status), ["pending", "burning"])
|
|
1876
|
-
};
|
|
1877
|
-
}
|
|
1878
|
-
case "human-merge":
|
|
1879
|
-
return { satisfied: false, reason: "use the Merge button to ship" };
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
function notYetTerminal(noun, statuses, order) {
|
|
1883
|
-
const counts = new Map;
|
|
1884
|
-
for (const s of statuses)
|
|
1885
|
-
counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
1886
|
-
const ordered = [
|
|
1887
|
-
...order.filter((s) => counts.has(s)),
|
|
1888
|
-
...[...counts.keys()].filter((s) => !order.includes(s))
|
|
1889
|
-
];
|
|
1890
|
-
const breakdown = ordered.map((s) => `${counts.get(s)} ${s}`).join(", ");
|
|
1891
|
-
return `${statuses.length} ${statuses.length === 1 ? noun : `${noun}s`} not yet terminal (${breakdown})`;
|
|
1892
|
-
}
|
|
1893
|
-
function fileGate(ctx, feature, fileName, reason) {
|
|
1894
|
-
const project = projectForFeature(ctx, feature);
|
|
1895
|
-
return existsSync6(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
|
|
1896
|
-
}
|
|
1897
|
-
function overrideGate(ctx, featureId, gate, reason) {
|
|
1898
|
-
const feature = getFeatureRow(ctx, featureId);
|
|
1899
|
-
ctx.db.insert(gateOverrides).values({ featureId, gate, reason, ts: Date.now() }).run();
|
|
1900
|
-
emit(ctx, featureId, {
|
|
1901
|
-
type: "gate.overridden",
|
|
1902
|
-
message: `gate ${gate} overridden: ${reason}`,
|
|
1903
|
-
data: { gate, reason }
|
|
1904
|
-
});
|
|
1905
|
-
const next = nextPhase(feature);
|
|
1906
|
-
if (next)
|
|
1907
|
-
return setPhase(ctx, featureId, next, "phase.advanced", `advanced to ${next} (override)`);
|
|
1908
|
-
return feature;
|
|
1909
|
-
}
|
|
1910
|
-
var init_gates = __esm(() => {
|
|
1911
|
-
init_src();
|
|
1912
|
-
init_schema();
|
|
1913
|
-
init_events();
|
|
1914
|
-
init_feature_docs();
|
|
1915
|
-
init_repo();
|
|
1916
|
-
init_tickets();
|
|
1917
|
-
init_waypoints();
|
|
1918
|
-
});
|
|
1919
|
-
|
|
1920
|
-
// src/pty/dev-pane.ts
|
|
1921
|
-
function drivePaneId(featureId) {
|
|
1922
|
-
return `${DRIVE_PREFIX}${featureId}`;
|
|
1923
|
-
}
|
|
1924
|
-
function sniffDevUrl(text2) {
|
|
1925
|
-
const m = text2.match(/https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/[^\s"'<>`\x1b]*)?/i);
|
|
1926
|
-
return m ? m[0] : undefined;
|
|
1927
|
-
}
|
|
1928
|
-
function devSpawnTarget(devCommand) {
|
|
1929
|
-
if (process.platform === "win32") {
|
|
1930
|
-
return { file: process.env.ComSpec ?? "cmd.exe", args: ["/d", "/s", "/c", devCommand] };
|
|
1931
|
-
}
|
|
1932
|
-
return { file: "/bin/sh", args: ["-c", devCommand] };
|
|
1933
|
-
}
|
|
1934
|
-
function startDevPane(input) {
|
|
1935
|
-
const { ctx, featureId, repoPath, devCommand, onUrl } = input;
|
|
1936
|
-
const paneId = drivePaneId(featureId);
|
|
1937
|
-
const { file, args } = devSpawnTarget(devCommand);
|
|
1938
|
-
try {
|
|
1939
|
-
ptyRegistry().create({
|
|
1940
|
-
sessionId: paneId,
|
|
1941
|
-
cmd: file,
|
|
1942
|
-
args,
|
|
1943
|
-
opts: { cwd: repoPath, env: process.env, cols: 80, rows: 24, useConpty: true }
|
|
1944
|
-
});
|
|
1945
|
-
} catch (err) {
|
|
1946
|
-
emit(ctx, featureId, {
|
|
1947
|
-
type: "testdrive.dev_failed",
|
|
1948
|
-
message: `dev server failed to start: ${err instanceof Error ? err.message : String(err)}`,
|
|
1949
|
-
data: { paneId }
|
|
1950
|
-
});
|
|
1951
|
-
return;
|
|
1952
|
-
}
|
|
1953
|
-
let scanned = "";
|
|
1954
|
-
let found = false;
|
|
1955
|
-
const sniffer = {
|
|
1956
|
-
sendData(chunk) {
|
|
1957
|
-
if (found)
|
|
1958
|
-
return;
|
|
1959
|
-
scanned = (scanned + chunk.toString("utf8")).slice(-SNIFF_WINDOW);
|
|
1960
|
-
const url = sniffDevUrl(scanned);
|
|
1961
|
-
if (url) {
|
|
1962
|
-
found = true;
|
|
1963
|
-
onUrl(url);
|
|
1964
|
-
}
|
|
1965
|
-
},
|
|
1966
|
-
sendControl() {}
|
|
1967
|
-
};
|
|
1968
|
-
ptyRegistry().attach(paneId, sniffer);
|
|
1969
|
-
emit(ctx, featureId, {
|
|
1970
|
-
type: "testdrive.dev_started",
|
|
1971
|
-
message: `dev server spawned: ${devCommand}`,
|
|
1972
|
-
data: { paneId }
|
|
1973
|
-
});
|
|
1974
|
-
return paneId;
|
|
2054
|
+
return paneId;
|
|
1975
2055
|
}
|
|
1976
2056
|
function stopDevPane(paneId) {
|
|
1977
2057
|
const reg = ptyRegistry();
|
|
@@ -1991,10 +2071,13 @@ var init_dev_pane = __esm(() => {
|
|
|
1991
2071
|
});
|
|
1992
2072
|
|
|
1993
2073
|
// src/services/git.ts
|
|
1994
|
-
import { existsSync as
|
|
2074
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, mkdtempSync, realpathSync, rmSync } from "fs";
|
|
1995
2075
|
import { tmpdir } from "os";
|
|
1996
|
-
import { dirname, join as join6, resolve as
|
|
2076
|
+
import { dirname as dirname2, join as join6, resolve as resolve4 } from "path";
|
|
1997
2077
|
import { simpleGit } from "simple-git";
|
|
2078
|
+
function migrationPaths(paths) {
|
|
2079
|
+
return paths.filter((p) => MIGRATION_DIR_RE.test(p) || DRIZZLE_SQL_RE.test(p));
|
|
2080
|
+
}
|
|
1998
2081
|
function featureBranch(slug) {
|
|
1999
2082
|
return `feature/${slug}`;
|
|
2000
2083
|
}
|
|
@@ -2005,14 +2088,14 @@ function errMsg(e) {
|
|
|
2005
2088
|
return e instanceof Error ? e.message : String(e);
|
|
2006
2089
|
}
|
|
2007
2090
|
function canon(p) {
|
|
2008
|
-
let abs =
|
|
2091
|
+
let abs = resolve4(p);
|
|
2009
2092
|
try {
|
|
2010
2093
|
abs = realpathSync.native(abs);
|
|
2011
2094
|
} catch {}
|
|
2012
2095
|
return process.platform === "win32" ? abs.replace(/\\/g, "/").toLowerCase() : abs;
|
|
2013
2096
|
}
|
|
2014
2097
|
async function assertRepo(repoPath) {
|
|
2015
|
-
if (!
|
|
2098
|
+
if (!existsSync6(repoPath)) {
|
|
2016
2099
|
throw new InvalidInputError(`path does not exist: ${repoPath}`);
|
|
2017
2100
|
}
|
|
2018
2101
|
let isRepo = false;
|
|
@@ -2101,7 +2184,7 @@ async function ensureTalkWorktree(project, feature) {
|
|
|
2101
2184
|
await ensureBranchExists(g, branch, feature.baseBranch ?? project.mainBranch);
|
|
2102
2185
|
if (await worktreeIsValid(g, worktreePath, branch))
|
|
2103
2186
|
return worktreePath;
|
|
2104
|
-
mkdirSync2(
|
|
2187
|
+
mkdirSync2(dirname2(worktreePath), { recursive: true });
|
|
2105
2188
|
try {
|
|
2106
2189
|
await g.raw(["worktree", "add", worktreePath, branch]);
|
|
2107
2190
|
return worktreePath;
|
|
@@ -2124,7 +2207,7 @@ async function ensureBranchExists(g, branch, from) {
|
|
|
2124
2207
|
}
|
|
2125
2208
|
}
|
|
2126
2209
|
async function worktreeIsValid(g, worktreePath, branch) {
|
|
2127
|
-
if (!
|
|
2210
|
+
if (!existsSync6(worktreePath))
|
|
2128
2211
|
return false;
|
|
2129
2212
|
const registered = await registeredWorktrees(g);
|
|
2130
2213
|
if (!registered.has(canon(worktreePath)))
|
|
@@ -2170,7 +2253,7 @@ async function worktreesOnBranch(g, branch, mainRepoPath) {
|
|
|
2170
2253
|
return (await listWorktrees(g)).filter((e) => e.branch === branch && canon(e.path) !== main).map((e) => e.path);
|
|
2171
2254
|
}
|
|
2172
2255
|
async function detachWorktree(path) {
|
|
2173
|
-
if (!
|
|
2256
|
+
if (!existsSync6(path))
|
|
2174
2257
|
return false;
|
|
2175
2258
|
const g = git(path);
|
|
2176
2259
|
let head;
|
|
@@ -2185,12 +2268,27 @@ async function detachWorktree(path) {
|
|
|
2185
2268
|
return true;
|
|
2186
2269
|
}
|
|
2187
2270
|
async function reattachWorktree(path, branch) {
|
|
2188
|
-
if (!
|
|
2271
|
+
if (!existsSync6(path))
|
|
2189
2272
|
return;
|
|
2190
2273
|
try {
|
|
2191
2274
|
await git(path).checkout(branch);
|
|
2192
2275
|
} catch {}
|
|
2193
2276
|
}
|
|
2277
|
+
function prepBranchName(unique) {
|
|
2278
|
+
return `${PREP_BRANCH_PREFIX}${unique}`;
|
|
2279
|
+
}
|
|
2280
|
+
async function readFileAtRef(repoPath, ref, path) {
|
|
2281
|
+
try {
|
|
2282
|
+
return await git(repoPath).raw(["show", `${ref}:${path}`]);
|
|
2283
|
+
} catch {
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
async function deletePrepBranch(repoPath, branch) {
|
|
2288
|
+
try {
|
|
2289
|
+
await deleteBranchDetachingWorktrees(git(repoPath), repoPath, branch);
|
|
2290
|
+
} catch {}
|
|
2291
|
+
}
|
|
2194
2292
|
function tempBranchSlugSegment(slug) {
|
|
2195
2293
|
return slug.slice(0, TEMP_BRANCH_SLUG_MAX).replace(/-+$/, "");
|
|
2196
2294
|
}
|
|
@@ -2208,14 +2306,14 @@ async function cleanupBurnWorktree(repoPath, branch, opts = {}) {
|
|
|
2208
2306
|
const attempts = Math.max(1, opts.attempts ?? 3);
|
|
2209
2307
|
const delayMs = opts.delayMs ?? 750;
|
|
2210
2308
|
const g = git(repoPath);
|
|
2211
|
-
for (let attempt = 1;attempt <= attempts &&
|
|
2309
|
+
for (let attempt = 1;attempt <= attempts && existsSync6(path); attempt++) {
|
|
2212
2310
|
try {
|
|
2213
2311
|
await g.raw(["worktree", "remove", "--force", path]);
|
|
2214
2312
|
} catch {}
|
|
2215
|
-
if (
|
|
2313
|
+
if (existsSync6(path) && attempt < attempts)
|
|
2216
2314
|
await sleep(delayMs);
|
|
2217
2315
|
}
|
|
2218
|
-
if (
|
|
2316
|
+
if (existsSync6(path)) {
|
|
2219
2317
|
try {
|
|
2220
2318
|
rmSync(path, { recursive: true, force: true });
|
|
2221
2319
|
} catch {}
|
|
@@ -2223,7 +2321,7 @@ async function cleanupBurnWorktree(repoPath, branch, opts = {}) {
|
|
|
2223
2321
|
try {
|
|
2224
2322
|
await g.raw(["worktree", "prune"]);
|
|
2225
2323
|
} catch {}
|
|
2226
|
-
return !
|
|
2324
|
+
return !existsSync6(path);
|
|
2227
2325
|
}
|
|
2228
2326
|
function sleep(ms) {
|
|
2229
2327
|
return new Promise((done) => setTimeout(done, ms));
|
|
@@ -2237,6 +2335,32 @@ async function branchCommitsAhead(repoPath, base, branch) {
|
|
|
2237
2335
|
return [];
|
|
2238
2336
|
}
|
|
2239
2337
|
}
|
|
2338
|
+
async function headSha(repoPath, ref) {
|
|
2339
|
+
try {
|
|
2340
|
+
const out = (await git(repoPath).revparse([ref])).trim();
|
|
2341
|
+
return out.length > 0 ? out : undefined;
|
|
2342
|
+
} catch {
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
async function commitsSince(repoPath, sha, branch) {
|
|
2347
|
+
try {
|
|
2348
|
+
const out = (await git(repoPath).raw(["rev-list", "--count", `${sha}..${branch}`])).trim();
|
|
2349
|
+
const n = Number(out);
|
|
2350
|
+
return Number.isFinite(n) ? n : undefined;
|
|
2351
|
+
} catch {
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
async function diffPaths(repoPath, from, to) {
|
|
2356
|
+
try {
|
|
2357
|
+
const out = (await git(repoPath).raw(["diff", "--name-only", from, to])).trim();
|
|
2358
|
+
return out ? out.split(`
|
|
2359
|
+
`).map((s) => s.trim()).filter(Boolean) : [];
|
|
2360
|
+
} catch {
|
|
2361
|
+
return [];
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2240
2364
|
async function commitSummaries(repoPath, base, branch, limit = 20) {
|
|
2241
2365
|
try {
|
|
2242
2366
|
const out = await git(repoPath).raw([
|
|
@@ -2432,7 +2556,7 @@ async function removeTalkWorktree(repoPath, worktreePath) {
|
|
|
2432
2556
|
try {
|
|
2433
2557
|
await g.raw(["worktree", "prune"]);
|
|
2434
2558
|
} catch {}
|
|
2435
|
-
if (
|
|
2559
|
+
if (existsSync6(worktreePath)) {
|
|
2436
2560
|
throw new InvalidInputError(`could not remove talk worktree at ${worktreePath} \u2014 a file may be locked; ` + "close anything using it and retry the delete");
|
|
2437
2561
|
}
|
|
2438
2562
|
}
|
|
@@ -2459,7 +2583,7 @@ async function deleteFeatureBranches(repoPath, slug) {
|
|
|
2459
2583
|
return { deleted, kept };
|
|
2460
2584
|
}
|
|
2461
2585
|
async function commitDocs(worktreePath, message) {
|
|
2462
|
-
if (!
|
|
2586
|
+
if (!existsSync6(resolve4(worktreePath, DOCS_PATHSPEC)))
|
|
2463
2587
|
return;
|
|
2464
2588
|
const g = git(worktreePath);
|
|
2465
2589
|
await g.add([DOCS_PATHSPEC]);
|
|
@@ -2504,6 +2628,7 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2504
2628
|
const devPaneId = testDriveState.devPaneId;
|
|
2505
2629
|
if (devPaneId)
|
|
2506
2630
|
stopDevPane(devPaneId);
|
|
2631
|
+
const carriedChanges = await dirtyPaths(g);
|
|
2507
2632
|
await g.checkout(previousBranch2);
|
|
2508
2633
|
if (detachedWorktree2)
|
|
2509
2634
|
await reattachWorktree(detachedWorktree2, branch);
|
|
@@ -2513,7 +2638,20 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2513
2638
|
message: `test drive stopped \u2014 back on ${previousBranch2}`,
|
|
2514
2639
|
data: { branch: previousBranch2 }
|
|
2515
2640
|
});
|
|
2516
|
-
|
|
2641
|
+
if (carriedChanges.length > 0) {
|
|
2642
|
+
emit(ctx, feature.id, {
|
|
2643
|
+
type: "testdrive.carried_changes",
|
|
2644
|
+
message: `${carriedChanges.length} uncommitted file(s) came back with you onto ${previousBranch2}: ${carriedChanges.slice(0, 5).join(", ")}${carriedChanges.length > 5 ? ", \u2026" : ""}`,
|
|
2645
|
+
data: { branch: previousBranch2, files: carriedChanges }
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
const dbDrift = await detectDbDrift(ctx, project, feature, previousBranch2, branch);
|
|
2649
|
+
return {
|
|
2650
|
+
ok: true,
|
|
2651
|
+
branch: previousBranch2,
|
|
2652
|
+
...carriedChanges.length > 0 ? { carriedChanges } : {},
|
|
2653
|
+
...dbDrift ? { dbDrift } : {}
|
|
2654
|
+
};
|
|
2517
2655
|
}
|
|
2518
2656
|
const porcelain = (await g.raw(["status", "--porcelain"])).trim();
|
|
2519
2657
|
if (porcelain !== "")
|
|
@@ -2550,6 +2688,35 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2550
2688
|
}
|
|
2551
2689
|
return { ok: true, branch };
|
|
2552
2690
|
}
|
|
2691
|
+
async function dirtyPaths(g) {
|
|
2692
|
+
try {
|
|
2693
|
+
const out = (await g.raw(["status", "--porcelain"])).trim();
|
|
2694
|
+
if (!out)
|
|
2695
|
+
return [];
|
|
2696
|
+
return out.split(`
|
|
2697
|
+
`).map((line) => line.slice(3).trim()).map((p) => p.includes(" -> ") ? p.split(" -> ").at(-1) ?? p : p).map((p) => p.replace(/^"|"$/g, "")).filter(Boolean);
|
|
2698
|
+
} catch {
|
|
2699
|
+
return [];
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
async function detectDbDrift(ctx, project, feature, previousBranch, driveBranch) {
|
|
2703
|
+
let files;
|
|
2704
|
+
try {
|
|
2705
|
+
files = migrationPaths(await diffPaths(project.repoPath, previousBranch, driveBranch));
|
|
2706
|
+
} catch {
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
if (files.length === 0)
|
|
2710
|
+
return;
|
|
2711
|
+
const resetCommand = project.dbResetCommand?.trim() || undefined;
|
|
2712
|
+
const drift = { files, ...resetCommand ? { resetCommand } : {} };
|
|
2713
|
+
emit(ctx, feature.id, {
|
|
2714
|
+
type: "testdrive.db_drift",
|
|
2715
|
+
message: `${driveBranch} and ${previousBranch} differ by ${files.length} migration file(s) \u2014 ` + "anything you migrated during the drive is still applied to your dev database, so the next " + `migrate on ${previousBranch} may report drift. ` + (resetCommand ? `Rebuild it with: ${resetCommand}` : 'Set a "Database reset command" in project settings to get a one-click fix here.'),
|
|
2716
|
+
data: { files, previousBranch, driveBranch, resetCommand: resetCommand ?? null }
|
|
2717
|
+
});
|
|
2718
|
+
return drift;
|
|
2719
|
+
}
|
|
2553
2720
|
async function mergeFeature(project, feature) {
|
|
2554
2721
|
const target = feature.baseBranch ?? project.mainBranch;
|
|
2555
2722
|
if (testDriveState) {
|
|
@@ -2605,171 +2772,18 @@ async function mergeInProgress(g) {
|
|
|
2605
2772
|
return false;
|
|
2606
2773
|
}
|
|
2607
2774
|
}
|
|
2608
|
-
var DOCS_PATHSPEC = "docs/features", DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", TEMP_BRANCH_PREFIXES, TEMP_BRANCH_SLUG_MAX = 16, testDriveState;
|
|
2775
|
+
var DOCS_PATHSPEC = "docs/features", MIGRATION_DIR_RE, DRIZZLE_SQL_RE, DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", PREP_BRANCH_PREFIX = "runcastle/prep/", TEMP_BRANCH_PREFIXES, TEMP_BRANCH_SLUG_MAX = 16, testDriveState;
|
|
2609
2776
|
var init_git = __esm(() => {
|
|
2610
2777
|
init_paths();
|
|
2611
2778
|
init_errors();
|
|
2612
2779
|
init_dev_pane();
|
|
2613
2780
|
init_events();
|
|
2614
2781
|
init_repo();
|
|
2782
|
+
MIGRATION_DIR_RE = /(^|\/)(migrations|migrate)\//i;
|
|
2783
|
+
DRIZZLE_SQL_RE = /(^|\/)drizzle\/.*\.sql$/i;
|
|
2615
2784
|
TEMP_BRANCH_PREFIXES = [RESEARCH_BRANCH_PREFIX, TICKET_BRANCH_PREFIX];
|
|
2616
2785
|
});
|
|
2617
2786
|
|
|
2618
|
-
// src/services/knowledge.ts
|
|
2619
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync } from "fs";
|
|
2620
|
-
import { join as join7, relative, resolve as resolve4, sep } from "path";
|
|
2621
|
-
function scaffoldDocs(ctx, feature) {
|
|
2622
|
-
const project = projectForFeature(ctx, feature);
|
|
2623
|
-
const dir = featureDocsDir(project, feature);
|
|
2624
|
-
mkdirSync3(dir, { recursive: true });
|
|
2625
|
-
const briefPath = featureDocPath(project, feature, "brief.md");
|
|
2626
|
-
if (!existsSync8(briefPath)) {
|
|
2627
|
-
const created = new Date().toISOString();
|
|
2628
|
-
const brief = [
|
|
2629
|
-
`# ${feature.title}`,
|
|
2630
|
-
"",
|
|
2631
|
-
feature.oneLiner,
|
|
2632
|
-
"",
|
|
2633
|
-
`- Slug: ${feature.slug}`,
|
|
2634
|
-
`- Created: ${created}`,
|
|
2635
|
-
""
|
|
2636
|
-
].join(`
|
|
2637
|
-
`);
|
|
2638
|
-
writeFileSync(briefPath, brief, "utf8");
|
|
2639
|
-
}
|
|
2640
|
-
if (feature.mapped) {
|
|
2641
|
-
scaffoldMapDoc(project, feature);
|
|
2642
|
-
}
|
|
2643
|
-
emit(ctx, feature.id, {
|
|
2644
|
-
type: "docs.scaffolded",
|
|
2645
|
-
message: feature.mapped ? `scaffolded docs/features/${feature.slug}/{brief,map}.md` : `scaffolded docs/features/${feature.slug}/brief.md`
|
|
2646
|
-
});
|
|
2647
|
-
}
|
|
2648
|
-
function scaffoldMapDoc(project, feature, seed) {
|
|
2649
|
-
mkdirSync3(featureDocsDir(project, feature), { recursive: true });
|
|
2650
|
-
const mapPath = featureDocPath(project, feature, "map.md");
|
|
2651
|
-
if (existsSync8(mapPath))
|
|
2652
|
-
return;
|
|
2653
|
-
writeFileSync(mapPath, mapDocBody(feature, seed), "utf8");
|
|
2654
|
-
}
|
|
2655
|
-
function mapDocBody(feature, seed) {
|
|
2656
|
-
const seeded = {
|
|
2657
|
-
Destination: seed?.destination?.trim(),
|
|
2658
|
-
Notes: seed?.notes?.trim()
|
|
2659
|
-
};
|
|
2660
|
-
const lines = [`# ${feature.title} \u2014 map`, ""];
|
|
2661
|
-
for (const section of MAP_SECTIONS) {
|
|
2662
|
-
lines.push(`## ${section}`, "");
|
|
2663
|
-
const body = seeded[section];
|
|
2664
|
-
if (body)
|
|
2665
|
-
lines.push(body, "");
|
|
2666
|
-
}
|
|
2667
|
-
return lines.join(`
|
|
2668
|
-
`);
|
|
2669
|
-
}
|
|
2670
|
-
function listDocs(ctx, feature) {
|
|
2671
|
-
const project = projectForFeature(ctx, feature);
|
|
2672
|
-
const dir = featureDocsDir(project, feature);
|
|
2673
|
-
if (!existsSync8(dir))
|
|
2674
|
-
return [];
|
|
2675
|
-
return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join7(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
2676
|
-
}
|
|
2677
|
-
function readDoc(ctx, feature, relPath) {
|
|
2678
|
-
const project = projectForFeature(ctx, feature);
|
|
2679
|
-
const dir = featureDocsDir(project, feature);
|
|
2680
|
-
const root = resolve4(dir);
|
|
2681
|
-
const target = resolve4(dir, relPath);
|
|
2682
|
-
const rel = relative(root, target);
|
|
2683
|
-
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
2684
|
-
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
2685
|
-
}
|
|
2686
|
-
if (!existsSync8(target))
|
|
2687
|
-
throw new NotFoundError(`doc not found: ${relPath}`);
|
|
2688
|
-
return { content: readFileSync3(target, "utf8") };
|
|
2689
|
-
}
|
|
2690
|
-
function readTitle(path, fallback) {
|
|
2691
|
-
try {
|
|
2692
|
-
const firstLine = readFileSync3(path, "utf8").split(`
|
|
2693
|
-
`, 1)[0] ?? "";
|
|
2694
|
-
const heading = firstLine.match(/^#\s+(.+)$/);
|
|
2695
|
-
return heading ? heading[1].trim() : fallback;
|
|
2696
|
-
} catch {
|
|
2697
|
-
return fallback;
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
var MAP_SECTIONS;
|
|
2701
|
-
var init_knowledge = __esm(() => {
|
|
2702
|
-
init_errors();
|
|
2703
|
-
init_events();
|
|
2704
|
-
init_feature_docs();
|
|
2705
|
-
init_repo();
|
|
2706
|
-
MAP_SECTIONS = [
|
|
2707
|
-
"Destination",
|
|
2708
|
-
"Notes",
|
|
2709
|
-
"Not yet specified",
|
|
2710
|
-
"Out of scope"
|
|
2711
|
-
];
|
|
2712
|
-
});
|
|
2713
|
-
|
|
2714
|
-
// src/pty/end-session.ts
|
|
2715
|
-
function endSession(ctx, sessionId) {
|
|
2716
|
-
const registry = ptyRegistry();
|
|
2717
|
-
const killed = registry.kill(sessionId);
|
|
2718
|
-
registry.remove(sessionId);
|
|
2719
|
-
const session = markSessionEnded(ctx, sessionId);
|
|
2720
|
-
if (session) {
|
|
2721
|
-
releaseForSession(ctx, sessionId);
|
|
2722
|
-
emit(ctx, session.featureId, {
|
|
2723
|
-
type: "session.ended",
|
|
2724
|
-
message: "session ended by user",
|
|
2725
|
-
data: { sessionId, killed }
|
|
2726
|
-
});
|
|
2727
|
-
} else {
|
|
2728
|
-
const existing = getSessionRow(ctx, sessionId);
|
|
2729
|
-
if (!existing)
|
|
2730
|
-
return { sessionId, ok: false, killed };
|
|
2731
|
-
}
|
|
2732
|
-
return { sessionId, ok: true, killed };
|
|
2733
|
-
}
|
|
2734
|
-
var init_end_session = __esm(() => {
|
|
2735
|
-
init_sessions();
|
|
2736
|
-
init_events();
|
|
2737
|
-
init_waypoints();
|
|
2738
|
-
init_registry();
|
|
2739
|
-
});
|
|
2740
|
-
|
|
2741
|
-
// src/launcher/skills-root.ts
|
|
2742
|
-
import { existsSync as existsSync9 } from "fs";
|
|
2743
|
-
import { dirname as dirname2, join as join8, resolve as resolve5 } from "path";
|
|
2744
|
-
function resolveSkillsRoot(fromDir) {
|
|
2745
|
-
const override = process.env[SKILLS_DIR_ENV];
|
|
2746
|
-
if (override) {
|
|
2747
|
-
const root = resolve5(override);
|
|
2748
|
-
if (existsSync9(join8(root, SKILLS_MARKER)))
|
|
2749
|
-
return root;
|
|
2750
|
-
throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
|
|
2751
|
-
}
|
|
2752
|
-
const searched = [];
|
|
2753
|
-
let dir = fromDir;
|
|
2754
|
-
for (let i = 0;i < 8; i += 1) {
|
|
2755
|
-
const candidate = join8(dir, "packages", "skills");
|
|
2756
|
-
searched.push(candidate);
|
|
2757
|
-
if (existsSync9(join8(candidate, SKILLS_MARKER)))
|
|
2758
|
-
return candidate;
|
|
2759
|
-
const parent = dirname2(dir);
|
|
2760
|
-
if (parent === dir)
|
|
2761
|
-
break;
|
|
2762
|
-
dir = parent;
|
|
2763
|
-
}
|
|
2764
|
-
throw new Error(`runcastle skills root not found; searched:
|
|
2765
|
-
${searched.join(`
|
|
2766
|
-
`)}`);
|
|
2767
|
-
}
|
|
2768
|
-
var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
|
|
2769
|
-
var init_skills_root = __esm(() => {
|
|
2770
|
-
SKILLS_MARKER = join8("packs", "runcastle");
|
|
2771
|
-
});
|
|
2772
|
-
|
|
2773
2787
|
// src/services/agent-stream.ts
|
|
2774
2788
|
function beginTranscript(ticketId) {
|
|
2775
2789
|
transcripts.set(ticketId, { live: true, chunks: [], nextIndex: 0, bytes: 0 });
|
|
@@ -2814,9 +2828,92 @@ var init_agent_stream = __esm(() => {
|
|
|
2814
2828
|
transcripts = new Map;
|
|
2815
2829
|
});
|
|
2816
2830
|
|
|
2831
|
+
// src/workflows/burn-guard.ts
|
|
2832
|
+
function renderGuardScript(rules = GUARD_RULES) {
|
|
2833
|
+
const lines = [
|
|
2834
|
+
"#!/bin/sh",
|
|
2835
|
+
"# runcastle burn guard (PreToolUse). Generated \u2014 edit burn-guard.ts, not this.",
|
|
2836
|
+
"set -u",
|
|
2837
|
+
"payload=$(cat 2>/dev/null || true)",
|
|
2838
|
+
'[ -n "$payload" ] || exit 0',
|
|
2839
|
+
"command -v jq >/dev/null 2>&1 || exit 0",
|
|
2840
|
+
'tool=$(printf %s "$payload" | jq -r ".tool_name // empty" 2>/dev/null || true)',
|
|
2841
|
+
'[ "$tool" = "Bash" ] || exit 0',
|
|
2842
|
+
'cmd=$(printf %s "$payload" | jq -r ".tool_input.command // empty" 2>/dev/null || true)',
|
|
2843
|
+
'[ -n "$cmd" ] || exit 0',
|
|
2844
|
+
"",
|
|
2845
|
+
"# Match against the command with quoted spans blanked, so an argument is",
|
|
2846
|
+
'# never read as a command: `grep -rn "git stash" docs/` searches for a',
|
|
2847
|
+
"# string, it does not run one.",
|
|
2848
|
+
`cmd=$(printf %s "$cmd" | sed "s/'[^']*'/ /g; s/\\"[^\\"]*\\"/ /g")`,
|
|
2849
|
+
"",
|
|
2850
|
+
"deny() {",
|
|
2851
|
+
` printf %s "$1" | jq -R -s '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:.}}'`,
|
|
2852
|
+
" exit 0",
|
|
2853
|
+
"}",
|
|
2854
|
+
""
|
|
2855
|
+
];
|
|
2856
|
+
for (const rule of rules) {
|
|
2857
|
+
lines.push(`# ${rule.id}`, `if printf %s "$cmd" | grep -Eq -- ${shSingleQuote(rule.pattern)}; then`, ` deny ${shSingleQuote(rule.reason)}`, "fi", "");
|
|
2858
|
+
}
|
|
2859
|
+
lines.push("exit 0", "");
|
|
2860
|
+
return lines.join(`
|
|
2861
|
+
`);
|
|
2862
|
+
}
|
|
2863
|
+
function shSingleQuote(value) {
|
|
2864
|
+
return `'${value.split("'").join(`'\\''`)}'`;
|
|
2865
|
+
}
|
|
2866
|
+
function renderGuardSettings() {
|
|
2867
|
+
return {
|
|
2868
|
+
hooks: {
|
|
2869
|
+
PreToolUse: [
|
|
2870
|
+
{
|
|
2871
|
+
matcher: "Bash",
|
|
2872
|
+
hooks: [{ type: "command", command: `sh ${GUARD_SCRIPT_PATH}`, timeout: 5 }]
|
|
2873
|
+
}
|
|
2874
|
+
]
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
function buildGuardInstallCommand() {
|
|
2879
|
+
const script = Buffer.from(renderGuardScript(), "utf8").toString("base64");
|
|
2880
|
+
const settings = Buffer.from(JSON.stringify(renderGuardSettings(), null, 2), "utf8").toString("base64");
|
|
2881
|
+
return [
|
|
2882
|
+
`mkdir -p "$HOME/.claude/hooks"`,
|
|
2883
|
+
`printf %s '${script}' | base64 -d > "${GUARD_SCRIPT_PATH}"`,
|
|
2884
|
+
`chmod +x "${GUARD_SCRIPT_PATH}"`,
|
|
2885
|
+
`printf %s '${settings}' | base64 -d > "${GUARD_SETTINGS_PATH}"`
|
|
2886
|
+
].join(" && ");
|
|
2887
|
+
}
|
|
2888
|
+
var CMD_START = "(^|[;&|(]|&&)[[:space:]]*", GUARD_RULES, GUARD_SCRIPT_PATH = "$HOME/.claude/hooks/burn-guard.sh", GUARD_SETTINGS_PATH = "$HOME/.claude/settings.json";
|
|
2889
|
+
var init_burn_guard = __esm(() => {
|
|
2890
|
+
GUARD_RULES = [
|
|
2891
|
+
{
|
|
2892
|
+
id: "no-stash",
|
|
2893
|
+
pattern: `${CMD_START}git[[:space:]]+stash`,
|
|
2894
|
+
reason: "git stash is blocked in burns: stashed work is invisible to the orchestrator and unrecoverable if this process dies, and only commits survive. To compare against the pre-change state use `git show HEAD:<path>`, or the pre-existing-failure baseline in your prompt."
|
|
2895
|
+
},
|
|
2896
|
+
{
|
|
2897
|
+
id: "no-serialised-tests",
|
|
2898
|
+
pattern: "--maxWorkers|--runInBand|--pool=|--shard=|singleFork|--poolOptions",
|
|
2899
|
+
reason: `Do not override the test runner's concurrency. Measured in this sandbox, a suite that runs in ~55s at its configured concurrency takes 10-20 minutes serialised, so this "safe" flag is the single most expensive habit in a burn. Run the repo's test command as configured. If it is killed for memory, run only the test files your change touches and say so in your final message.`
|
|
2900
|
+
},
|
|
2901
|
+
{
|
|
2902
|
+
id: "no-interpreter-heredoc-edit",
|
|
2903
|
+
pattern: `${CMD_START}(python3?|node|bun|perl|ruby)[[:space:]]+-[[:space:]]*<<`,
|
|
2904
|
+
reason: "Do not rewrite files by piping a heredoc into an interpreter \u2014 individual such calls have been measured at 29s, 57s, 120s and 761s, and a half-written file survives if this process dies mid-command. Use the Edit tool, which is faster, atomic, and fails loudly when its target text is not found."
|
|
2905
|
+
},
|
|
2906
|
+
{
|
|
2907
|
+
id: "no-cat-heredoc-edit",
|
|
2908
|
+
pattern: `${CMD_START}cat[[:space:]]+>>?[[:space:]]*[^[:space:]|]+[[:space:]]*<<`,
|
|
2909
|
+
reason: "Do not write files with `cat > file <<EOF`. Use the Write tool for a new file, or Edit to change part of an existing one."
|
|
2910
|
+
}
|
|
2911
|
+
];
|
|
2912
|
+
});
|
|
2913
|
+
|
|
2817
2914
|
// src/workflows/ticket-burner.ts
|
|
2818
|
-
import { existsSync as
|
|
2819
|
-
import { dirname as dirname3, join as
|
|
2915
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
2916
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
2820
2917
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2821
2918
|
import { claudeCode, run } from "@ai-hero/sandcastle";
|
|
2822
2919
|
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
@@ -2932,13 +3029,13 @@ function parseEnvFile(content) {
|
|
|
2932
3029
|
if (line.length === 0 || line.startsWith("#"))
|
|
2933
3030
|
continue;
|
|
2934
3031
|
const body = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
2935
|
-
const
|
|
2936
|
-
if (
|
|
3032
|
+
const eq5 = body.indexOf("=");
|
|
3033
|
+
if (eq5 === -1)
|
|
2937
3034
|
continue;
|
|
2938
|
-
const key = body.slice(0,
|
|
3035
|
+
const key = body.slice(0, eq5).trim();
|
|
2939
3036
|
if (key.length === 0)
|
|
2940
3037
|
continue;
|
|
2941
|
-
let value = body.slice(
|
|
3038
|
+
let value = body.slice(eq5 + 1).trim();
|
|
2942
3039
|
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
2943
3040
|
value = value.slice(1, -1);
|
|
2944
3041
|
}
|
|
@@ -3020,13 +3117,31 @@ function buildWorkspaceNotes(mode) {
|
|
|
3020
3117
|
].join(`
|
|
3021
3118
|
`);
|
|
3022
3119
|
}
|
|
3120
|
+
function buildVerifyNotes(config2) {
|
|
3121
|
+
const commands = config2.verifyCommands?.trim();
|
|
3122
|
+
const failures = config2.knownFailures?.trim();
|
|
3123
|
+
const out = [];
|
|
3124
|
+
if (commands) {
|
|
3125
|
+
out.push("Verify with exactly these commands \u2014 they are correct for this repo, so do not go looking for alternatives or guess at workspace/package filter names:", "", "```", commands, "```");
|
|
3126
|
+
} else {
|
|
3127
|
+
out.push("No verification commands are configured, so derive them ONCE from the repo (root `package.json` scripts, the workspace's own manifest, CI config) before your first test run \u2014 reading a manifest is free, discovering a filter name by running the wrong suite is not. Write the working commands into your notes and reuse them verbatim for the rest of the ticket.");
|
|
3128
|
+
}
|
|
3129
|
+
out.push("");
|
|
3130
|
+
if (failures) {
|
|
3131
|
+
out.push("These tests ALREADY fail on this repo before you touch anything:", "", "```", failures, "```", "", "Treat that as your baseline \u2014 do NOT spend a run establishing it yourself. Only a failure outside this set is yours to fix; if you hit one that looks pre-existing but is not listed, confirm it on a single targeted run of that one test, never a whole-suite re-run.");
|
|
3132
|
+
} else {
|
|
3133
|
+
out.push("No pre-existing-failure baseline is configured. If the suite is already red, capture the baseline ONCE \u2014 the first full run you do, before your changes land, is the baseline \u2014 and compare every later run against those saved results. Never re-run a whole suite just to re-establish what was already failing.");
|
|
3134
|
+
}
|
|
3135
|
+
return out.join(`
|
|
3136
|
+
`);
|
|
3137
|
+
}
|
|
3023
3138
|
function readRepoToolchain(repoPath) {
|
|
3024
|
-
const pkgPath =
|
|
3025
|
-
const hasPackageJson =
|
|
3139
|
+
const pkgPath = join7(repoPath, "package.json");
|
|
3140
|
+
const hasPackageJson = existsSync7(pkgPath);
|
|
3026
3141
|
let packageManagerField;
|
|
3027
3142
|
if (hasPackageJson) {
|
|
3028
3143
|
try {
|
|
3029
|
-
const pkg = JSON.parse(
|
|
3144
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
3030
3145
|
if (typeof pkg.packageManager === "string")
|
|
3031
3146
|
packageManagerField = pkg.packageManager;
|
|
3032
3147
|
} catch {}
|
|
@@ -3035,10 +3150,10 @@ function readRepoToolchain(repoPath) {
|
|
|
3035
3150
|
hasPackageJson,
|
|
3036
3151
|
...packageManagerField !== undefined ? { packageManagerField } : {},
|
|
3037
3152
|
lockfiles: {
|
|
3038
|
-
bun:
|
|
3039
|
-
pnpm:
|
|
3040
|
-
yarn:
|
|
3041
|
-
npm:
|
|
3153
|
+
bun: existsSync7(join7(repoPath, "bun.lock")) || existsSync7(join7(repoPath, "bun.lockb")),
|
|
3154
|
+
pnpm: existsSync7(join7(repoPath, "pnpm-lock.yaml")),
|
|
3155
|
+
yarn: existsSync7(join7(repoPath, "yarn.lock")),
|
|
3156
|
+
npm: existsSync7(join7(repoPath, "package-lock.json"))
|
|
3042
3157
|
}
|
|
3043
3158
|
};
|
|
3044
3159
|
}
|
|
@@ -3076,6 +3191,63 @@ function createStreamThrottle(emit2, opts = {}) {
|
|
|
3076
3191
|
};
|
|
3077
3192
|
return { onEvent, flush: flushText };
|
|
3078
3193
|
}
|
|
3194
|
+
function normalizeCommandForClassification(command) {
|
|
3195
|
+
return command.replace(/<<\s*['"]?(\w+)['"]?[\s\S]*?(^|\n)\s*\1\b/gm, "<<$1 ").replace(/<<\s*['"]?(\w+)['"]?[\s\S]*$/, "<<$1 ").replace(/'[^']*'/g, " ").replace(/"[^"]*"/g, " ");
|
|
3196
|
+
}
|
|
3197
|
+
function classifyToolCall(name, args) {
|
|
3198
|
+
const byName = TOOL_NAME_CATEGORY[name];
|
|
3199
|
+
if (byName)
|
|
3200
|
+
return byName;
|
|
3201
|
+
if (name !== "Bash")
|
|
3202
|
+
return "other";
|
|
3203
|
+
const normalized = normalizeCommandForClassification(args);
|
|
3204
|
+
for (const [category, pattern] of BASH_PATTERNS) {
|
|
3205
|
+
if (pattern.test(normalized))
|
|
3206
|
+
return category;
|
|
3207
|
+
}
|
|
3208
|
+
return "other";
|
|
3209
|
+
}
|
|
3210
|
+
function createToolTimer() {
|
|
3211
|
+
const byCategory = {};
|
|
3212
|
+
let calls = 0;
|
|
3213
|
+
let totalMs = 0;
|
|
3214
|
+
let pending = null;
|
|
3215
|
+
let iteration = null;
|
|
3216
|
+
const charge = (category, ms) => {
|
|
3217
|
+
const slot = byCategory[category] ??= { calls: 0, ms: 0 };
|
|
3218
|
+
slot.ms += ms;
|
|
3219
|
+
totalMs += ms;
|
|
3220
|
+
};
|
|
3221
|
+
const onEvent = (event) => {
|
|
3222
|
+
if (event.type === "raw")
|
|
3223
|
+
return;
|
|
3224
|
+
const at = event.timestamp.getTime();
|
|
3225
|
+
if (iteration !== null && event.iteration !== iteration)
|
|
3226
|
+
pending = null;
|
|
3227
|
+
iteration = event.iteration;
|
|
3228
|
+
if (pending) {
|
|
3229
|
+
const gap = at - pending.at;
|
|
3230
|
+
if (gap > 0 && gap <= MAX_ATTRIBUTABLE_GAP_MS)
|
|
3231
|
+
charge(pending.category, gap);
|
|
3232
|
+
}
|
|
3233
|
+
if (event.type === "toolCall") {
|
|
3234
|
+
const category = classifyToolCall(event.name, event.formattedArgs ?? "");
|
|
3235
|
+
const slot = byCategory[category] ??= { calls: 0, ms: 0 };
|
|
3236
|
+
slot.calls += 1;
|
|
3237
|
+
calls += 1;
|
|
3238
|
+
pending = { category, at };
|
|
3239
|
+
} else {
|
|
3240
|
+
pending = { category: "model", at };
|
|
3241
|
+
}
|
|
3242
|
+
};
|
|
3243
|
+
return { onEvent, summary: () => ({ totalMs, calls, byCategory }) };
|
|
3244
|
+
}
|
|
3245
|
+
function formatTimingSummary(s) {
|
|
3246
|
+
if (s.totalMs === 0)
|
|
3247
|
+
return `${s.calls} tool call(s), no measurable time`;
|
|
3248
|
+
const parts = Object.entries(s.byCategory).sort((a, b) => (b[1]?.ms ?? 0) - (a[1]?.ms ?? 0)).slice(0, 5).map(([c, t]) => `${c} ${Math.round((t?.ms ?? 0) / s.totalMs * 100)}%`);
|
|
3249
|
+
return `${Math.round(s.totalMs / 60000)}min across ${s.calls} tool call(s) \u2014 ${parts.join(", ")}`;
|
|
3250
|
+
}
|
|
3079
3251
|
function interpretRunResult(result, blockedContent) {
|
|
3080
3252
|
const commits = result.commits.map((c) => c.sha);
|
|
3081
3253
|
if (commits.length > 0)
|
|
@@ -3122,18 +3294,18 @@ function buildRetryNotes(input) {
|
|
|
3122
3294
|
`);
|
|
3123
3295
|
}
|
|
3124
3296
|
function delayUnlessAborted(ms, signal) {
|
|
3125
|
-
return new Promise((
|
|
3297
|
+
return new Promise((resolve5) => {
|
|
3126
3298
|
if (signal.aborted) {
|
|
3127
|
-
|
|
3299
|
+
resolve5();
|
|
3128
3300
|
return;
|
|
3129
3301
|
}
|
|
3130
3302
|
const onAbort = () => {
|
|
3131
3303
|
clearTimeout(t);
|
|
3132
|
-
|
|
3304
|
+
resolve5();
|
|
3133
3305
|
};
|
|
3134
3306
|
const t = setTimeout(() => {
|
|
3135
3307
|
signal.removeEventListener("abort", onAbort);
|
|
3136
|
-
|
|
3308
|
+
resolve5();
|
|
3137
3309
|
}, ms);
|
|
3138
3310
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
3139
3311
|
});
|
|
@@ -3156,38 +3328,38 @@ function createSerialQueue() {
|
|
|
3156
3328
|
};
|
|
3157
3329
|
}
|
|
3158
3330
|
async function landWithResolve(branch, deps) {
|
|
3159
|
-
let
|
|
3331
|
+
let current = branch;
|
|
3160
3332
|
for (let attempt = 1;; attempt++) {
|
|
3161
|
-
const merge = await deps.merge(
|
|
3333
|
+
const merge = await deps.merge(current);
|
|
3162
3334
|
if (merge.ok)
|
|
3163
|
-
return { status: "landed", branch:
|
|
3335
|
+
return { status: "landed", branch: current };
|
|
3164
3336
|
const detail = merge.error ?? "merge failed";
|
|
3165
3337
|
if (!merge.conflict)
|
|
3166
|
-
return { status: "failed", branch:
|
|
3338
|
+
return { status: "failed", branch: current, error: detail };
|
|
3167
3339
|
const files = merge.files ?? [];
|
|
3168
3340
|
if (attempt > deps.maxResolveAttempts) {
|
|
3169
|
-
return { status: "conflict", branch:
|
|
3341
|
+
return { status: "conflict", branch: current, files, error: detail };
|
|
3170
3342
|
}
|
|
3171
3343
|
const where = files.length > 0 ? `${files.length} file(s)` : "the merge";
|
|
3172
3344
|
deps.emit({
|
|
3173
3345
|
type: "merge.conflict.resolving",
|
|
3174
3346
|
message: `${deps.label}: conflicts with ${deps.featureBranch} on ${where} \u2014 resolving with an agent (pass ${attempt}/${deps.maxResolveAttempts})`,
|
|
3175
|
-
data: { branch:
|
|
3347
|
+
data: { branch: current, files, attempt, maxAttempts: deps.maxResolveAttempts }
|
|
3176
3348
|
});
|
|
3177
3349
|
const resolved = await deps.resolve({
|
|
3178
|
-
branch:
|
|
3350
|
+
branch: current,
|
|
3179
3351
|
files,
|
|
3180
3352
|
attempt,
|
|
3181
3353
|
maxAttempts: deps.maxResolveAttempts
|
|
3182
3354
|
});
|
|
3183
|
-
|
|
3355
|
+
current = resolved.branch;
|
|
3184
3356
|
if (!resolved.ok) {
|
|
3185
|
-
return { status: "conflict", branch:
|
|
3357
|
+
return { status: "conflict", branch: current, files, error: resolved.error ?? detail };
|
|
3186
3358
|
}
|
|
3187
3359
|
deps.emit({
|
|
3188
3360
|
type: "merge.conflict.resolved",
|
|
3189
|
-
message: `${deps.label}: conflict resolved on ${
|
|
3190
|
-
data: { branch:
|
|
3361
|
+
message: `${deps.label}: conflict resolved on ${current} \u2014 landing`,
|
|
3362
|
+
data: { branch: current, files, attempt }
|
|
3191
3363
|
});
|
|
3192
3364
|
}
|
|
3193
3365
|
}
|
|
@@ -3345,26 +3517,26 @@ function resolverTemplatePath() {
|
|
|
3345
3517
|
}
|
|
3346
3518
|
function burnerAssetPath(file) {
|
|
3347
3519
|
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
3348
|
-
return
|
|
3520
|
+
return join7(resolveSkillsRoot(here), "burner", file);
|
|
3349
3521
|
}
|
|
3350
3522
|
function readDocsDigestFromDisk(projectId, slug) {
|
|
3351
3523
|
const worktree = worktreeDir(projectId, slug);
|
|
3352
|
-
if (!
|
|
3524
|
+
if (!existsSync7(worktree))
|
|
3353
3525
|
return "_No talk worktree on disk \u2014 docs digest skipped._";
|
|
3354
|
-
const docsDir =
|
|
3355
|
-
if (!
|
|
3526
|
+
const docsDir = join7(worktree, ...featureDocsRel(slug).split("/"));
|
|
3527
|
+
if (!existsSync7(docsDir))
|
|
3356
3528
|
return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
|
|
3357
|
-
const files =
|
|
3529
|
+
const files = readdirSync2(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync3(join7(docsDir, e.name), "utf8") }));
|
|
3358
3530
|
return buildDocsDigest(files);
|
|
3359
3531
|
}
|
|
3360
3532
|
function readBlockedFile(dirs) {
|
|
3361
3533
|
for (const dir of dirs) {
|
|
3362
3534
|
if (!dir)
|
|
3363
3535
|
continue;
|
|
3364
|
-
const p =
|
|
3365
|
-
if (
|
|
3536
|
+
const p = join7(dir, "BLOCKED.md");
|
|
3537
|
+
if (existsSync7(p)) {
|
|
3366
3538
|
try {
|
|
3367
|
-
return
|
|
3539
|
+
return readFileSync3(p, "utf8");
|
|
3368
3540
|
} catch {}
|
|
3369
3541
|
}
|
|
3370
3542
|
}
|
|
@@ -3391,10 +3563,7 @@ function buildBurnAgent(config2, token, model) {
|
|
|
3391
3563
|
return agent;
|
|
3392
3564
|
}
|
|
3393
3565
|
function selectSandbox(config2, mounts = []) {
|
|
3394
|
-
const imageOpts =
|
|
3395
|
-
imageName: resolveSandboxImage(config2),
|
|
3396
|
-
...mounts.length > 0 ? { mounts } : {}
|
|
3397
|
-
};
|
|
3566
|
+
const imageOpts = buildSandboxOptions(config2, mounts);
|
|
3398
3567
|
switch (config2.sandbox) {
|
|
3399
3568
|
case "docker":
|
|
3400
3569
|
return docker(imageOpts);
|
|
@@ -3404,6 +3573,13 @@ function selectSandbox(config2, mounts = []) {
|
|
|
3404
3573
|
return noSandbox();
|
|
3405
3574
|
}
|
|
3406
3575
|
}
|
|
3576
|
+
function buildSandboxOptions(config2, mounts = []) {
|
|
3577
|
+
return {
|
|
3578
|
+
imageName: resolveSandboxImage(config2),
|
|
3579
|
+
...mounts.length > 0 ? { mounts } : {},
|
|
3580
|
+
...config2.burnCpus !== undefined ? { cpus: config2.burnCpus } : {}
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3407
3583
|
async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget) {
|
|
3408
3584
|
const { project, feature } = ctx;
|
|
3409
3585
|
const workspaceMode = resolveBurnWorkspaceMode(config2);
|
|
@@ -3413,30 +3589,37 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3413
3589
|
const featureBrief = buildFeatureBrief(feature);
|
|
3414
3590
|
const docsDigest = readDocsDigestFromDisk(project.id, feature.slug);
|
|
3415
3591
|
const workspaceNotes = buildWorkspaceNotes(workspaceMode);
|
|
3416
|
-
const
|
|
3592
|
+
const prepared = resolvePreparedSettings(config2, project);
|
|
3593
|
+
const verifyNotes = buildVerifyNotes(prepared);
|
|
3594
|
+
const basePrompt = renderTicketPrompt(readFileSync3(burnerTemplatePath(), "utf8"), {
|
|
3417
3595
|
TICKET_JSON: ticketJson,
|
|
3418
3596
|
FEATURE_BRIEF: featureBrief,
|
|
3419
3597
|
DOCS_DIGEST: docsDigest,
|
|
3420
3598
|
COMMIT_CONVENTION: `ticket(${ticket.seq}): <summary>`,
|
|
3599
|
+
VERIFY_NOTES: verifyNotes,
|
|
3421
3600
|
WORKSPACE_NOTES: workspaceNotes
|
|
3422
3601
|
});
|
|
3423
3602
|
const toolchain = readRepoToolchain(project.repoPath);
|
|
3424
3603
|
const pm = detectPackageManager(toolchain);
|
|
3425
|
-
const setupCommand = resolveSetupCommand(toolchain,
|
|
3604
|
+
const setupCommand = resolveSetupCommand(toolchain, prepared.setupCommand);
|
|
3426
3605
|
const mounts = [];
|
|
3427
3606
|
if (config2.sandbox !== "noSandbox" && pm) {
|
|
3428
3607
|
const mount = cacheMountFor(pm, burnCacheDir(pm));
|
|
3429
3608
|
if (mount) {
|
|
3430
|
-
|
|
3609
|
+
mkdirSync3(mount.hostPath, { recursive: true });
|
|
3431
3610
|
mounts.push(mount);
|
|
3432
3611
|
}
|
|
3433
3612
|
}
|
|
3434
|
-
|
|
3435
|
-
const
|
|
3613
|
+
const guardInstall = config2.burnGuard && config2.sandbox !== "noSandbox" ? buildGuardInstallCommand() : undefined;
|
|
3614
|
+
const withGuard = (setup) => guardInstall === undefined ? setup : setup ? `${guardInstall} && ${setup}` : guardInstall;
|
|
3615
|
+
mkdirSync3(logsDir(), { recursive: true });
|
|
3616
|
+
const logFilePath = join7(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
|
|
3436
3617
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
3437
3618
|
beginTranscript(ticket.id);
|
|
3619
|
+
const timer = createToolTimer();
|
|
3438
3620
|
const onStreamEvent = (event) => {
|
|
3439
3621
|
throttle.onEvent(event);
|
|
3622
|
+
timer.onEvent(event);
|
|
3440
3623
|
if (event.type === "text") {
|
|
3441
3624
|
appendTranscript(ticket.id, { kind: "text", text: event.message });
|
|
3442
3625
|
} else if (event.type === "toolCall") {
|
|
@@ -3476,17 +3659,18 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3476
3659
|
const runResolver = async (input) => {
|
|
3477
3660
|
const resolveBranch = ticketBranchName(feature.slug, ticket.seq, newId("r").slice(2, 10));
|
|
3478
3661
|
const otherSide = await commitSummaries(project.repoPath, input.branch, feature.branch);
|
|
3479
|
-
const prompt = renderTemplate(
|
|
3662
|
+
const prompt = renderTemplate(readFileSync3(resolverTemplatePath(), "utf8"), {
|
|
3480
3663
|
TICKET_JSON: ticketJson,
|
|
3481
3664
|
FEATURE_BRIEF: featureBrief,
|
|
3482
3665
|
DOCS_DIGEST: docsDigest,
|
|
3483
3666
|
WORKSPACE_NOTES: workspaceNotes,
|
|
3667
|
+
VERIFY_NOTES: verifyNotes,
|
|
3484
3668
|
FEATURE_BRANCH: feature.branch,
|
|
3485
3669
|
CONFLICT_FILES: buildConflictFilesBlock(input.files),
|
|
3486
3670
|
OTHER_SIDE: buildOtherSideBlock(otherSide),
|
|
3487
3671
|
MERGE_COMMAND: resolveMergeCommand(workspaceMode, feature.branch)
|
|
3488
3672
|
});
|
|
3489
|
-
const hookCommand = workspaceMode === "isolated" ? buildIsolatedSetupCommand(resolveBranch, setupCommand, pm) : setupCommand;
|
|
3673
|
+
const hookCommand = withGuard(workspaceMode === "isolated" ? buildIsolatedSetupCommand(resolveBranch, setupCommand, pm) : setupCommand);
|
|
3490
3674
|
appendTranscript(ticket.id, {
|
|
3491
3675
|
kind: "text",
|
|
3492
3676
|
text: `
|
|
@@ -3616,7 +3800,7 @@ ${blocked.trim()}`
|
|
|
3616
3800
|
let tempBranch = "";
|
|
3617
3801
|
for (let attempt = 1;; attempt++) {
|
|
3618
3802
|
tempBranch = ticketBranchName(feature.slug, ticket.seq, newId("b").slice(2, 10));
|
|
3619
|
-
const hookCommand = workspaceMode === "isolated" ? buildIsolatedSetupCommand(tempBranch, setupCommand, pm) : setupCommand;
|
|
3803
|
+
const hookCommand = withGuard(workspaceMode === "isolated" ? buildIsolatedSetupCommand(tempBranch, setupCommand, pm) : setupCommand);
|
|
3620
3804
|
if (hookCommand && attempt === 1) {
|
|
3621
3805
|
ctx.emitEvent({
|
|
3622
3806
|
type: "burn.setup",
|
|
@@ -3751,87 +3935,1008 @@ ${blocked.trim()}` };
|
|
|
3751
3935
|
activeTicketAborts.delete(ticket.id);
|
|
3752
3936
|
throttle.flush();
|
|
3753
3937
|
endTranscript(ticket.id);
|
|
3938
|
+
const timing = timer.summary();
|
|
3939
|
+
if (timing.calls > 0) {
|
|
3940
|
+
ctx.emitEvent({
|
|
3941
|
+
type: "ticket.timing",
|
|
3942
|
+
message: `ticket ${ticket.seq} spent ${formatTimingSummary(timing)}`,
|
|
3943
|
+
ticketId: ticket.id,
|
|
3944
|
+
data: timing
|
|
3945
|
+
});
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
function resolveBurnDeps(ctx) {
|
|
3950
|
+
const config2 = loadConfig();
|
|
3951
|
+
const token = readTokenFromEnvFile(envPath());
|
|
3952
|
+
const model = resolveModel("implement", config2, ctx.project, ctx.modelOverride);
|
|
3953
|
+
const land = createSerialQueue();
|
|
3954
|
+
let pushTargetReady;
|
|
3955
|
+
const ensureIsolatedPushTarget = () => pushTargetReady ??= allowPushToCheckedOutBranches(ctx.project.repoPath);
|
|
3956
|
+
return {
|
|
3957
|
+
config: config2,
|
|
3958
|
+
hasAuthToken: token !== undefined,
|
|
3959
|
+
concurrency: config2.burnConcurrency,
|
|
3960
|
+
executeTicketRun: (c, ticket) => realExecuteTicketRun(c, ticket, config2, token, model, land, ensureIsolatedPushTarget)
|
|
3961
|
+
};
|
|
3962
|
+
}
|
|
3963
|
+
function readTokenFromEnvFile(path) {
|
|
3964
|
+
let fromFile;
|
|
3965
|
+
if (existsSync7(path)) {
|
|
3966
|
+
try {
|
|
3967
|
+
fromFile = parseEnvFile(readFileSync3(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
|
|
3968
|
+
} catch {
|
|
3969
|
+
fromFile = undefined;
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
3973
|
+
return token && token.length > 0 ? token : undefined;
|
|
3974
|
+
}
|
|
3975
|
+
var AUTH_MISSING_EVENT = "auth.missing", AUTH_MISSING_MESSAGE = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", RUN, BASH_PATTERNS, TOOL_NAME_CATEGORY, MAX_ATTRIBUTABLE_GAP_MS, BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
3976
|
+
var init_ticket_burner = __esm(() => {
|
|
3977
|
+
init_src();
|
|
3978
|
+
init_config_load();
|
|
3979
|
+
init_paths();
|
|
3980
|
+
init_skills_root();
|
|
3981
|
+
init_agent_stream();
|
|
3982
|
+
init_git();
|
|
3983
|
+
init_burn_guard();
|
|
3984
|
+
PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
3985
|
+
PM_CACHE_SANDBOX_PATHS = {
|
|
3986
|
+
bun: "~/.bun/install/cache",
|
|
3987
|
+
yarn: "~/.cache/yarn",
|
|
3988
|
+
npm: "~/.npm"
|
|
3989
|
+
};
|
|
3990
|
+
RUN = String.raw`(?![\w./-])`;
|
|
3991
|
+
BASH_PATTERNS = [
|
|
3992
|
+
["tests", new RegExp(String.raw`\b(vitest|jest|pytest|go test|cargo test|bun test|playwright|maestro)${RUN}|\b(pnpm|npm|yarn|bun|npx)\b[^&;|]*?\s(run\s+)?test${RUN}|--testPathPatterns`)],
|
|
3993
|
+
["typecheck", /\btsc\b|\btypecheck\b|\bmypy\b|\bcargo check\b/],
|
|
3994
|
+
["build", new RegExp(String.raw`\bbuild${RUN}|\bprisma\s+(generate|migrate)\b|\bcodegen${RUN}`)],
|
|
3995
|
+
["lint", new RegExp(String.raw`\b(eslint|prettier|ruff|biome|lint|format)${RUN}`)],
|
|
3996
|
+
["install", /\b(pnpm|npm|yarn|bun|corepack)\s+\w*\s*install\b|\bnpm ci\b/],
|
|
3997
|
+
["git", /\bgit\b/],
|
|
3998
|
+
["search", /\b(grep|rg|ag|find)\b/],
|
|
3999
|
+
["file-read", /\b(cat|sed|head|tail|less|wc|ls)\b/],
|
|
4000
|
+
["file-edit", /<<\s*['"]?(PY|EOF|SH|JS|TS)\b|\bpython3?\s+-\s*<</]
|
|
4001
|
+
];
|
|
4002
|
+
TOOL_NAME_CATEGORY = {
|
|
4003
|
+
Read: "file-read",
|
|
4004
|
+
NotebookRead: "file-read",
|
|
4005
|
+
Grep: "search",
|
|
4006
|
+
Glob: "search",
|
|
4007
|
+
Edit: "file-edit",
|
|
4008
|
+
Write: "file-edit",
|
|
4009
|
+
NotebookEdit: "file-edit"
|
|
4010
|
+
};
|
|
4011
|
+
MAX_ATTRIBUTABLE_GAP_MS = 20 * 60000;
|
|
4012
|
+
BURN_WORKTREE_PATH = /[\\/]\.sandcastle[\\/]worktrees[\\/]/i;
|
|
4013
|
+
WORKTREE_REMOVAL_FAILURES = [
|
|
4014
|
+
/failed to delete/i,
|
|
4015
|
+
/directory not empty|enotempty/i,
|
|
4016
|
+
/unable to (unlink|delete)/i,
|
|
4017
|
+
/is not a working tree/i,
|
|
4018
|
+
/resource busy|ebusy|eperm|eacces/i
|
|
4019
|
+
];
|
|
4020
|
+
FATAL_ERROR_PATTERNS = [
|
|
4021
|
+
/invalid (api key|x-api-key)/i,
|
|
4022
|
+
/authentication|unauthorized|permission denied/i,
|
|
4023
|
+
/credit balance|billing/i,
|
|
4024
|
+
/oauth token|setup-token/i,
|
|
4025
|
+
/issue with the selected model|model not found|unknown model/i,
|
|
4026
|
+
/does not support resumeSession|resumeSession .* not found/i
|
|
4027
|
+
];
|
|
4028
|
+
RETRYABLE_ERROR_PATTERNS = [
|
|
4029
|
+
/exited with code \d+/i,
|
|
4030
|
+
/idle timeout|AgentIdleTimeout/i,
|
|
4031
|
+
/connection (closed|error|refused|reset)|socket hang up|fetch failed/i,
|
|
4032
|
+
/econnreset|etimedout|econnrefused|epipe|eai_again/i,
|
|
4033
|
+
/overloaded|rate.?limit|too many requests/i,
|
|
4034
|
+
/internal server error|service unavailable|bad gateway|gateway timeout/i,
|
|
4035
|
+
/\bapi error\b/i,
|
|
4036
|
+
/session capture failed/i
|
|
4037
|
+
];
|
|
4038
|
+
activeTicketAborts = new Map;
|
|
4039
|
+
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
4040
|
+
ticketBurner = {
|
|
4041
|
+
id: "ticket-burner",
|
|
4042
|
+
run: (ctx) => burnRun(ctx, resolveBurnDeps(ctx))
|
|
4043
|
+
};
|
|
4044
|
+
});
|
|
4045
|
+
|
|
4046
|
+
// src/workflows/project-prep.ts
|
|
4047
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "fs";
|
|
4048
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
4049
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4050
|
+
import { run as run2 } from "@ai-hero/sandcastle";
|
|
4051
|
+
import * as z3 from "zod";
|
|
4052
|
+
function parsePrepFindings(raw) {
|
|
4053
|
+
let json;
|
|
4054
|
+
try {
|
|
4055
|
+
json = JSON.parse(raw);
|
|
4056
|
+
} catch (e) {
|
|
4057
|
+
throw new Error(`findings file is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
4058
|
+
}
|
|
4059
|
+
const outer = RawFindings.safeParse(json);
|
|
4060
|
+
if (!outer.success)
|
|
4061
|
+
throw new Error("findings file is not a JSON object");
|
|
4062
|
+
const warnings = [];
|
|
4063
|
+
const values = {};
|
|
4064
|
+
const known = new Set(PREPARED_KEYS);
|
|
4065
|
+
for (const [key, rawValue] of Object.entries(outer.data)) {
|
|
4066
|
+
if (key === "notes")
|
|
4067
|
+
continue;
|
|
4068
|
+
if (!known.has(key)) {
|
|
4069
|
+
warnings.push(`ignored unknown key "${key}"`);
|
|
4070
|
+
continue;
|
|
4071
|
+
}
|
|
4072
|
+
const parsed = RawFinding.safeParse(rawValue);
|
|
4073
|
+
if (!parsed.success) {
|
|
4074
|
+
warnings.push(`ignored "${key}": not a string or {value, evidence} object`);
|
|
4075
|
+
continue;
|
|
4076
|
+
}
|
|
4077
|
+
const entry = parsed.data;
|
|
4078
|
+
const value = (typeof entry === "string" ? entry : entry.value ?? "").trim();
|
|
4079
|
+
if (value === "")
|
|
4080
|
+
continue;
|
|
4081
|
+
const evidence = typeof entry === "string" ? undefined : entry.evidence?.trim();
|
|
4082
|
+
values[key] = { value, ...evidence ? { evidence } : {} };
|
|
4083
|
+
}
|
|
4084
|
+
const notes = outer.data.notes?.trim();
|
|
4085
|
+
return { findings: { values, ...notes ? { notes } : {} }, warnings };
|
|
4086
|
+
}
|
|
4087
|
+
function renderPrepPrompt(template, values) {
|
|
4088
|
+
let out = template;
|
|
4089
|
+
for (const key of PLACEHOLDERS) {
|
|
4090
|
+
out = out.split(`{{${key}}}`).join(values[key]);
|
|
4091
|
+
}
|
|
4092
|
+
return out;
|
|
4093
|
+
}
|
|
4094
|
+
function buildRequestedKeysBlock(keys) {
|
|
4095
|
+
if (keys.length === 0) {
|
|
4096
|
+
return "_Everything is already established. Report an empty findings object and stop._";
|
|
4097
|
+
}
|
|
4098
|
+
const order = new Map(PREPARED_KEYS.map((k, i) => [k, i]));
|
|
4099
|
+
return [...keys].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0)).map((k) => `- ${KEY_BRIEF[k]}`).join(`
|
|
4100
|
+
`);
|
|
4101
|
+
}
|
|
4102
|
+
async function prepRun(ctx, deps) {
|
|
4103
|
+
if (ctx.keys.length === 0) {
|
|
4104
|
+
ctx.emitEvent({
|
|
4105
|
+
type: "prep.skipped",
|
|
4106
|
+
message: "nothing to prepare \u2014 every field is already set"
|
|
4107
|
+
});
|
|
4108
|
+
return { status: "succeeded", summary: "nothing to prepare" };
|
|
4109
|
+
}
|
|
4110
|
+
if (deps.config.sandbox !== "noSandbox" && !deps.hasAuthToken) {
|
|
4111
|
+
ctx.emitEvent({ type: AUTH_MISSING_EVENT2, message: AUTH_MISSING_MESSAGE2 });
|
|
4112
|
+
return { status: "failed", summary: "preparation aborted: auth token missing" };
|
|
4113
|
+
}
|
|
4114
|
+
ctx.emitEvent({
|
|
4115
|
+
type: "prep.started",
|
|
4116
|
+
message: `preparing ${ctx.project.name} \u2014 establishing ${ctx.keys.join(", ")}`,
|
|
4117
|
+
data: { keys: [...ctx.keys] }
|
|
4118
|
+
});
|
|
4119
|
+
const outcome = await deps.executePrepRun(ctx);
|
|
4120
|
+
if (outcome.status === "failed") {
|
|
4121
|
+
ctx.emitEvent({
|
|
4122
|
+
type: "prep.failed",
|
|
4123
|
+
message: `preparation failed: ${errorHeadline2(outcome.error)}`,
|
|
4124
|
+
data: { error: outcome.error }
|
|
4125
|
+
});
|
|
4126
|
+
return { status: "failed", summary: outcome.error };
|
|
4127
|
+
}
|
|
4128
|
+
for (const warning of outcome.warnings) {
|
|
4129
|
+
ctx.emitEvent({ type: "prep.warning", message: `preparation: ${warning}` });
|
|
4130
|
+
}
|
|
4131
|
+
const found = Object.keys(outcome.findings.values);
|
|
4132
|
+
const missed = ctx.keys.filter((k) => !found.includes(k));
|
|
4133
|
+
const summary = found.length === 0 ? "preparation established nothing" : `established ${found.join(", ")}${missed.length > 0 ? ` (could not establish ${missed.join(", ")})` : ""}`;
|
|
4134
|
+
return { status: "succeeded", summary, findings: outcome.findings };
|
|
4135
|
+
}
|
|
4136
|
+
function errorHeadline2(s) {
|
|
4137
|
+
const lines = s.split(`
|
|
4138
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
4139
|
+
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
4140
|
+
return causes.at(-1) ?? lines[0] ?? "";
|
|
4141
|
+
}
|
|
4142
|
+
function createPrepStreamThrottle(emit2, opts = {}) {
|
|
4143
|
+
return createStreamThrottle((e) => emit2({ ...e, type: e.type.replace(/^burn\./, "prep.") }), opts);
|
|
4144
|
+
}
|
|
4145
|
+
function prepTemplatePath() {
|
|
4146
|
+
const here = dirname4(fileURLToPath3(import.meta.url));
|
|
4147
|
+
return join8(resolveSkillsRoot(here), "burner", "prepare-project.md");
|
|
4148
|
+
}
|
|
4149
|
+
async function realExecutePrepRun(ctx, config2, token, model) {
|
|
4150
|
+
const { project } = ctx;
|
|
4151
|
+
const branch = prepBranchName(newId("b").slice(2, 10));
|
|
4152
|
+
const toolchain = readRepoToolchain(project.repoPath);
|
|
4153
|
+
const pm = detectPackageManager(toolchain);
|
|
4154
|
+
const prepared = resolvePreparedSettings(config2, project);
|
|
4155
|
+
const setupCommand = resolveSetupCommand(toolchain, prepared.setupCommand);
|
|
4156
|
+
const workspaceMode = resolveBurnWorkspaceMode(config2);
|
|
4157
|
+
const mounts = [];
|
|
4158
|
+
if (config2.sandbox !== "noSandbox" && pm) {
|
|
4159
|
+
const mount = cacheMountFor(pm, burnCacheDir(pm));
|
|
4160
|
+
if (mount) {
|
|
4161
|
+
mkdirSync4(mount.hostPath, { recursive: true });
|
|
4162
|
+
mounts.push(mount);
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
const template = readFileSync4(prepTemplatePath(), "utf8");
|
|
4166
|
+
const prompt = renderPrepPrompt(template, {
|
|
4167
|
+
REQUESTED_KEYS: buildRequestedKeysBlock(ctx.keys),
|
|
4168
|
+
SETUP_COMMAND: setupCommand ?? "(no install step detected)"
|
|
4169
|
+
});
|
|
4170
|
+
mkdirSync4(logsDir(), { recursive: true });
|
|
4171
|
+
const logFilePath = join8(logsDir(), `prep-${project.id}.log`);
|
|
4172
|
+
const throttle = createPrepStreamThrottle((e) => ctx.emitEvent(e));
|
|
4173
|
+
if (setupCommand) {
|
|
4174
|
+
ctx.emitEvent({
|
|
4175
|
+
type: "prep.setup",
|
|
4176
|
+
message: `installing deps before the agent starts: ${setupCommand}`
|
|
4177
|
+
});
|
|
4178
|
+
}
|
|
4179
|
+
const hookCommand = workspaceMode === "isolated" ? buildIsolatedSetupCommand(branch, setupCommand, pm) : setupCommand;
|
|
4180
|
+
const runOptions = {
|
|
4181
|
+
agent: buildBurnAgent(config2, token, model),
|
|
4182
|
+
sandbox: selectSandbox(config2, mounts),
|
|
4183
|
+
cwd: project.repoPath,
|
|
4184
|
+
prompt,
|
|
4185
|
+
branchStrategy: { type: "branch", branch, baseBranch: project.mainBranch },
|
|
4186
|
+
signal: ctx.signal,
|
|
4187
|
+
name: "prepare",
|
|
4188
|
+
maxIterations: 1,
|
|
4189
|
+
...hookCommand ? {
|
|
4190
|
+
hooks: {
|
|
4191
|
+
sandbox: {
|
|
4192
|
+
onSandboxReady: [{ command: hookCommand, timeoutMs: PREP_SETUP_TIMEOUT_MS }]
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
} : {},
|
|
4196
|
+
logging: { type: "file", path: logFilePath, onAgentStreamEvent: throttle.onEvent }
|
|
4197
|
+
};
|
|
4198
|
+
try {
|
|
4199
|
+
let result;
|
|
4200
|
+
try {
|
|
4201
|
+
result = await run2(runOptions);
|
|
4202
|
+
} catch (err) {
|
|
4203
|
+
throttle.flush();
|
|
4204
|
+
if (ctx.signal.aborted)
|
|
4205
|
+
throw err;
|
|
4206
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4207
|
+
const salvaged = await readFindings(project.repoPath, branch);
|
|
4208
|
+
if (!salvaged)
|
|
4209
|
+
return { status: "failed", error: msg };
|
|
4210
|
+
ctx.emitEvent({
|
|
4211
|
+
type: "prep.salvaged",
|
|
4212
|
+
message: `the agent finished but sandcastle errored on teardown (${errorHeadline2(msg)}) \u2014 reading the findings it committed anyway`
|
|
4213
|
+
});
|
|
4214
|
+
return salvaged;
|
|
4215
|
+
}
|
|
4216
|
+
throttle.flush();
|
|
4217
|
+
if (result.commits.length === 0) {
|
|
4218
|
+
return {
|
|
4219
|
+
status: "failed",
|
|
4220
|
+
error: `agent produced no commits \u2014 no ${PREP_FINDINGS_PATH} landed`
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
4223
|
+
const read = await readFindings(project.repoPath, branch);
|
|
4224
|
+
return read ?? {
|
|
4225
|
+
status: "failed",
|
|
4226
|
+
error: `agent committed, but ${PREP_FINDINGS_PATH} is missing from ${branch}`
|
|
4227
|
+
};
|
|
4228
|
+
} finally {
|
|
4229
|
+
await deletePrepBranch(project.repoPath, branch);
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
async function readFindings(repoPath, branch) {
|
|
4233
|
+
const raw = await readFileAtRef(repoPath, branch, PREP_FINDINGS_PATH);
|
|
4234
|
+
if (raw === undefined)
|
|
4235
|
+
return;
|
|
4236
|
+
try {
|
|
4237
|
+
const { findings, warnings } = parsePrepFindings(raw);
|
|
4238
|
+
return { status: "done", findings, warnings };
|
|
4239
|
+
} catch (e) {
|
|
4240
|
+
return { status: "failed", error: e instanceof Error ? e.message : String(e) };
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
function resolvePrepDeps(project) {
|
|
4244
|
+
const config2 = loadConfig();
|
|
4245
|
+
const token = readTokenFromEnvFile(envPath());
|
|
4246
|
+
const model = resolveModel("prepare", config2, project);
|
|
4247
|
+
return {
|
|
4248
|
+
config: config2,
|
|
4249
|
+
hasAuthToken: token !== undefined,
|
|
4250
|
+
executePrepRun: (c) => realExecutePrepRun(c, config2, token, model)
|
|
4251
|
+
};
|
|
4252
|
+
}
|
|
4253
|
+
async function prepHeadSha(project) {
|
|
4254
|
+
return existsSync8(project.repoPath) ? headSha(project.repoPath, project.mainBranch) : undefined;
|
|
4255
|
+
}
|
|
4256
|
+
var AUTH_MISSING_EVENT2 = "auth.missing", AUTH_MISSING_MESSAGE2 = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PREP_FINDINGS_PATH = ".runcastle/prep.json", PREP_SETUP_TIMEOUT_MS, RawFinding, RawFindings, PLACEHOLDERS, KEY_BRIEF;
|
|
4257
|
+
var init_project_prep = __esm(() => {
|
|
4258
|
+
init_src();
|
|
4259
|
+
init_config_load();
|
|
4260
|
+
init_paths();
|
|
4261
|
+
init_skills_root();
|
|
4262
|
+
init_git();
|
|
4263
|
+
init_ticket_burner();
|
|
4264
|
+
PREP_SETUP_TIMEOUT_MS = 15 * 60000;
|
|
4265
|
+
RawFinding = z3.union([
|
|
4266
|
+
z3.string(),
|
|
4267
|
+
z3.object({
|
|
4268
|
+
value: z3.string().nullish(),
|
|
4269
|
+
evidence: z3.string().nullish()
|
|
4270
|
+
})
|
|
4271
|
+
]);
|
|
4272
|
+
RawFindings = z3.object({ notes: z3.string().nullish() }).catchall(z3.unknown());
|
|
4273
|
+
PLACEHOLDERS = ["REQUESTED_KEYS", "SETUP_COMMAND"];
|
|
4274
|
+
KEY_BRIEF = {
|
|
4275
|
+
setupCommand: "**`setupCommand`** \u2014 the command that takes a clean checkout to a state where the code builds and tests run: dependency install, plus any codegen or contract build that every task would otherwise discover it needed mid-flight (ORM client generation, protobuf, a `contracts:build` step). Run it and report what worked.",
|
|
4276
|
+
verifyCommands: "**`verifyCommands`** \u2014 the exact typecheck / test / lint commands for this repo, one per line. In a monorepo these must include the correct workspace filter names. Run each one and confirm it executes the thing it claims to.",
|
|
4277
|
+
knownFailures: '**`knownFailures`** \u2014 which tests are ALREADY failing on this checkout, before anything is changed. Run the full suite once and report the count plus the failing suite names. "0 known failures" is a real, useful answer.',
|
|
4278
|
+
devCommand: "**`devCommand`** \u2014 the command that starts this project's dev server on a developer's own machine. Read it from config; do not run it here.",
|
|
4279
|
+
dbResetCommand: "**`dbResetCommand`** \u2014 the command that rebuilds the dev database from the migrations currently in the working tree. Read it from config; do not run it here. Omit it entirely if this repo has no database."
|
|
4280
|
+
};
|
|
4281
|
+
});
|
|
4282
|
+
|
|
4283
|
+
// src/services/findings.ts
|
|
4284
|
+
import { and as and5, eq as eq5 } from "drizzle-orm";
|
|
4285
|
+
function isPreparedKey(key) {
|
|
4286
|
+
return PREPARED_SET.has(key);
|
|
4287
|
+
}
|
|
4288
|
+
function findingRow(ctx, projectId, key) {
|
|
4289
|
+
return ctx.db.select({
|
|
4290
|
+
source: projectFindings.source,
|
|
4291
|
+
evidence: projectFindings.evidence,
|
|
4292
|
+
establishedAt: projectFindings.establishedAt,
|
|
4293
|
+
establishedSha: projectFindings.establishedSha
|
|
4294
|
+
}).from(projectFindings).where(and5(eq5(projectFindings.projectId, projectId), eq5(projectFindings.key, key))).get();
|
|
4295
|
+
}
|
|
4296
|
+
function isOverwritable(ctx, projectId, key) {
|
|
4297
|
+
return findingRow(ctx, projectId, key)?.source !== "human";
|
|
4298
|
+
}
|
|
4299
|
+
function recordFinding(ctx, projectId, input) {
|
|
4300
|
+
const { key, value, source, evidence, establishedSha } = input;
|
|
4301
|
+
ctx.db.transaction((tx) => {
|
|
4302
|
+
tx.update(projects).set({ [COLUMN_NAME[key]]: value }).where(eq5(projects.id, projectId)).run();
|
|
4303
|
+
if (value === null) {
|
|
4304
|
+
tx.delete(projectFindings).where(and5(eq5(projectFindings.projectId, projectId), eq5(projectFindings.key, key))).run();
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
tx.insert(projectFindings).values({
|
|
4308
|
+
projectId,
|
|
4309
|
+
key,
|
|
4310
|
+
source,
|
|
4311
|
+
evidence: evidence ?? null,
|
|
4312
|
+
establishedAt: Date.now(),
|
|
4313
|
+
establishedSha: establishedSha ?? null
|
|
4314
|
+
}).onConflictDoUpdate({
|
|
4315
|
+
target: [projectFindings.projectId, projectFindings.key],
|
|
4316
|
+
set: {
|
|
4317
|
+
source,
|
|
4318
|
+
evidence: evidence ?? null,
|
|
4319
|
+
establishedAt: Date.now(),
|
|
4320
|
+
establishedSha: establishedSha ?? null
|
|
4321
|
+
}
|
|
4322
|
+
}).run();
|
|
4323
|
+
});
|
|
4324
|
+
}
|
|
4325
|
+
function recordHuman(ctx, projectId, key, value) {
|
|
4326
|
+
if (!isPreparedKey(key))
|
|
4327
|
+
return;
|
|
4328
|
+
const existing = findingRow(ctx, projectId, key);
|
|
4329
|
+
if (value === null) {
|
|
4330
|
+
if (existing) {
|
|
4331
|
+
ctx.db.delete(projectFindings).where(and5(eq5(projectFindings.projectId, projectId), eq5(projectFindings.key, key))).run();
|
|
4332
|
+
}
|
|
4333
|
+
return;
|
|
4334
|
+
}
|
|
4335
|
+
ctx.db.insert(projectFindings).values({
|
|
4336
|
+
projectId,
|
|
4337
|
+
key,
|
|
4338
|
+
source: "human",
|
|
4339
|
+
evidence: null,
|
|
4340
|
+
establishedAt: Date.now(),
|
|
4341
|
+
establishedSha: null
|
|
4342
|
+
}).onConflictDoUpdate({
|
|
4343
|
+
target: [projectFindings.projectId, projectFindings.key],
|
|
4344
|
+
set: { source: "human", evidence: null, establishedAt: Date.now(), establishedSha: null }
|
|
4345
|
+
}).run();
|
|
4346
|
+
}
|
|
4347
|
+
async function listFindings(ctx, project) {
|
|
4348
|
+
const rows = ctx.db.select().from(projectFindings).where(eq5(projectFindings.projectId, project.id)).all();
|
|
4349
|
+
const distances = new Map;
|
|
4350
|
+
for (const row of rows) {
|
|
4351
|
+
const sha = row.establishedSha;
|
|
4352
|
+
if (!sha || distances.has(sha))
|
|
4353
|
+
continue;
|
|
4354
|
+
distances.set(sha, await commitsSince(project.repoPath, sha, project.mainBranch));
|
|
4355
|
+
}
|
|
4356
|
+
const order = new Map(PREPARED_KEYS.map((k, i) => [k, i]));
|
|
4357
|
+
return rows.filter((row) => isPreparedKey(row.key)).sort((a, b) => (order.get(a.key) ?? 0) - (order.get(b.key) ?? 0)).map((row) => {
|
|
4358
|
+
const staleCommits = row.establishedSha ? distances.get(row.establishedSha) : undefined;
|
|
4359
|
+
return {
|
|
4360
|
+
key: row.key,
|
|
4361
|
+
source: row.source,
|
|
4362
|
+
...row.evidence ? { evidence: row.evidence } : {},
|
|
4363
|
+
establishedAt: row.establishedAt,
|
|
4364
|
+
...row.establishedSha ? { establishedSha: row.establishedSha } : {},
|
|
4365
|
+
...staleCommits !== undefined ? { staleCommits } : {}
|
|
4366
|
+
};
|
|
4367
|
+
});
|
|
4368
|
+
}
|
|
4369
|
+
function unsetPreparedKeys(project) {
|
|
4370
|
+
return PREPARED_KEYS.filter((key) => {
|
|
4371
|
+
const value = project[key];
|
|
4372
|
+
return value === undefined || value.trim() === "";
|
|
4373
|
+
});
|
|
4374
|
+
}
|
|
4375
|
+
var VALUE_COLUMN, COLUMN_NAME, PREPARED_SET;
|
|
4376
|
+
var init_findings = __esm(() => {
|
|
4377
|
+
init_src();
|
|
4378
|
+
init_schema();
|
|
4379
|
+
init_git();
|
|
4380
|
+
VALUE_COLUMN = {
|
|
4381
|
+
setupCommand: projects.setupCommand,
|
|
4382
|
+
verifyCommands: projects.verifyCommands,
|
|
4383
|
+
knownFailures: projects.knownFailures,
|
|
4384
|
+
devCommand: projects.devCommand,
|
|
4385
|
+
dbResetCommand: projects.dbResetCommand
|
|
4386
|
+
};
|
|
4387
|
+
COLUMN_NAME = {
|
|
4388
|
+
setupCommand: "setupCommand",
|
|
4389
|
+
verifyCommands: "verifyCommands",
|
|
4390
|
+
knownFailures: "knownFailures",
|
|
4391
|
+
devCommand: "devCommand",
|
|
4392
|
+
dbResetCommand: "dbResetCommand"
|
|
4393
|
+
};
|
|
4394
|
+
PREPARED_SET = new Set(PREPARED_KEYS);
|
|
4395
|
+
});
|
|
4396
|
+
|
|
4397
|
+
// src/services/prep.ts
|
|
4398
|
+
import { desc as desc3, eq as eq6 } from "drizzle-orm";
|
|
4399
|
+
function isPreparing(projectId) {
|
|
4400
|
+
return controllers.has(projectId);
|
|
4401
|
+
}
|
|
4402
|
+
function keysToPrepare(ctx, project, opts = {}) {
|
|
4403
|
+
const unset = new Set(unsetPreparedKeys(project));
|
|
4404
|
+
const candidates = opts.keys ?? (opts.refresh ? PREPARED_KEYS : [...unset]);
|
|
4405
|
+
return candidates.filter((key) => (opts.refresh || unset.has(key)) && isOverwritable(ctx, project.id, key));
|
|
4406
|
+
}
|
|
4407
|
+
async function startPrep(ctx, projectId, opts = {}) {
|
|
4408
|
+
const project = requireProjectById(ctx, projectId);
|
|
4409
|
+
if (controllers.has(projectId)) {
|
|
4410
|
+
throw new InvalidInputError(`${project.name} is already being prepared`);
|
|
4411
|
+
}
|
|
4412
|
+
const keys = keysToPrepare(ctx, project, opts);
|
|
4413
|
+
const prepId = newId("prep");
|
|
4414
|
+
const headSha2 = await prepHeadSha(project);
|
|
4415
|
+
ctx.db.insert(projectPreps).values({
|
|
4416
|
+
id: prepId,
|
|
4417
|
+
projectId,
|
|
4418
|
+
status: "running",
|
|
4419
|
+
startedAt: Date.now(),
|
|
4420
|
+
endedAt: null,
|
|
4421
|
+
summary: null,
|
|
4422
|
+
headSha: headSha2 ?? null
|
|
4423
|
+
}).run();
|
|
4424
|
+
emitProject(ctx, projectId, {
|
|
4425
|
+
type: "prep.run.started",
|
|
4426
|
+
message: keys.length === 0 ? `preparing ${project.name} \u2014 nothing left to establish` : `preparing ${project.name} \u2014 ${keys.length} field(s) to establish`,
|
|
4427
|
+
data: { prepId, keys }
|
|
4428
|
+
});
|
|
4429
|
+
const controller = new AbortController;
|
|
4430
|
+
controllers.set(projectId, { runId: prepId, controller });
|
|
4431
|
+
const prepCtx = {
|
|
4432
|
+
project,
|
|
4433
|
+
keys,
|
|
4434
|
+
emitEvent: (e) => {
|
|
4435
|
+
emitProject(ctx, projectId, { type: e.type, message: e.message, data: e.data });
|
|
4436
|
+
},
|
|
4437
|
+
signal: controller.signal
|
|
4438
|
+
};
|
|
4439
|
+
const deps = opts.deps ?? resolvePrepDeps(project);
|
|
4440
|
+
const done = finalize(ctx, project, prepId, prepCtx, deps, controller, headSha2);
|
|
4441
|
+
return { prepId, keys, done };
|
|
4442
|
+
}
|
|
4443
|
+
function cancelPrep(projectId) {
|
|
4444
|
+
controllers.get(projectId)?.controller.abort();
|
|
4445
|
+
}
|
|
4446
|
+
async function finalize(ctx, project, prepId, prepCtx, deps, controller, headSha2) {
|
|
4447
|
+
let status = "failed";
|
|
4448
|
+
let summary = "preparation failed";
|
|
4449
|
+
let findings;
|
|
4450
|
+
try {
|
|
4451
|
+
const result = await prepRun(prepCtx, deps);
|
|
4452
|
+
status = result.status === "succeeded" ? "succeeded" : "failed";
|
|
4453
|
+
summary = result.summary;
|
|
4454
|
+
findings = result.findings;
|
|
4455
|
+
} catch (e) {
|
|
4456
|
+
if (controller.signal.aborted) {
|
|
4457
|
+
status = "cancelled";
|
|
4458
|
+
summary = "preparation cancelled";
|
|
4459
|
+
} else {
|
|
4460
|
+
status = "failed";
|
|
4461
|
+
summary = e instanceof Error ? e.message : "preparation failed";
|
|
4462
|
+
}
|
|
4463
|
+
} finally {
|
|
4464
|
+
controllers.delete(project.id);
|
|
4465
|
+
}
|
|
4466
|
+
const applied = findings ? applyFindings(ctx, project, findings, headSha2) : [];
|
|
4467
|
+
ctx.db.update(projectPreps).set({ status, endedAt: Date.now(), summary }).where(eq6(projectPreps.id, prepId)).run();
|
|
4468
|
+
emitProject(ctx, project.id, {
|
|
4469
|
+
type: "prep.run.finished",
|
|
4470
|
+
message: `preparation ${status}: ${summary}`,
|
|
4471
|
+
data: { prepId, status, summary, applied }
|
|
4472
|
+
});
|
|
4473
|
+
}
|
|
4474
|
+
function applyFindings(ctx, project, findings, headSha2) {
|
|
4475
|
+
const applied = [];
|
|
4476
|
+
for (const [key, finding] of Object.entries(findings.values)) {
|
|
4477
|
+
if (!isOverwritable(ctx, project.id, key)) {
|
|
4478
|
+
emitProject(ctx, project.id, {
|
|
4479
|
+
type: "prep.skipped_field",
|
|
4480
|
+
message: `kept your ${key} \u2014 preparation proposed "${truncate(finding.value)}" but you set this by hand`,
|
|
4481
|
+
data: { key, proposed: finding.value }
|
|
4482
|
+
});
|
|
4483
|
+
continue;
|
|
4484
|
+
}
|
|
4485
|
+
recordFinding(ctx, project.id, {
|
|
4486
|
+
key,
|
|
4487
|
+
value: finding.value,
|
|
4488
|
+
source: "prep",
|
|
4489
|
+
...finding.evidence ? { evidence: finding.evidence } : {},
|
|
4490
|
+
...headSha2 ? { establishedSha: headSha2 } : {}
|
|
4491
|
+
});
|
|
4492
|
+
applied.push(key);
|
|
4493
|
+
emitProject(ctx, project.id, {
|
|
4494
|
+
type: "prep.established",
|
|
4495
|
+
message: `${key}: ${truncate(finding.value)}`,
|
|
4496
|
+
data: { key, value: finding.value, evidence: finding.evidence }
|
|
4497
|
+
});
|
|
4498
|
+
}
|
|
4499
|
+
if (findings.notes) {
|
|
4500
|
+
emitProject(ctx, project.id, {
|
|
4501
|
+
type: "prep.notes",
|
|
4502
|
+
message: `preparation notes: ${findings.notes}`,
|
|
4503
|
+
data: { notes: findings.notes }
|
|
4504
|
+
});
|
|
4505
|
+
}
|
|
4506
|
+
return applied;
|
|
4507
|
+
}
|
|
4508
|
+
function truncate(value, max = 120) {
|
|
4509
|
+
const oneLine = value.replace(/\s*\n\s*/g, " \xB7 ").trim();
|
|
4510
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}\u2026` : oneLine;
|
|
4511
|
+
}
|
|
4512
|
+
function latestPrep(ctx, projectId) {
|
|
4513
|
+
const row = ctx.db.select().from(projectPreps).where(eq6(projectPreps.projectId, projectId)).orderBy(desc3(projectPreps.startedAt)).get();
|
|
4514
|
+
if (!row)
|
|
4515
|
+
return null;
|
|
4516
|
+
return {
|
|
4517
|
+
id: row.id,
|
|
4518
|
+
projectId: row.projectId,
|
|
4519
|
+
status: row.status,
|
|
4520
|
+
startedAt: row.startedAt,
|
|
4521
|
+
...row.endedAt !== null ? { endedAt: row.endedAt } : {},
|
|
4522
|
+
...row.summary !== null ? { summary: row.summary } : {},
|
|
4523
|
+
...row.headSha !== null ? { headSha: row.headSha } : {}
|
|
4524
|
+
};
|
|
4525
|
+
}
|
|
4526
|
+
function reconcilePreps(ctx) {
|
|
4527
|
+
const stale = ctx.db.select({ id: projectPreps.id, projectId: projectPreps.projectId }).from(projectPreps).where(eq6(projectPreps.status, "running")).all().filter((row) => !controllers.has(row.projectId));
|
|
4528
|
+
for (const row of stale) {
|
|
4529
|
+
ctx.db.update(projectPreps).set({ status: "failed", endedAt: Date.now(), summary: "server stopped while preparing" }).where(eq6(projectPreps.id, row.id)).run();
|
|
4530
|
+
}
|
|
4531
|
+
return stale.length;
|
|
4532
|
+
}
|
|
4533
|
+
var controllers;
|
|
4534
|
+
var init_prep = __esm(() => {
|
|
4535
|
+
init_src();
|
|
4536
|
+
init_schema();
|
|
4537
|
+
init_errors();
|
|
4538
|
+
init_project_prep();
|
|
4539
|
+
init_events();
|
|
4540
|
+
init_findings();
|
|
4541
|
+
init_repo();
|
|
4542
|
+
controllers = new Map;
|
|
4543
|
+
});
|
|
4544
|
+
|
|
4545
|
+
// src/launcher/runtime.ts
|
|
4546
|
+
function setRuntimeCtx(ctx) {
|
|
4547
|
+
current = ctx;
|
|
4548
|
+
}
|
|
4549
|
+
async function getRuntimeCtx() {
|
|
4550
|
+
if (current)
|
|
4551
|
+
return current;
|
|
4552
|
+
const [{ createDb: createDb2 }, { dbPath: dbPath2 }, { loadConfig: loadConfig2 }] = await Promise.all([
|
|
4553
|
+
Promise.resolve().then(() => (init_client(), exports_client)),
|
|
4554
|
+
Promise.resolve().then(() => (init_paths(), exports_paths)),
|
|
4555
|
+
Promise.resolve().then(() => (init_config_load(), exports_config_load))
|
|
4556
|
+
]);
|
|
4557
|
+
const db = createDb2(dbPath2());
|
|
4558
|
+
current = { db, config: loadConfig2() };
|
|
4559
|
+
return current;
|
|
4560
|
+
}
|
|
4561
|
+
var current = null;
|
|
4562
|
+
|
|
4563
|
+
// src/services/feature-docs.ts
|
|
4564
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4565
|
+
import { join as join9 } from "path";
|
|
4566
|
+
function featureDocsDir(project, feature) {
|
|
4567
|
+
const relSegments = featureDocsRel(feature.slug).split("/");
|
|
4568
|
+
const worktree = worktreeDir(project.id, feature.slug);
|
|
4569
|
+
const base = existsSync9(worktree) ? worktree : project.repoPath;
|
|
4570
|
+
return join9(base, ...relSegments);
|
|
4571
|
+
}
|
|
4572
|
+
function featureDocPath(project, feature, fileName) {
|
|
4573
|
+
return join9(featureDocsDir(project, feature), fileName);
|
|
4574
|
+
}
|
|
4575
|
+
var init_feature_docs = __esm(() => {
|
|
4576
|
+
init_paths();
|
|
4577
|
+
});
|
|
4578
|
+
|
|
4579
|
+
// src/services/tickets.ts
|
|
4580
|
+
import { and as and6, asc as asc3, eq as eq7 } from "drizzle-orm";
|
|
4581
|
+
function rowToTicket(row) {
|
|
4582
|
+
return {
|
|
4583
|
+
id: row.id,
|
|
4584
|
+
featureId: row.featureId,
|
|
4585
|
+
seq: row.seq,
|
|
4586
|
+
title: row.title,
|
|
4587
|
+
goal: row.goal,
|
|
4588
|
+
context: row.context,
|
|
4589
|
+
acceptanceCriteria: row.acceptanceCriteria,
|
|
4590
|
+
seams: row.seams,
|
|
4591
|
+
blockedBy: row.blockedBy,
|
|
4592
|
+
status: row.status,
|
|
4593
|
+
commits: row.commits,
|
|
4594
|
+
error: row.error ?? undefined,
|
|
4595
|
+
attemptBranch: row.attemptBranch ?? undefined,
|
|
4596
|
+
conflictFiles: row.conflictFiles ?? undefined
|
|
4597
|
+
};
|
|
4598
|
+
}
|
|
4599
|
+
function resolveBlocking(inputs, startSeq) {
|
|
4600
|
+
try {
|
|
4601
|
+
return resolveBatchBlocking(inputs, { startSeq });
|
|
4602
|
+
} catch (e) {
|
|
4603
|
+
if (e instanceof BlockingEdgeError)
|
|
4604
|
+
throw new InvalidInputError(e.message);
|
|
4605
|
+
throw e;
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
function listByFeature2(ctx, featureId) {
|
|
4609
|
+
return ctx.db.select().from(tickets).where(eq7(tickets.featureId, featureId)).orderBy(asc3(tickets.seq)).all().map(rowToTicket);
|
|
4610
|
+
}
|
|
4611
|
+
function storeTickets(ctx, featureId, inputs) {
|
|
4612
|
+
if (inputs.length === 0)
|
|
4613
|
+
return [];
|
|
4614
|
+
const existing = ctx.db.select({ seq: tickets.seq }).from(tickets).where(eq7(tickets.featureId, featureId)).all();
|
|
4615
|
+
const startSeq = existing.reduce((max, r) => Math.max(max, r.seq), 0) + 1;
|
|
4616
|
+
const resolved = resolveBlocking(inputs, startSeq);
|
|
4617
|
+
const rows = inputs.map((t, i) => ({
|
|
4618
|
+
id: newId("tkt"),
|
|
4619
|
+
featureId,
|
|
4620
|
+
seq: resolved[i].seq,
|
|
4621
|
+
title: t.title,
|
|
4622
|
+
goal: t.goal,
|
|
4623
|
+
context: t.context,
|
|
4624
|
+
acceptanceCriteria: t.acceptanceCriteria,
|
|
4625
|
+
seams: t.seams,
|
|
4626
|
+
blockedBy: resolved[i].blockedBy,
|
|
4627
|
+
status: "pending",
|
|
4628
|
+
commits: [],
|
|
4629
|
+
error: null,
|
|
4630
|
+
attemptBranch: null,
|
|
4631
|
+
conflictFiles: null
|
|
4632
|
+
}));
|
|
4633
|
+
ctx.db.insert(tickets).values(rows).run();
|
|
4634
|
+
emit(ctx, featureId, {
|
|
4635
|
+
type: "tickets.stored",
|
|
4636
|
+
message: `${rows.length} ticket(s) stored`,
|
|
4637
|
+
data: { count: rows.length, seqs: rows.map((r) => r.seq) }
|
|
4638
|
+
});
|
|
4639
|
+
return rows.map(rowToTicket);
|
|
4640
|
+
}
|
|
4641
|
+
function getTicket(ctx, id) {
|
|
4642
|
+
const row = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
4643
|
+
if (!row)
|
|
4644
|
+
throw new NotFoundError(`ticket ${id} not found`);
|
|
4645
|
+
return rowToTicket(row);
|
|
4646
|
+
}
|
|
4647
|
+
function assertMutable(t, verb) {
|
|
4648
|
+
if (!MUTABLE_STATUSES.has(t.status)) {
|
|
4649
|
+
throw new InvalidInputError(`cannot ${verb} ticket ${t.seq} \u2014 it is ${t.status}; only pending or failed tickets can be ${verb}${verb.endsWith("l") ? "led" : "ed"}`);
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
function editTicket(ctx, id, patch) {
|
|
4653
|
+
const current2 = getTicket(ctx, id);
|
|
4654
|
+
assertMutable(current2, "edit");
|
|
4655
|
+
const set = {};
|
|
4656
|
+
if (patch.title !== undefined)
|
|
4657
|
+
set.title = patch.title;
|
|
4658
|
+
if (patch.goal !== undefined)
|
|
4659
|
+
set.goal = patch.goal;
|
|
4660
|
+
if (patch.context !== undefined)
|
|
4661
|
+
set.context = patch.context;
|
|
4662
|
+
if (patch.acceptanceCriteria !== undefined)
|
|
4663
|
+
set.acceptanceCriteria = patch.acceptanceCriteria;
|
|
4664
|
+
if (patch.seams !== undefined)
|
|
4665
|
+
set.seams = patch.seams;
|
|
4666
|
+
if (Object.keys(set).length === 0) {
|
|
4667
|
+
throw new InvalidInputError("nothing to edit \u2014 pass at least one content field");
|
|
4668
|
+
}
|
|
4669
|
+
ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
|
|
4670
|
+
emit(ctx, current2.featureId, {
|
|
4671
|
+
type: "ticket.edited",
|
|
4672
|
+
message: `ticket ${current2.seq} content edited (${Object.keys(set).join(", ")})`,
|
|
4673
|
+
ticketId: id,
|
|
4674
|
+
data: { fields: Object.keys(set) }
|
|
4675
|
+
});
|
|
4676
|
+
return getTicket(ctx, id);
|
|
4677
|
+
}
|
|
4678
|
+
function cancelTicket(ctx, id, reason) {
|
|
4679
|
+
const current2 = getTicket(ctx, id);
|
|
4680
|
+
assertMutable(current2, "cancel");
|
|
4681
|
+
ctx.db.update(tickets).set({ status: "cancelled", error: reason?.trim() ? reason.trim() : null }).where(eq7(tickets.id, id)).run();
|
|
4682
|
+
emit(ctx, current2.featureId, {
|
|
4683
|
+
type: "ticket.cancelled",
|
|
4684
|
+
message: `ticket ${current2.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
|
|
4685
|
+
ticketId: id,
|
|
4686
|
+
data: { reason: reason ?? null }
|
|
4687
|
+
});
|
|
4688
|
+
return getTicket(ctx, id);
|
|
4689
|
+
}
|
|
4690
|
+
function sweepOrphanedBurning(ctx, featureId, reason) {
|
|
4691
|
+
const orphaned = ctx.db.select().from(tickets).where(and6(eq7(tickets.featureId, featureId), eq7(tickets.status, "burning"))).all().map(rowToTicket);
|
|
4692
|
+
for (const t of orphaned) {
|
|
4693
|
+
updateTicket(ctx, t.id, { status: "failed", error: reason });
|
|
4694
|
+
emit(ctx, featureId, {
|
|
4695
|
+
type: "ticket.failed",
|
|
4696
|
+
message: `ticket ${t.seq} failed: ${reason}`,
|
|
4697
|
+
ticketId: t.id,
|
|
4698
|
+
data: { error: reason, orphaned: true }
|
|
4699
|
+
});
|
|
4700
|
+
}
|
|
4701
|
+
return orphaned.map((t) => ({ ...t, status: "failed", error: reason }));
|
|
4702
|
+
}
|
|
4703
|
+
function updateTicket(ctx, id, patch) {
|
|
4704
|
+
const current2 = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
4705
|
+
if (!current2)
|
|
4706
|
+
throw new NotFoundError(`ticket ${id} not found`);
|
|
4707
|
+
const set = {};
|
|
4708
|
+
if (patch.status !== undefined)
|
|
4709
|
+
set.status = patch.status;
|
|
4710
|
+
if (patch.commits !== undefined)
|
|
4711
|
+
set.commits = patch.commits;
|
|
4712
|
+
if (patch.error !== undefined)
|
|
4713
|
+
set.error = patch.error;
|
|
4714
|
+
if (patch.attemptBranch !== undefined)
|
|
4715
|
+
set.attemptBranch = patch.attemptBranch;
|
|
4716
|
+
if (patch.conflictFiles !== undefined)
|
|
4717
|
+
set.conflictFiles = patch.conflictFiles;
|
|
4718
|
+
ctx.db.update(tickets).set(set).where(eq7(tickets.id, id)).run();
|
|
4719
|
+
emit(ctx, current2.featureId, {
|
|
4720
|
+
type: "ticket.updated",
|
|
4721
|
+
message: `ticket ${current2.seq} \u2192 ${patch.status ?? current2.status}`,
|
|
4722
|
+
ticketId: id,
|
|
4723
|
+
data: patch
|
|
4724
|
+
});
|
|
4725
|
+
const updated = ctx.db.select().from(tickets).where(eq7(tickets.id, id)).get();
|
|
4726
|
+
return rowToTicket(updated);
|
|
4727
|
+
}
|
|
4728
|
+
var MUTABLE_STATUSES;
|
|
4729
|
+
var init_tickets = __esm(() => {
|
|
4730
|
+
init_src();
|
|
4731
|
+
init_schema();
|
|
4732
|
+
init_errors();
|
|
4733
|
+
init_events();
|
|
4734
|
+
MUTABLE_STATUSES = new Set(["pending", "failed"]);
|
|
4735
|
+
});
|
|
4736
|
+
|
|
4737
|
+
// src/services/gates.ts
|
|
4738
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4739
|
+
function checkGate(ctx, check, feature) {
|
|
4740
|
+
switch (check) {
|
|
4741
|
+
case "decisions-file-exists":
|
|
4742
|
+
return fileGate(ctx, feature, "decisions.md", "run the ideation session to capture decisions first");
|
|
4743
|
+
case "all-waypoints-terminal": {
|
|
4744
|
+
const wps = listByFeature(ctx, feature.id);
|
|
4745
|
+
if (wps.length === 0) {
|
|
4746
|
+
return { satisfied: false, reason: "no waypoints charted yet \u2014 chart the map before converging" };
|
|
4747
|
+
}
|
|
4748
|
+
const open = wps.filter((w) => w.status !== "resolved" && w.status !== "dropped");
|
|
4749
|
+
return open.length === 0 ? { satisfied: true } : {
|
|
4750
|
+
satisfied: false,
|
|
4751
|
+
reason: notYetTerminal("waypoint", open.map((w) => w.status), ["open", "claimed"])
|
|
4752
|
+
};
|
|
4753
|
+
}
|
|
4754
|
+
case "spec-file-exists":
|
|
4755
|
+
return fileGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
|
|
4756
|
+
case "tickets-approved": {
|
|
4757
|
+
const count = listByFeature2(ctx, feature.id).filter((t) => t.status !== "cancelled").length;
|
|
4758
|
+
return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
|
|
4759
|
+
}
|
|
4760
|
+
case "all-tickets-terminal": {
|
|
4761
|
+
const tickets2 = listByFeature2(ctx, feature.id);
|
|
4762
|
+
if (tickets2.length === 0)
|
|
4763
|
+
return { satisfied: false, reason: "no tickets have run yet" };
|
|
4764
|
+
const open = tickets2.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
4765
|
+
return open.length === 0 ? { satisfied: true } : {
|
|
4766
|
+
satisfied: false,
|
|
4767
|
+
reason: notYetTerminal("ticket", open.map((t) => t.status), ["pending", "burning"])
|
|
4768
|
+
};
|
|
4769
|
+
}
|
|
4770
|
+
case "human-merge":
|
|
4771
|
+
return { satisfied: false, reason: "use the Merge button to ship" };
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
function notYetTerminal(noun, statuses, order) {
|
|
4775
|
+
const counts = new Map;
|
|
4776
|
+
for (const s of statuses)
|
|
4777
|
+
counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
4778
|
+
const ordered = [
|
|
4779
|
+
...order.filter((s) => counts.has(s)),
|
|
4780
|
+
...[...counts.keys()].filter((s) => !order.includes(s))
|
|
4781
|
+
];
|
|
4782
|
+
const breakdown = ordered.map((s) => `${counts.get(s)} ${s}`).join(", ");
|
|
4783
|
+
return `${statuses.length} ${statuses.length === 1 ? noun : `${noun}s`} not yet terminal (${breakdown})`;
|
|
4784
|
+
}
|
|
4785
|
+
function fileGate(ctx, feature, fileName, reason) {
|
|
4786
|
+
const project = projectForFeature(ctx, feature);
|
|
4787
|
+
return existsSync10(featureDocPath(project, feature, fileName)) ? { satisfied: true } : { satisfied: false, reason };
|
|
4788
|
+
}
|
|
4789
|
+
function overrideGate(ctx, featureId, gate, reason) {
|
|
4790
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
4791
|
+
ctx.db.insert(gateOverrides).values({ featureId, gate, reason, ts: Date.now() }).run();
|
|
4792
|
+
emit(ctx, featureId, {
|
|
4793
|
+
type: "gate.overridden",
|
|
4794
|
+
message: `gate ${gate} overridden: ${reason}`,
|
|
4795
|
+
data: { gate, reason }
|
|
4796
|
+
});
|
|
4797
|
+
const next = nextPhase(feature);
|
|
4798
|
+
if (next)
|
|
4799
|
+
return setPhase(ctx, featureId, next, "phase.advanced", `advanced to ${next} (override)`);
|
|
4800
|
+
return feature;
|
|
4801
|
+
}
|
|
4802
|
+
var init_gates = __esm(() => {
|
|
4803
|
+
init_src();
|
|
4804
|
+
init_schema();
|
|
4805
|
+
init_events();
|
|
4806
|
+
init_feature_docs();
|
|
4807
|
+
init_repo();
|
|
4808
|
+
init_tickets();
|
|
4809
|
+
init_waypoints();
|
|
4810
|
+
});
|
|
4811
|
+
|
|
4812
|
+
// src/services/knowledge.ts
|
|
4813
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, writeFileSync } from "fs";
|
|
4814
|
+
import { join as join10, relative, resolve as resolve5, sep } from "path";
|
|
4815
|
+
function scaffoldDocs(ctx, feature) {
|
|
4816
|
+
const project = projectForFeature(ctx, feature);
|
|
4817
|
+
const dir = featureDocsDir(project, feature);
|
|
4818
|
+
mkdirSync5(dir, { recursive: true });
|
|
4819
|
+
const briefPath = featureDocPath(project, feature, "brief.md");
|
|
4820
|
+
if (!existsSync11(briefPath)) {
|
|
4821
|
+
const created = new Date().toISOString();
|
|
4822
|
+
const brief = [
|
|
4823
|
+
`# ${feature.title}`,
|
|
4824
|
+
"",
|
|
4825
|
+
feature.oneLiner,
|
|
4826
|
+
"",
|
|
4827
|
+
`- Slug: ${feature.slug}`,
|
|
4828
|
+
`- Created: ${created}`,
|
|
4829
|
+
""
|
|
4830
|
+
].join(`
|
|
4831
|
+
`);
|
|
4832
|
+
writeFileSync(briefPath, brief, "utf8");
|
|
4833
|
+
}
|
|
4834
|
+
if (feature.mapped) {
|
|
4835
|
+
scaffoldMapDoc(project, feature);
|
|
3754
4836
|
}
|
|
4837
|
+
emit(ctx, feature.id, {
|
|
4838
|
+
type: "docs.scaffolded",
|
|
4839
|
+
message: feature.mapped ? `scaffolded docs/features/${feature.slug}/{brief,map}.md` : `scaffolded docs/features/${feature.slug}/brief.md`
|
|
4840
|
+
});
|
|
3755
4841
|
}
|
|
3756
|
-
function
|
|
3757
|
-
|
|
3758
|
-
const
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
executeTicketRun: (c, ticket) => realExecuteTicketRun(c, ticket, config2, token, model, land, ensureIsolatedPushTarget)
|
|
4842
|
+
function scaffoldMapDoc(project, feature, seed) {
|
|
4843
|
+
mkdirSync5(featureDocsDir(project, feature), { recursive: true });
|
|
4844
|
+
const mapPath = featureDocPath(project, feature, "map.md");
|
|
4845
|
+
if (existsSync11(mapPath))
|
|
4846
|
+
return;
|
|
4847
|
+
writeFileSync(mapPath, mapDocBody(feature, seed), "utf8");
|
|
4848
|
+
}
|
|
4849
|
+
function mapDocBody(feature, seed) {
|
|
4850
|
+
const seeded = {
|
|
4851
|
+
Destination: seed?.destination?.trim(),
|
|
4852
|
+
Notes: seed?.notes?.trim()
|
|
3768
4853
|
};
|
|
4854
|
+
const lines = [`# ${feature.title} \u2014 map`, ""];
|
|
4855
|
+
for (const section of MAP_SECTIONS) {
|
|
4856
|
+
lines.push(`## ${section}`, "");
|
|
4857
|
+
const body = seeded[section];
|
|
4858
|
+
if (body)
|
|
4859
|
+
lines.push(body, "");
|
|
4860
|
+
}
|
|
4861
|
+
return lines.join(`
|
|
4862
|
+
`);
|
|
3769
4863
|
}
|
|
3770
|
-
function
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
4864
|
+
function listDocs(ctx, feature) {
|
|
4865
|
+
const project = projectForFeature(ctx, feature);
|
|
4866
|
+
const dir = featureDocsDir(project, feature);
|
|
4867
|
+
if (!existsSync11(dir))
|
|
4868
|
+
return [];
|
|
4869
|
+
return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join10(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
4870
|
+
}
|
|
4871
|
+
function readDoc(ctx, feature, relPath) {
|
|
4872
|
+
const project = projectForFeature(ctx, feature);
|
|
4873
|
+
const dir = featureDocsDir(project, feature);
|
|
4874
|
+
const root = resolve5(dir);
|
|
4875
|
+
const target = resolve5(dir, relPath);
|
|
4876
|
+
const rel = relative(root, target);
|
|
4877
|
+
if (rel === "" || rel.startsWith("..") || rel.split(sep).includes("..")) {
|
|
4878
|
+
throw new InvalidInputError(`doc path escapes the feature docs dir: ${relPath}`);
|
|
3778
4879
|
}
|
|
3779
|
-
|
|
3780
|
-
|
|
4880
|
+
if (!existsSync11(target))
|
|
4881
|
+
throw new NotFoundError(`doc not found: ${relPath}`);
|
|
4882
|
+
return { content: readFileSync5(target, "utf8") };
|
|
3781
4883
|
}
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
];
|
|
3804
|
-
FATAL_ERROR_PATTERNS = [
|
|
3805
|
-
/invalid (api key|x-api-key)/i,
|
|
3806
|
-
/authentication|unauthorized|permission denied/i,
|
|
3807
|
-
/credit balance|billing/i,
|
|
3808
|
-
/oauth token|setup-token/i,
|
|
3809
|
-
/issue with the selected model|model not found|unknown model/i,
|
|
3810
|
-
/does not support resumeSession|resumeSession .* not found/i
|
|
3811
|
-
];
|
|
3812
|
-
RETRYABLE_ERROR_PATTERNS = [
|
|
3813
|
-
/exited with code \d+/i,
|
|
3814
|
-
/idle timeout|AgentIdleTimeout/i,
|
|
3815
|
-
/connection (closed|error|refused|reset)|socket hang up|fetch failed/i,
|
|
3816
|
-
/econnreset|etimedout|econnrefused|epipe|eai_again/i,
|
|
3817
|
-
/overloaded|rate.?limit|too many requests/i,
|
|
3818
|
-
/internal server error|service unavailable|bad gateway|gateway timeout/i,
|
|
3819
|
-
/\bapi error\b/i,
|
|
3820
|
-
/session capture failed/i
|
|
4884
|
+
function readTitle(path, fallback) {
|
|
4885
|
+
try {
|
|
4886
|
+
const firstLine = readFileSync5(path, "utf8").split(`
|
|
4887
|
+
`, 1)[0] ?? "";
|
|
4888
|
+
const heading = firstLine.match(/^#\s+(.+)$/);
|
|
4889
|
+
return heading ? heading[1].trim() : fallback;
|
|
4890
|
+
} catch {
|
|
4891
|
+
return fallback;
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
var MAP_SECTIONS;
|
|
4895
|
+
var init_knowledge = __esm(() => {
|
|
4896
|
+
init_errors();
|
|
4897
|
+
init_events();
|
|
4898
|
+
init_feature_docs();
|
|
4899
|
+
init_repo();
|
|
4900
|
+
MAP_SECTIONS = [
|
|
4901
|
+
"Destination",
|
|
4902
|
+
"Notes",
|
|
4903
|
+
"Not yet specified",
|
|
4904
|
+
"Out of scope"
|
|
3821
4905
|
];
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
4906
|
+
});
|
|
4907
|
+
|
|
4908
|
+
// src/pty/end-session.ts
|
|
4909
|
+
function endSession(ctx, sessionId) {
|
|
4910
|
+
const registry = ptyRegistry();
|
|
4911
|
+
const killed = registry.kill(sessionId);
|
|
4912
|
+
registry.remove(sessionId);
|
|
4913
|
+
const session = markSessionEnded(ctx, sessionId);
|
|
4914
|
+
if (session) {
|
|
4915
|
+
releaseForSession(ctx, sessionId);
|
|
4916
|
+
emit(ctx, session.featureId, {
|
|
4917
|
+
type: "session.ended",
|
|
4918
|
+
message: "session ended by user",
|
|
4919
|
+
data: { sessionId, killed }
|
|
4920
|
+
});
|
|
4921
|
+
} else {
|
|
4922
|
+
const existing = getSessionRow(ctx, sessionId);
|
|
4923
|
+
if (!existing)
|
|
4924
|
+
return { sessionId, ok: false, killed };
|
|
4925
|
+
}
|
|
4926
|
+
return { sessionId, ok: true, killed };
|
|
4927
|
+
}
|
|
4928
|
+
var init_end_session = __esm(() => {
|
|
4929
|
+
init_sessions();
|
|
4930
|
+
init_events();
|
|
4931
|
+
init_waypoints();
|
|
4932
|
+
init_registry();
|
|
3828
4933
|
});
|
|
3829
4934
|
|
|
3830
4935
|
// src/workflows/research.ts
|
|
3831
|
-
import { existsSync as
|
|
3832
|
-
import { dirname as
|
|
3833
|
-
import { fileURLToPath as
|
|
3834
|
-
import { run as
|
|
4936
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readdirSync as readdirSync4 } from "fs";
|
|
4937
|
+
import { dirname as dirname5, join as join11 } from "path";
|
|
4938
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
4939
|
+
import { run as run3 } from "@ai-hero/sandcastle";
|
|
3835
4940
|
import { docker as docker2 } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
3836
4941
|
import { noSandbox as noSandbox2 } from "@ai-hero/sandcastle/sandboxes/no-sandbox";
|
|
3837
4942
|
function waypointSlug(waypoint) {
|
|
@@ -3848,7 +4953,7 @@ async function researchRun(ctx, deps) {
|
|
|
3848
4953
|
return { status: "failed", summary: "no waypoint to research" };
|
|
3849
4954
|
}
|
|
3850
4955
|
if (deps.config.sandbox === "docker" && !deps.hasAuthToken) {
|
|
3851
|
-
ctx.emitEvent({ type:
|
|
4956
|
+
ctx.emitEvent({ type: AUTH_MISSING_EVENT3, message: AUTH_MISSING_MESSAGE3 });
|
|
3852
4957
|
return { status: "failed", summary: "research aborted: auth token missing" };
|
|
3853
4958
|
}
|
|
3854
4959
|
ctx.emitEvent({
|
|
@@ -3869,12 +4974,12 @@ async function researchRun(ctx, deps) {
|
|
|
3869
4974
|
}
|
|
3870
4975
|
ctx.emitEvent({
|
|
3871
4976
|
type: "research.failed",
|
|
3872
|
-
message: `waypoint ${waypoint.seq} research failed: ${
|
|
4977
|
+
message: `waypoint ${waypoint.seq} research failed: ${errorHeadline3(outcome.error)}`,
|
|
3873
4978
|
data: { waypointId: waypoint.id, error: outcome.error }
|
|
3874
4979
|
});
|
|
3875
4980
|
return { status: "failed", summary: outcome.error };
|
|
3876
4981
|
}
|
|
3877
|
-
function
|
|
4982
|
+
function errorHeadline3(s) {
|
|
3878
4983
|
const lines = s.split(`
|
|
3879
4984
|
`).map((l) => l.trim()).filter(Boolean);
|
|
3880
4985
|
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
@@ -3885,7 +4990,7 @@ function createResearchStreamThrottle(emit2, opts = {}) {
|
|
|
3885
4990
|
}
|
|
3886
4991
|
function renderResearchPrompt(template, values) {
|
|
3887
4992
|
let out = template;
|
|
3888
|
-
for (const key of
|
|
4993
|
+
for (const key of PLACEHOLDERS2) {
|
|
3889
4994
|
out = out.split(`{{${key}}}`).join(values[key]);
|
|
3890
4995
|
}
|
|
3891
4996
|
return out;
|
|
@@ -3900,32 +5005,32 @@ function interpretResearchResult(result, docRelPath) {
|
|
|
3900
5005
|
return { status: "failed", error: "agent produced no commits \u2014 no research summary landed" };
|
|
3901
5006
|
}
|
|
3902
5007
|
function researchTemplatePath() {
|
|
3903
|
-
const here =
|
|
3904
|
-
return
|
|
5008
|
+
const here = dirname5(fileURLToPath4(import.meta.url));
|
|
5009
|
+
return join11(resolveSkillsRoot(here), "burner", "research-waypoint.md");
|
|
3905
5010
|
}
|
|
3906
5011
|
function readDocsDigestFromDisk2(projectId, slug) {
|
|
3907
5012
|
const worktree = worktreeDir(projectId, slug);
|
|
3908
|
-
if (!
|
|
5013
|
+
if (!existsSync12(worktree))
|
|
3909
5014
|
return "_No talk worktree on disk \u2014 docs digest skipped._";
|
|
3910
|
-
const docsDir =
|
|
3911
|
-
if (!
|
|
5015
|
+
const docsDir = join11(worktree, ...featureDocsRel(slug).split("/"));
|
|
5016
|
+
if (!existsSync12(docsDir))
|
|
3912
5017
|
return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
|
|
3913
|
-
const files = readdirSync4(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content:
|
|
5018
|
+
const files = readdirSync4(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync6(join11(docsDir, e.name), "utf8") }));
|
|
3914
5019
|
return buildDocsDigest(files);
|
|
3915
5020
|
}
|
|
3916
5021
|
async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
3917
5022
|
const { project, feature } = ctx;
|
|
3918
5023
|
const docRelPath = researchDocRel(feature.slug, waypoint);
|
|
3919
5024
|
const tempBranch = researchBranchName(feature.slug, waypoint.seq, newId("b").slice(2, 10));
|
|
3920
|
-
const template =
|
|
5025
|
+
const template = readFileSync6(researchTemplatePath(), "utf8");
|
|
3921
5026
|
const prompt = renderResearchPrompt(template, {
|
|
3922
5027
|
WAYPOINT_JSON: buildWaypointJson(waypoint),
|
|
3923
5028
|
FEATURE_BRIEF: buildFeatureBrief(feature),
|
|
3924
5029
|
DOCS_DIGEST: readDocsDigestFromDisk2(project.id, feature.slug),
|
|
3925
5030
|
RESEARCH_DOC_PATH: docRelPath
|
|
3926
5031
|
});
|
|
3927
|
-
|
|
3928
|
-
const logFilePath =
|
|
5032
|
+
mkdirSync6(logsDir(), { recursive: true });
|
|
5033
|
+
const logFilePath = join11(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
|
|
3929
5034
|
const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
|
|
3930
5035
|
const runOptions = {
|
|
3931
5036
|
agent: buildBurnAgent(config2, token, model),
|
|
@@ -3940,7 +5045,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
3940
5045
|
let result;
|
|
3941
5046
|
let salvaged = [];
|
|
3942
5047
|
try {
|
|
3943
|
-
result = await
|
|
5048
|
+
result = await run3(runOptions);
|
|
3944
5049
|
} catch (err) {
|
|
3945
5050
|
throttle.flush();
|
|
3946
5051
|
if (ctx.signal.aborted)
|
|
@@ -3952,7 +5057,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
3952
5057
|
const removed = await cleanupBurnWorktree(project.repoPath, tempBranch);
|
|
3953
5058
|
ctx.emitEvent({
|
|
3954
5059
|
type: "burn.worktree.teardown-failed",
|
|
3955
|
-
message: `waypoint ${waypoint.seq}: agent finished but sandcastle could not remove its worktree (${
|
|
5060
|
+
message: `waypoint ${waypoint.seq}: agent finished but sandcastle could not remove its worktree (${errorHeadline3(msg)})${removed ? " \u2014 cleaned up" : " \u2014 left on disk"}; landing the ${salvaged.length} commit(s) anyway`,
|
|
3956
5061
|
data: { tempBranch, error: msg, cleanedUp: removed }
|
|
3957
5062
|
});
|
|
3958
5063
|
}
|
|
@@ -3965,7 +5070,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
3965
5070
|
const detail = merge.error ?? "merge failed";
|
|
3966
5071
|
ctx.emitEvent({
|
|
3967
5072
|
type: merge.conflict ? "merge.conflict.needs-human" : "research.merge_failed",
|
|
3968
|
-
message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${
|
|
5073
|
+
message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${errorHeadline3(detail)} \u2014 the branch is preserved for manual recovery`,
|
|
3969
5074
|
data: { tempBranch, featureBranch: feature.branch, conflict: merge.conflict ?? false, error: detail }
|
|
3970
5075
|
});
|
|
3971
5076
|
return {
|
|
@@ -3987,9 +5092,9 @@ function resolveResearchDeps(ctx) {
|
|
|
3987
5092
|
}
|
|
3988
5093
|
function readTokenFromEnvFile2(path) {
|
|
3989
5094
|
let fromFile;
|
|
3990
|
-
if (
|
|
5095
|
+
if (existsSync12(path)) {
|
|
3991
5096
|
try {
|
|
3992
|
-
fromFile = parseEnvFile(
|
|
5097
|
+
fromFile = parseEnvFile(readFileSync6(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
|
|
3993
5098
|
} catch {
|
|
3994
5099
|
fromFile = undefined;
|
|
3995
5100
|
}
|
|
@@ -3997,7 +5102,7 @@ function readTokenFromEnvFile2(path) {
|
|
|
3997
5102
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
3998
5103
|
return token && token.length > 0 ? token : undefined;
|
|
3999
5104
|
}
|
|
4000
|
-
var
|
|
5105
|
+
var AUTH_MISSING_EVENT3 = "auth.missing", AUTH_MISSING_MESSAGE3 = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PLACEHOLDERS2, research;
|
|
4001
5106
|
var init_research = __esm(() => {
|
|
4002
5107
|
init_src();
|
|
4003
5108
|
init_config_load();
|
|
@@ -4005,7 +5110,7 @@ var init_research = __esm(() => {
|
|
|
4005
5110
|
init_skills_root();
|
|
4006
5111
|
init_git();
|
|
4007
5112
|
init_ticket_burner();
|
|
4008
|
-
|
|
5113
|
+
PLACEHOLDERS2 = ["WAYPOINT_JSON", "FEATURE_BRIEF", "DOCS_DIGEST", "RESEARCH_DOC_PATH"];
|
|
4009
5114
|
research = {
|
|
4010
5115
|
id: "research",
|
|
4011
5116
|
run: (ctx) => researchRun(ctx, resolveResearchDeps(ctx))
|
|
@@ -4027,12 +5132,12 @@ var init_registry2 = __esm(() => {
|
|
|
4027
5132
|
});
|
|
4028
5133
|
|
|
4029
5134
|
// src/workflows/runner.ts
|
|
4030
|
-
import { eq as
|
|
5135
|
+
import { eq as eq8 } from "drizzle-orm";
|
|
4031
5136
|
function workflowClaimsFeatureBranch(workflowId) {
|
|
4032
5137
|
return BRANCH_CLAIMING.has(workflowId);
|
|
4033
5138
|
}
|
|
4034
5139
|
function isRunActive(runId) {
|
|
4035
|
-
return
|
|
5140
|
+
return controllers2.has(runId);
|
|
4036
5141
|
}
|
|
4037
5142
|
async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
4038
5143
|
const feature = getFeatureRow(ctx, featureId);
|
|
@@ -4056,7 +5161,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
4056
5161
|
claim(ctx, opts.claimWaypointId, runId);
|
|
4057
5162
|
} catch (e) {
|
|
4058
5163
|
const summary = e instanceof Error ? e.message : "claim failed";
|
|
4059
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(
|
|
5164
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary }).where(eq8(runs.id, runId)).run();
|
|
4060
5165
|
throw e;
|
|
4061
5166
|
}
|
|
4062
5167
|
}
|
|
@@ -4067,7 +5172,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
4067
5172
|
data: { workflow: workflowId }
|
|
4068
5173
|
});
|
|
4069
5174
|
const controller = new AbortController;
|
|
4070
|
-
|
|
5175
|
+
controllers2.set(runId, controller);
|
|
4071
5176
|
const talkWorktree = worktreeDir(project.id, feature.slug);
|
|
4072
5177
|
const talkDetached = workflowClaimsFeatureBranch(workflowId) ? await detachWorktree(talkWorktree) : false;
|
|
4073
5178
|
const wctx = {
|
|
@@ -4093,16 +5198,16 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
4093
5198
|
},
|
|
4094
5199
|
signal: controller.signal
|
|
4095
5200
|
};
|
|
4096
|
-
const done = executeRun(ctx, runId, featureId, def.run(wctx), controller, async () => {
|
|
5201
|
+
const done = executeRun(ctx, runId, featureId, workflowId, def.run(wctx), controller, async () => {
|
|
4097
5202
|
if (talkDetached)
|
|
4098
5203
|
await reattachWorktree(talkWorktree, feature.branch);
|
|
4099
5204
|
});
|
|
4100
5205
|
return { runId, done };
|
|
4101
5206
|
}
|
|
4102
5207
|
function cancelRun(runId) {
|
|
4103
|
-
|
|
5208
|
+
controllers2.get(runId)?.abort();
|
|
4104
5209
|
}
|
|
4105
|
-
async function executeRun(ctx, runId, featureId, runPromise, controller, cleanup) {
|
|
5210
|
+
async function executeRun(ctx, runId, featureId, workflow2, runPromise, controller, cleanup) {
|
|
4106
5211
|
let status = "failed";
|
|
4107
5212
|
let summary = "run failed";
|
|
4108
5213
|
try {
|
|
@@ -4119,10 +5224,13 @@ async function executeRun(ctx, runId, featureId, runPromise, controller, cleanup
|
|
|
4119
5224
|
}
|
|
4120
5225
|
emit(ctx, featureId, { type: "run.error", message: summary, runId });
|
|
4121
5226
|
} finally {
|
|
4122
|
-
|
|
5227
|
+
controllers2.delete(runId);
|
|
4123
5228
|
}
|
|
4124
|
-
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary }).where(
|
|
5229
|
+
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary }).where(eq8(runs.id, runId)).run();
|
|
4125
5230
|
releaseForSession(ctx, runId);
|
|
5231
|
+
if (workflowClaimsFeatureBranch(workflow2)) {
|
|
5232
|
+
sweepOrphanedBurning(ctx, featureId, `orphaned \u2014 the run ended (${status}) while it was burning`);
|
|
5233
|
+
}
|
|
4126
5234
|
emit(ctx, featureId, {
|
|
4127
5235
|
type: "run.finished",
|
|
4128
5236
|
message: `run ${status}: ${summary}`,
|
|
@@ -4148,7 +5256,7 @@ function maybeAutoAdvance(ctx, featureId) {
|
|
|
4148
5256
|
if (next)
|
|
4149
5257
|
setPhase(ctx, featureId, next, "phase.advanced", "auto-advanced to review after successful run");
|
|
4150
5258
|
}
|
|
4151
|
-
var
|
|
5259
|
+
var controllers2, BRANCH_CLAIMING;
|
|
4152
5260
|
var init_runner = __esm(() => {
|
|
4153
5261
|
init_src();
|
|
4154
5262
|
init_paths();
|
|
@@ -4161,12 +5269,12 @@ var init_runner = __esm(() => {
|
|
|
4161
5269
|
init_tickets();
|
|
4162
5270
|
init_waypoints();
|
|
4163
5271
|
init_registry2();
|
|
4164
|
-
|
|
5272
|
+
controllers2 = new Map;
|
|
4165
5273
|
BRANCH_CLAIMING = new Set(["ticket-burner"]);
|
|
4166
5274
|
});
|
|
4167
5275
|
|
|
4168
5276
|
// src/services/features.ts
|
|
4169
|
-
import { desc as
|
|
5277
|
+
import { desc as desc4, eq as eq9 } from "drizzle-orm";
|
|
4170
5278
|
import { rmSync as rmSync2 } from "fs";
|
|
4171
5279
|
async function createFeature(ctx, input) {
|
|
4172
5280
|
const project = requireProjectById(ctx, input.projectId);
|
|
@@ -4229,7 +5337,7 @@ function getFeatureFull(ctx, id) {
|
|
|
4229
5337
|
};
|
|
4230
5338
|
}
|
|
4231
5339
|
function list(ctx, projectId) {
|
|
4232
|
-
const rows = ctx.db.select().from(features).where(
|
|
5340
|
+
const rows = ctx.db.select().from(features).where(eq9(features.projectId, projectId)).orderBy(desc4(features.createdAt)).all();
|
|
4233
5341
|
return rows.map((row) => {
|
|
4234
5342
|
const feature = rowToFeature(row);
|
|
4235
5343
|
const tickets2 = listByFeature2(ctx, feature.id);
|
|
@@ -4264,7 +5372,7 @@ function advance(ctx, featureId) {
|
|
|
4264
5372
|
async function burn(ctx, featureId, opts = {}) {
|
|
4265
5373
|
const feature = getFeatureRow(ctx, featureId);
|
|
4266
5374
|
const running = hasActiveRun(ctx, featureId);
|
|
4267
|
-
|
|
5375
|
+
let tickets2 = listByFeature2(ctx, featureId);
|
|
4268
5376
|
const pending = tickets2.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
4269
5377
|
const restarting = feature.phase === "implementation" && !running;
|
|
4270
5378
|
const iterating = feature.phase === "review" && !running && pending.length >= 1;
|
|
@@ -4282,6 +5390,8 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
4282
5390
|
throw new GateError(tickets2.length > 0 ? "no burnable tickets \u2014 every ticket is cancelled" : "no tickets to burn");
|
|
4283
5391
|
}
|
|
4284
5392
|
if (restarting) {
|
|
5393
|
+
sweepOrphanedBurning(ctx, featureId, "orphaned \u2014 the previous run died while it was burning");
|
|
5394
|
+
tickets2 = listByFeature2(ctx, featureId);
|
|
4285
5395
|
const failed = opts.resetFailed === false ? [] : tickets2.filter((t) => t.status === "failed");
|
|
4286
5396
|
for (const t of failed) {
|
|
4287
5397
|
updateTicket(ctx, t.id, { status: "pending", error: null });
|
|
@@ -4372,7 +5482,7 @@ function escalateToMap(ctx, featureId, input) {
|
|
|
4372
5482
|
return { ok: true, warning: `feature ${feature.slug} is already mapped \u2014 no changes made` };
|
|
4373
5483
|
}
|
|
4374
5484
|
const project = projectForFeature(ctx, feature);
|
|
4375
|
-
ctx.db.update(features).set({ mapped: true }).where(
|
|
5485
|
+
ctx.db.update(features).set({ mapped: true }).where(eq9(features.id, featureId)).run();
|
|
4376
5486
|
scaffoldMapDoc(project, { ...feature, mapped: true }, input);
|
|
4377
5487
|
emit(ctx, featureId, {
|
|
4378
5488
|
type: "feature.escalated",
|
|
@@ -4389,7 +5499,7 @@ function archiveFeature(ctx, featureId) {
|
|
|
4389
5499
|
const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
|
|
4390
5500
|
if (live)
|
|
4391
5501
|
endSession(ctx, live.id);
|
|
4392
|
-
ctx.db.update(features).set({ status: "archived" }).where(
|
|
5502
|
+
ctx.db.update(features).set({ status: "archived" }).where(eq9(features.id, featureId)).run();
|
|
4393
5503
|
emit(ctx, featureId, {
|
|
4394
5504
|
type: "feature.archived",
|
|
4395
5505
|
message: `feature ${feature.slug} archived`,
|
|
@@ -4403,7 +5513,7 @@ function unarchiveFeature(ctx, featureId) {
|
|
|
4403
5513
|
throw new GateError(`feature ${feature.slug} is not archived`);
|
|
4404
5514
|
}
|
|
4405
5515
|
const restored = feature.phase === "shipped" ? "shipped" : "active";
|
|
4406
|
-
ctx.db.update(features).set({ status: restored }).where(
|
|
5516
|
+
ctx.db.update(features).set({ status: restored }).where(eq9(features.id, featureId)).run();
|
|
4407
5517
|
emit(ctx, featureId, {
|
|
4408
5518
|
type: "feature.unarchived",
|
|
4409
5519
|
message: `feature ${feature.slug} unarchived (${restored})`,
|
|
@@ -4417,9 +5527,9 @@ async function deleteFeature(ctx, featureId) {
|
|
|
4417
5527
|
throw new GateError(`feature ${feature.slug} is shipped \u2014 archive it instead (delete is for non-shipped features)`);
|
|
4418
5528
|
}
|
|
4419
5529
|
const project = projectForFeature(ctx, feature);
|
|
4420
|
-
for (const
|
|
4421
|
-
if (
|
|
4422
|
-
cancelRun(
|
|
5530
|
+
for (const run4 of listRunsByFeature(ctx, featureId)) {
|
|
5531
|
+
if (run4.status === "running")
|
|
5532
|
+
cancelRun(run4.id);
|
|
4423
5533
|
}
|
|
4424
5534
|
const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
|
|
4425
5535
|
if (live)
|
|
@@ -4443,13 +5553,13 @@ async function deleteFeature(ctx, featureId) {
|
|
|
4443
5553
|
return { ok: true, slug: feature.slug };
|
|
4444
5554
|
}
|
|
4445
5555
|
function deleteFeatureRows(ctx, featureId) {
|
|
4446
|
-
ctx.db.delete(tickets).where(
|
|
4447
|
-
ctx.db.delete(sessions).where(
|
|
4448
|
-
ctx.db.delete(runs).where(
|
|
4449
|
-
ctx.db.delete(events).where(
|
|
4450
|
-
ctx.db.delete(gateOverrides).where(
|
|
4451
|
-
ctx.db.delete(waypoints).where(
|
|
4452
|
-
ctx.db.delete(features).where(
|
|
5556
|
+
ctx.db.delete(tickets).where(eq9(tickets.featureId, featureId)).run();
|
|
5557
|
+
ctx.db.delete(sessions).where(eq9(sessions.featureId, featureId)).run();
|
|
5558
|
+
ctx.db.delete(runs).where(eq9(runs.featureId, featureId)).run();
|
|
5559
|
+
ctx.db.delete(events).where(eq9(events.featureId, featureId)).run();
|
|
5560
|
+
ctx.db.delete(gateOverrides).where(eq9(gateOverrides.featureId, featureId)).run();
|
|
5561
|
+
ctx.db.delete(waypoints).where(eq9(waypoints.featureId, featureId)).run();
|
|
5562
|
+
ctx.db.delete(features).where(eq9(features.id, featureId)).run();
|
|
4453
5563
|
}
|
|
4454
5564
|
function gateState(ctx, feature) {
|
|
4455
5565
|
const gate = nextGate(feature);
|
|
@@ -4460,7 +5570,7 @@ function gateState(ctx, feature) {
|
|
|
4460
5570
|
}
|
|
4461
5571
|
function uniqueSlug(ctx, projectId, title) {
|
|
4462
5572
|
const base = slugify(title);
|
|
4463
|
-
const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(
|
|
5573
|
+
const existing = new Set(ctx.db.select({ slug: features.slug }).from(features).where(eq9(features.projectId, projectId)).all().map((r) => r.slug));
|
|
4464
5574
|
if (!existing.has(base))
|
|
4465
5575
|
return base;
|
|
4466
5576
|
let n = 2;
|
|
@@ -4492,7 +5602,7 @@ var init_features = __esm(() => {
|
|
|
4492
5602
|
import { StreamableHTTPTransport } from "@hono/mcp";
|
|
4493
5603
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4494
5604
|
import { Hono } from "hono";
|
|
4495
|
-
import * as
|
|
5605
|
+
import * as z4 from "zod";
|
|
4496
5606
|
function headerSessionId(extra) {
|
|
4497
5607
|
const headers = extra.requestInfo?.headers;
|
|
4498
5608
|
if (!headers)
|
|
@@ -4646,7 +5756,7 @@ function buildMcpServer() {
|
|
|
4646
5756
|
server.registerTool("emit_tickets", {
|
|
4647
5757
|
title: "Emit tickets",
|
|
4648
5758
|
description: "Store the ideation session's ticket batch. Each ticket: title, goal, context, acceptanceCriteria[], seams[], blockedBy[] (1-based positions within THIS batch).",
|
|
4649
|
-
inputSchema: { tickets:
|
|
5759
|
+
inputSchema: { tickets: z4.array(TicketInput) }
|
|
4650
5760
|
}, async (args, extra) => {
|
|
4651
5761
|
const rs = await resolveCtxSession(extra);
|
|
4652
5762
|
if (!rs)
|
|
@@ -4659,12 +5769,12 @@ function buildMcpServer() {
|
|
|
4659
5769
|
title: "Update ticket",
|
|
4660
5770
|
description: "Rewrite a stored ticket's content \u2014 title, goal, context, acceptanceCriteria, seams (any subset). Only pending or failed tickets can be edited; done/cancelled tickets are history and burning tickets are already running. Use during a revisit when a decision change makes a ticket stale. Get ids from get_feature_context.",
|
|
4661
5771
|
inputSchema: {
|
|
4662
|
-
id:
|
|
4663
|
-
title:
|
|
4664
|
-
goal:
|
|
4665
|
-
context:
|
|
4666
|
-
acceptanceCriteria:
|
|
4667
|
-
seams:
|
|
5772
|
+
id: z4.string(),
|
|
5773
|
+
title: z4.string().optional(),
|
|
5774
|
+
goal: z4.string().optional(),
|
|
5775
|
+
context: z4.string().optional(),
|
|
5776
|
+
acceptanceCriteria: z4.array(z4.string()).optional(),
|
|
5777
|
+
seams: z4.array(z4.string()).optional()
|
|
4668
5778
|
}
|
|
4669
5779
|
}, async (args, extra) => {
|
|
4670
5780
|
const rs = await resolveCtxSession(extra);
|
|
@@ -4677,7 +5787,7 @@ function buildMcpServer() {
|
|
|
4677
5787
|
server.registerTool("cancel_ticket", {
|
|
4678
5788
|
title: "Cancel ticket",
|
|
4679
5789
|
description: "Cancel a ticket that is no longer needed (terminal state; only pending or failed tickets). The burner skips cancelled tickets, and tickets blocked by a cancelled one still burn \u2014 cancellation counts as satisfied. Pass a short reason so the timeline explains why.",
|
|
4680
|
-
inputSchema: { id:
|
|
5790
|
+
inputSchema: { id: z4.string(), reason: z4.string().optional() }
|
|
4681
5791
|
}, async (args, extra) => {
|
|
4682
5792
|
const rs = await resolveCtxSession(extra);
|
|
4683
5793
|
if (!rs)
|
|
@@ -4689,7 +5799,7 @@ function buildMcpServer() {
|
|
|
4689
5799
|
server.registerTool("escalate_to_map", {
|
|
4690
5800
|
title: "Escalate to map",
|
|
4691
5801
|
description: "Escalate this grilling session into a map when the feature outgrows one context window. Flips the feature to mapped and scaffolds docs/features/<slug>/map.md, seeding Destination + Notes from your arguments (Not-yet-specified and Out-of-scope start empty). Idempotent: calling it on an already-mapped feature warns and changes nothing.",
|
|
4692
|
-
inputSchema: { destination:
|
|
5802
|
+
inputSchema: { destination: z4.string(), notes: z4.string().optional() }
|
|
4693
5803
|
}, async (args, extra) => {
|
|
4694
5804
|
const rs = await resolveCtxSession(extra);
|
|
4695
5805
|
if (!rs)
|
|
@@ -4703,7 +5813,7 @@ function buildMcpServer() {
|
|
|
4703
5813
|
server.registerTool("emit_waypoints", {
|
|
4704
5814
|
title: "Emit waypoints",
|
|
4705
5815
|
description: "Batch-create waypoints on the map (the feature must already be mapped \u2014 call escalate_to_map first). Each waypoint: title, type (grilling|research|prototype|task), question, blockedBy[] (1-based positions within THIS batch, and/or ids of already-stored waypoints). Available from any session once mapped \u2014 any session may branch the map.",
|
|
4706
|
-
inputSchema: { waypoints:
|
|
5816
|
+
inputSchema: { waypoints: z4.array(WaypointInput) }
|
|
4707
5817
|
}, async (args, extra) => {
|
|
4708
5818
|
const rs = await resolveCtxSession(extra);
|
|
4709
5819
|
if (!rs)
|
|
@@ -4713,7 +5823,7 @@ function buildMcpServer() {
|
|
|
4713
5823
|
server.registerTool("resolve_waypoint", {
|
|
4714
5824
|
title: "Resolve waypoint",
|
|
4715
5825
|
description: "End the current waypoint: `resolved` (its question is answered) or `dropped` (no longer needed). Write the decision prose to decisions.md (or the gist to map.md Out-of-scope for a drop) FIRST \u2014 this tool flips machinery only (marks the waypoint terminal, frees any dependents on the frontier). `summary` is the one-line gist shown in the UI. Call this exactly once, as the last thing you do.",
|
|
4716
|
-
inputSchema: { id:
|
|
5826
|
+
inputSchema: { id: z4.string(), disposition: WaypointDisposition, summary: z4.string() }
|
|
4717
5827
|
}, async (args, extra) => {
|
|
4718
5828
|
const rs = await resolveCtxSession(extra);
|
|
4719
5829
|
if (!rs)
|
|
@@ -4725,7 +5835,7 @@ function buildMcpServer() {
|
|
|
4725
5835
|
server.registerTool("record_event", {
|
|
4726
5836
|
title: "Record event",
|
|
4727
5837
|
description: "Add a note to the feature timeline (decisions recorded, spec saved, milestones).",
|
|
4728
|
-
inputSchema: { type:
|
|
5838
|
+
inputSchema: { type: z4.string(), message: z4.string() }
|
|
4729
5839
|
}, async (args, extra) => {
|
|
4730
5840
|
const rs = await resolveCtxSession(extra);
|
|
4731
5841
|
if (!rs)
|
|
@@ -4848,12 +5958,16 @@ import { Hono as Hono2 } from "hono";
|
|
|
4848
5958
|
function handleSessionStart(ctx, sessionId, feature, payload) {
|
|
4849
5959
|
const ccSessionId = typeof payload?.session_id === "string" ? payload.session_id : undefined;
|
|
4850
5960
|
const transcriptPath = typeof payload?.transcript_path === "string" ? payload.transcript_path : undefined;
|
|
5961
|
+
const source = typeof payload?.source === "string" ? payload.source : undefined;
|
|
5962
|
+
const wasLive = getSessionRow(ctx, sessionId)?.status === "live";
|
|
4851
5963
|
markSessionLive(ctx, sessionId, { ccSessionId, transcriptPath });
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
5964
|
+
if (!wasLive) {
|
|
5965
|
+
emit(ctx, feature.id, {
|
|
5966
|
+
type: "session.started",
|
|
5967
|
+
message: source === "resume" ? "session live (conversation resumed)" : "session live",
|
|
5968
|
+
data: { sessionId, ccSessionId, transcriptPath, source: source ?? null }
|
|
5969
|
+
});
|
|
5970
|
+
}
|
|
4857
5971
|
return {
|
|
4858
5972
|
hookSpecificOutput: {
|
|
4859
5973
|
hookEventName: "SessionStart",
|
|
@@ -4861,7 +5975,9 @@ function handleSessionStart(ctx, sessionId, feature, payload) {
|
|
|
4861
5975
|
}
|
|
4862
5976
|
};
|
|
4863
5977
|
}
|
|
4864
|
-
function handleUserPrompt(ctx, feature) {
|
|
5978
|
+
function handleUserPrompt(ctx, sessionId, feature, payload) {
|
|
5979
|
+
const prompt = typeof payload?.prompt === "string" ? payload.prompt : undefined;
|
|
5980
|
+
noteKickoffPrompt(ctx, sessionId, prompt);
|
|
4865
5981
|
const tickets2 = listByFeature2(ctx, feature.id).length;
|
|
4866
5982
|
return {
|
|
4867
5983
|
hookSpecificOutput: {
|
|
@@ -4928,7 +6044,7 @@ var init_hooks = __esm(() => {
|
|
|
4928
6044
|
case "session-start":
|
|
4929
6045
|
return c.json(handleSessionStart(ctx, sessionId, feature, body.payload));
|
|
4930
6046
|
case "user-prompt":
|
|
4931
|
-
return c.json(handleUserPrompt(ctx, feature));
|
|
6047
|
+
return c.json(handleUserPrompt(ctx, sessionId, feature, body.payload));
|
|
4932
6048
|
case "session-end":
|
|
4933
6049
|
return c.json(handleSessionEnd(ctx, sessionId, feature));
|
|
4934
6050
|
default:
|
|
@@ -4993,9 +6109,9 @@ var init_stream = __esm(() => {
|
|
|
4993
6109
|
});
|
|
4994
6110
|
|
|
4995
6111
|
// src/routes/web.ts
|
|
4996
|
-
import { existsSync as
|
|
6112
|
+
import { existsSync as existsSync13, statSync } from "fs";
|
|
4997
6113
|
import { readFile } from "fs/promises";
|
|
4998
|
-
import { extname, join as
|
|
6114
|
+
import { extname, join as join12, normalize, resolve as resolve6, sep as sep2 } from "path";
|
|
4999
6115
|
function contentType(path) {
|
|
5000
6116
|
return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
5001
6117
|
}
|
|
@@ -5013,7 +6129,7 @@ function safeJoin(root, pathname) {
|
|
|
5013
6129
|
} catch {
|
|
5014
6130
|
return null;
|
|
5015
6131
|
}
|
|
5016
|
-
const joined = normalize(
|
|
6132
|
+
const joined = normalize(join12(root, decoded));
|
|
5017
6133
|
if (joined !== root && !joined.startsWith(root + sep2))
|
|
5018
6134
|
return null;
|
|
5019
6135
|
return joined;
|
|
@@ -5026,7 +6142,7 @@ function resolveWebDist() {
|
|
|
5026
6142
|
}
|
|
5027
6143
|
function mountWebApp(app, distDir) {
|
|
5028
6144
|
const root = resolve6(distDir);
|
|
5029
|
-
const indexHtml =
|
|
6145
|
+
const indexHtml = join12(root, "index.html");
|
|
5030
6146
|
app.get("*", async (c, next) => {
|
|
5031
6147
|
const { pathname } = new URL(c.req.url);
|
|
5032
6148
|
if (/^\/(api|mcp|ws|health)(\/|$)/.test(pathname))
|
|
@@ -5046,7 +6162,7 @@ function mountWebApp(app, distDir) {
|
|
|
5046
6162
|
}
|
|
5047
6163
|
function mountWebAppIfBuilt(app) {
|
|
5048
6164
|
const dist = resolveWebDist();
|
|
5049
|
-
if (
|
|
6165
|
+
if (existsSync13(join12(dist, "index.html")))
|
|
5050
6166
|
mountWebApp(app, dist);
|
|
5051
6167
|
}
|
|
5052
6168
|
var MIME;
|
|
@@ -5093,14 +6209,14 @@ var init_context = __esm(() => {
|
|
|
5093
6209
|
});
|
|
5094
6210
|
|
|
5095
6211
|
// src/trpc/routers/docs.ts
|
|
5096
|
-
import * as
|
|
6212
|
+
import * as z5 from "zod";
|
|
5097
6213
|
var docsRouter;
|
|
5098
6214
|
var init_docs = __esm(() => {
|
|
5099
6215
|
init_knowledge();
|
|
5100
6216
|
init_repo();
|
|
5101
6217
|
init_context();
|
|
5102
6218
|
docsRouter = router({
|
|
5103
|
-
read: publicProcedure.input(
|
|
6219
|
+
read: publicProcedure.input(z5.object({ featureId: z5.string(), relPath: z5.string() })).query(({ ctx, input }) => {
|
|
5104
6220
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
5105
6221
|
return readDoc(ctx, feature, input.relPath);
|
|
5106
6222
|
})
|
|
@@ -5108,23 +6224,23 @@ var init_docs = __esm(() => {
|
|
|
5108
6224
|
});
|
|
5109
6225
|
|
|
5110
6226
|
// src/trpc/routers/events.ts
|
|
5111
|
-
import * as
|
|
6227
|
+
import * as z6 from "zod";
|
|
5112
6228
|
var eventsRouter;
|
|
5113
6229
|
var init_events2 = __esm(() => {
|
|
5114
6230
|
init_events();
|
|
5115
6231
|
init_context();
|
|
5116
6232
|
eventsRouter = router({
|
|
5117
|
-
list: publicProcedure.input(
|
|
5118
|
-
listByProject: publicProcedure.input(
|
|
6233
|
+
list: publicProcedure.input(z6.object({ featureId: z6.string(), afterId: z6.number().optional() })).query(({ ctx, input }) => listAfter(ctx, input.featureId, input.afterId)),
|
|
6234
|
+
listByProject: publicProcedure.input(z6.object({ projectId: z6.string(), afterId: z6.number().optional() })).query(({ ctx, input }) => listByProject(ctx, input.projectId, input.afterId))
|
|
5119
6235
|
});
|
|
5120
6236
|
});
|
|
5121
6237
|
|
|
5122
6238
|
// src/launcher/artifacts.ts
|
|
5123
|
-
import { mkdirSync as
|
|
5124
|
-
import { fileURLToPath as
|
|
5125
|
-
import { join as
|
|
6239
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
6240
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
6241
|
+
import { join as join13 } from "path";
|
|
5126
6242
|
function hookClientPath() {
|
|
5127
|
-
return resolveAsset(ASSET_ENV.hookClient,
|
|
6243
|
+
return resolveAsset(ASSET_ENV.hookClient, fileURLToPath5(new URL("./hook-client.ts", import.meta.url)));
|
|
5128
6244
|
}
|
|
5129
6245
|
function serverUrlFor(config2) {
|
|
5130
6246
|
return `http://localhost:${config2.serverPort}`;
|
|
@@ -5325,7 +6441,10 @@ function renderSettings(hookClient) {
|
|
|
5325
6441
|
return {
|
|
5326
6442
|
permissions: { allow: [...RUNCASTLE_MCP_ALLOW_RULES, ...SESSION_BASH_ALLOW_RULES] },
|
|
5327
6443
|
hooks: {
|
|
5328
|
-
SessionStart:
|
|
6444
|
+
SessionStart: SESSION_START_SOURCES.map((source) => ({
|
|
6445
|
+
matcher: source,
|
|
6446
|
+
hooks: [cmd("session-start")]
|
|
6447
|
+
})),
|
|
5329
6448
|
UserPromptSubmit: [{ hooks: [cmd("user-prompt")] }],
|
|
5330
6449
|
SessionEnd: [{ hooks: [cmd("session-end")] }]
|
|
5331
6450
|
}
|
|
@@ -5345,16 +6464,16 @@ function renderMcpConfig(session, config2) {
|
|
|
5345
6464
|
async function writeSessionArtifacts(input) {
|
|
5346
6465
|
const { session, feature, config: config2, waypoint } = input;
|
|
5347
6466
|
const dir = sessionDir(session.id);
|
|
5348
|
-
|
|
5349
|
-
const systemPromptPath =
|
|
5350
|
-
const settingsPath =
|
|
5351
|
-
const mcpConfigPath =
|
|
6467
|
+
mkdirSync7(dir, { recursive: true });
|
|
6468
|
+
const systemPromptPath = join13(dir, "system-prompt.md");
|
|
6469
|
+
const settingsPath = join13(dir, "settings.json");
|
|
6470
|
+
const mcpConfigPath = join13(dir, "mcp.json");
|
|
5352
6471
|
writeFileSync2(systemPromptPath, renderSystemPrompt(feature, session.kind, waypoint), "utf8");
|
|
5353
6472
|
writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath()), null, 2), "utf8");
|
|
5354
6473
|
writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
|
|
5355
6474
|
return { systemPromptPath, settingsPath, mcpConfigPath };
|
|
5356
6475
|
}
|
|
5357
|
-
var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_ALLOW_RULES;
|
|
6476
|
+
var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_ALLOW_RULES, SESSION_START_SOURCES;
|
|
5358
6477
|
var init_artifacts = __esm(() => {
|
|
5359
6478
|
init_paths();
|
|
5360
6479
|
init_asset_paths();
|
|
@@ -5379,13 +6498,14 @@ var init_artifacts = __esm(() => {
|
|
|
5379
6498
|
"Bash(git branch:*)",
|
|
5380
6499
|
"Bash(git show:*)"
|
|
5381
6500
|
];
|
|
6501
|
+
SESSION_START_SOURCES = ["startup", "resume", "clear", "compact", "fork"];
|
|
5382
6502
|
});
|
|
5383
6503
|
|
|
5384
6504
|
// src/launcher/launcher.ts
|
|
5385
|
-
import { existsSync as
|
|
5386
|
-
import { dirname as
|
|
5387
|
-
import { fileURLToPath as
|
|
5388
|
-
import { and as
|
|
6505
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6506
|
+
import { dirname as dirname6, join as join14, resolve as resolve7 } from "path";
|
|
6507
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
6508
|
+
import { and as and7, eq as eq10 } from "drizzle-orm";
|
|
5389
6509
|
function ptyExitMessage(exitCode) {
|
|
5390
6510
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
5391
6511
|
return `terminal exited (code ${label})`;
|
|
@@ -5411,32 +6531,28 @@ function buildClaudeArgs(input) {
|
|
|
5411
6531
|
];
|
|
5412
6532
|
}
|
|
5413
6533
|
function resolveClaudeExecutable() {
|
|
5414
|
-
return
|
|
6534
|
+
return resolveTool("claude");
|
|
5415
6535
|
}
|
|
5416
6536
|
function claudeSpawnTarget(claudeArgs) {
|
|
5417
|
-
|
|
5418
|
-
if (/\.(cmd|bat)$/i.test(exe)) {
|
|
5419
|
-
return { file: process.env.ComSpec ?? "cmd.exe", args: ["/c", exe, ...claudeArgs] };
|
|
5420
|
-
}
|
|
5421
|
-
return { file: exe, args: claudeArgs };
|
|
6537
|
+
return spawnTargetFor(resolveClaudeExecutable(), claudeArgs);
|
|
5422
6538
|
}
|
|
5423
|
-
function resolvePluginDir(fromDir =
|
|
5424
|
-
const rel =
|
|
6539
|
+
function resolvePluginDir(fromDir = dirname6(fileURLToPath6(import.meta.url))) {
|
|
6540
|
+
const rel = join14("packages", "skills", "packs", "runcastle");
|
|
5425
6541
|
const override = process.env[SKILLS_DIR_ENV];
|
|
5426
6542
|
if (override) {
|
|
5427
|
-
const dir2 =
|
|
5428
|
-
if (
|
|
6543
|
+
const dir2 = join14(resolve7(override), "packs", "runcastle");
|
|
6544
|
+
if (existsSync14(dir2))
|
|
5429
6545
|
return dir2;
|
|
5430
6546
|
throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
|
|
5431
6547
|
}
|
|
5432
6548
|
const searched = [];
|
|
5433
6549
|
let dir = fromDir;
|
|
5434
6550
|
for (let i = 0;i < 8; i += 1) {
|
|
5435
|
-
const candidate =
|
|
6551
|
+
const candidate = join14(dir, rel);
|
|
5436
6552
|
searched.push(candidate);
|
|
5437
|
-
if (
|
|
6553
|
+
if (existsSync14(candidate))
|
|
5438
6554
|
return candidate;
|
|
5439
|
-
const parent =
|
|
6555
|
+
const parent = dirname6(dir);
|
|
5440
6556
|
if (parent === dir)
|
|
5441
6557
|
break;
|
|
5442
6558
|
dir = parent;
|
|
@@ -5446,7 +6562,7 @@ function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
|
|
|
5446
6562
|
`)}`);
|
|
5447
6563
|
}
|
|
5448
6564
|
function activeRunFor(ctx, featureId) {
|
|
5449
|
-
const row = ctx.db.select().from(runs).where(
|
|
6565
|
+
const row = ctx.db.select().from(runs).where(and7(eq10(runs.featureId, featureId), eq10(runs.status, "running"))).limit(1).get();
|
|
5450
6566
|
return row ? rowToRun(row) : null;
|
|
5451
6567
|
}
|
|
5452
6568
|
function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
@@ -5697,6 +6813,7 @@ function spawnEmbeddedPty(ctx, feature, session, worktreePath, serverUrl, claude
|
|
|
5697
6813
|
message: "embedded terminal spawned",
|
|
5698
6814
|
data: { sessionId: session.id, mode: "embedded", pid: entry.pty.pid }
|
|
5699
6815
|
});
|
|
6816
|
+
armSessionReadyWatchdog(ctx, session);
|
|
5700
6817
|
} catch (err) {
|
|
5701
6818
|
markSessionEnded(ctx, session.id);
|
|
5702
6819
|
releaseForSession(ctx, session.id);
|
|
@@ -5725,6 +6842,7 @@ var init_launcher = __esm(() => {
|
|
|
5725
6842
|
init_runner();
|
|
5726
6843
|
init_artifacts();
|
|
5727
6844
|
init_sessions();
|
|
6845
|
+
init_sessions();
|
|
5728
6846
|
init_end_session();
|
|
5729
6847
|
CC_NESTING_ENV = [
|
|
5730
6848
|
"CLAUDE_CODE_CHILD_SESSION",
|
|
@@ -5739,7 +6857,7 @@ var init_launcher = __esm(() => {
|
|
|
5739
6857
|
});
|
|
5740
6858
|
|
|
5741
6859
|
// src/trpc/routers/feature.ts
|
|
5742
|
-
import * as
|
|
6860
|
+
import * as z7 from "zod";
|
|
5743
6861
|
var gateId, featureRouter;
|
|
5744
6862
|
var init_feature = __esm(() => {
|
|
5745
6863
|
init_src();
|
|
@@ -5750,37 +6868,38 @@ var init_feature = __esm(() => {
|
|
|
5750
6868
|
init_git();
|
|
5751
6869
|
init_repo();
|
|
5752
6870
|
init_context();
|
|
5753
|
-
gateId =
|
|
6871
|
+
gateId = z7.enum(["G1", "G2", "G3", "G4", "G5"]);
|
|
5754
6872
|
featureRouter = router({
|
|
5755
|
-
create: publicProcedure.input(
|
|
5756
|
-
projectId:
|
|
5757
|
-
title:
|
|
5758
|
-
oneLiner:
|
|
5759
|
-
baseBranch:
|
|
6873
|
+
create: publicProcedure.input(z7.object({
|
|
6874
|
+
projectId: z7.string(),
|
|
6875
|
+
title: z7.string().min(1),
|
|
6876
|
+
oneLiner: z7.string(),
|
|
6877
|
+
baseBranch: z7.string().optional()
|
|
5760
6878
|
})).mutation(({ ctx, input }) => createFeature(ctx, input)),
|
|
5761
|
-
list: publicProcedure.input(
|
|
5762
|
-
get: publicProcedure.input(
|
|
5763
|
-
launchSession: publicProcedure.input(
|
|
5764
|
-
featureId:
|
|
6879
|
+
list: publicProcedure.input(z7.object({ projectId: z7.string() })).query(({ ctx, input }) => list(ctx, input.projectId)),
|
|
6880
|
+
get: publicProcedure.input(z7.object({ id: z7.string() })).query(({ ctx, input }) => getFeatureFull(ctx, input.id)),
|
|
6881
|
+
launchSession: publicProcedure.input(z7.object({
|
|
6882
|
+
featureId: z7.string(),
|
|
5765
6883
|
kind: SessionKind,
|
|
5766
|
-
kickoffLine:
|
|
6884
|
+
kickoffLine: z7.string().min(1).optional()
|
|
5767
6885
|
})).mutation(({ ctx, input }) => launchSession(ctx, input)),
|
|
5768
|
-
workWaypoint: publicProcedure.input(
|
|
5769
|
-
converge: publicProcedure.input(
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
6886
|
+
workWaypoint: publicProcedure.input(z7.object({ featureId: z7.string(), waypointId: z7.string() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
|
|
6887
|
+
converge: publicProcedure.input(z7.object({ featureId: z7.string(), overrideReason: z7.string().min(1).optional() })).mutation(({ ctx, input }) => converge(ctx, { featureId: input.featureId, overrideReason: input.overrideReason })),
|
|
6888
|
+
resendKickoff: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => resendKickoff(ctx, input.sessionId)),
|
|
6889
|
+
endSession: publicProcedure.input(z7.object({ sessionId: z7.string() })).mutation(({ ctx, input }) => endSession(ctx, input.sessionId)),
|
|
6890
|
+
advance: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => advance(ctx, input.featureId)),
|
|
6891
|
+
overrideGate: publicProcedure.input(z7.object({ featureId: z7.string(), gate: gateId, reason: z7.string().min(1) })).mutation(({ ctx, input }) => overrideGate(ctx, input.featureId, input.gate, input.reason)),
|
|
6892
|
+
archive: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => archiveFeature(ctx, input.featureId)),
|
|
6893
|
+
unarchive: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => unarchiveFeature(ctx, input.featureId)),
|
|
6894
|
+
delete: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(({ ctx, input }) => deleteFeature(ctx, input.featureId)),
|
|
6895
|
+
burn: publicProcedure.input(z7.object({ featureId: z7.string(), model: z7.string().min(1).optional() })).mutation(({ ctx, input }) => burn(ctx, input.featureId, { modelOverride: input.model })),
|
|
6896
|
+
testDrive: publicProcedure.input(z7.object({ featureId: z7.string(), action: z7.enum(["start", "stop"]) })).mutation(async ({ ctx, input }) => {
|
|
5778
6897
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
5779
6898
|
const project = projectForFeature(ctx, feature);
|
|
5780
6899
|
return testDrive(ctx, project, feature, input.action);
|
|
5781
6900
|
}),
|
|
5782
6901
|
driveInfo: publicProcedure.query(() => activeDriveInfo()),
|
|
5783
|
-
merge: publicProcedure.input(
|
|
6902
|
+
merge: publicProcedure.input(z7.object({ featureId: z7.string() })).mutation(async ({ ctx, input }) => {
|
|
5784
6903
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
5785
6904
|
const project = projectForFeature(ctx, feature);
|
|
5786
6905
|
if (activeTestDriveFeatureId() === feature.id) {
|
|
@@ -5803,9 +6922,9 @@ var init_feature = __esm(() => {
|
|
|
5803
6922
|
});
|
|
5804
6923
|
|
|
5805
6924
|
// src/services/fsbrowse.ts
|
|
5806
|
-
import { existsSync as
|
|
6925
|
+
import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
|
|
5807
6926
|
import { homedir as homedir2 } from "os";
|
|
5808
|
-
import { basename, dirname as
|
|
6927
|
+
import { basename, dirname as dirname7, isAbsolute, join as join15, resolve as resolve8, sep as sep3 } from "path";
|
|
5809
6928
|
function expandPath(input, home = homedir2()) {
|
|
5810
6929
|
const trimmed = input.trim();
|
|
5811
6930
|
if (trimmed === "")
|
|
@@ -5814,12 +6933,12 @@ function expandPath(input, home = homedir2()) {
|
|
|
5814
6933
|
if (out === "~")
|
|
5815
6934
|
out = home;
|
|
5816
6935
|
else if (out.startsWith("~/") || out.startsWith("~\\"))
|
|
5817
|
-
out =
|
|
6936
|
+
out = join15(home, out.slice(2));
|
|
5818
6937
|
return resolve8(out);
|
|
5819
6938
|
}
|
|
5820
6939
|
function looksLikeRepo(dir) {
|
|
5821
6940
|
try {
|
|
5822
|
-
return
|
|
6941
|
+
return existsSync15(join15(dir, ".git"));
|
|
5823
6942
|
} catch {
|
|
5824
6943
|
return false;
|
|
5825
6944
|
}
|
|
@@ -5830,20 +6949,20 @@ function listRoots(home = homedir2()) {
|
|
|
5830
6949
|
for (let code = 67;code <= 90; code++) {
|
|
5831
6950
|
const drive = `${String.fromCharCode(code)}:${sep3}`;
|
|
5832
6951
|
try {
|
|
5833
|
-
if (
|
|
6952
|
+
if (existsSync15(drive))
|
|
5834
6953
|
roots.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
|
|
5835
6954
|
} catch {}
|
|
5836
6955
|
}
|
|
5837
6956
|
} else {
|
|
5838
6957
|
roots.push({ label: "/", path: "/", kind: "drive" });
|
|
5839
6958
|
}
|
|
5840
|
-
if (
|
|
6959
|
+
if (existsSync15(home))
|
|
5841
6960
|
roots.push({ label: "Home", path: home, kind: "home" });
|
|
5842
6961
|
const seen = new Set(roots.map(pathKey));
|
|
5843
6962
|
for (const name of COMMON_DIRS) {
|
|
5844
|
-
const candidate =
|
|
6963
|
+
const candidate = join15(home, name);
|
|
5845
6964
|
const key = pathKey({ path: candidate });
|
|
5846
|
-
if (seen.has(key) || !
|
|
6965
|
+
if (seen.has(key) || !existsSync15(candidate))
|
|
5847
6966
|
continue;
|
|
5848
6967
|
seen.add(key);
|
|
5849
6968
|
roots.push({ label: name, path: candidate, kind: "common" });
|
|
@@ -5858,7 +6977,7 @@ function crumbsFor(dir) {
|
|
|
5858
6977
|
let cur = dir;
|
|
5859
6978
|
for (;; ) {
|
|
5860
6979
|
out.unshift({ name: basename(cur) || cur, path: cur });
|
|
5861
|
-
const parent =
|
|
6980
|
+
const parent = dirname7(cur);
|
|
5862
6981
|
if (parent === cur)
|
|
5863
6982
|
break;
|
|
5864
6983
|
cur = parent;
|
|
@@ -5866,7 +6985,7 @@ function crumbsFor(dir) {
|
|
|
5866
6985
|
return out;
|
|
5867
6986
|
}
|
|
5868
6987
|
function parentOf(dir) {
|
|
5869
|
-
const parent =
|
|
6988
|
+
const parent = dirname7(dir);
|
|
5870
6989
|
return parent === dir ? null : parent;
|
|
5871
6990
|
}
|
|
5872
6991
|
function browseDir(input, showHidden = false, home = homedir2()) {
|
|
@@ -5874,13 +6993,13 @@ function browseDir(input, showHidden = false, home = homedir2()) {
|
|
|
5874
6993
|
if (!isAbsolute(target)) {
|
|
5875
6994
|
throw new InvalidInputError(`path is not absolute: ${target}`);
|
|
5876
6995
|
}
|
|
5877
|
-
if (!
|
|
6996
|
+
if (!existsSync15(target)) {
|
|
5878
6997
|
throw new InvalidInputError(`path does not exist: ${target}`);
|
|
5879
6998
|
}
|
|
5880
6999
|
let dir = target;
|
|
5881
7000
|
try {
|
|
5882
7001
|
if (!statSync2(target).isDirectory())
|
|
5883
|
-
dir =
|
|
7002
|
+
dir = dirname7(target);
|
|
5884
7003
|
} catch (e) {
|
|
5885
7004
|
throw new InvalidInputError(`cannot read path: ${target} (${errMsg2(e)})`);
|
|
5886
7005
|
}
|
|
@@ -5902,7 +7021,7 @@ function browseDir(input, showHidden = false, home = homedir2()) {
|
|
|
5902
7021
|
continue;
|
|
5903
7022
|
if (!showHidden && name.startsWith("."))
|
|
5904
7023
|
continue;
|
|
5905
|
-
const path =
|
|
7024
|
+
const path = join15(dir, name);
|
|
5906
7025
|
const isSymlink = dirent.isSymbolicLink();
|
|
5907
7026
|
let isDirectory = dirent.isDirectory();
|
|
5908
7027
|
if (isSymlink) {
|
|
@@ -5937,9 +7056,9 @@ var init_fsbrowse = __esm(() => {
|
|
|
5937
7056
|
});
|
|
5938
7057
|
|
|
5939
7058
|
// src/services/projects.ts
|
|
5940
|
-
import { existsSync as
|
|
5941
|
-
import { basename as basename2, join as
|
|
5942
|
-
import { eq as
|
|
7059
|
+
import { existsSync as existsSync16 } from "fs";
|
|
7060
|
+
import { basename as basename2, join as join16 } from "path";
|
|
7061
|
+
import { eq as eq11, isNull } from "drizzle-orm";
|
|
5943
7062
|
function listProjects(ctx) {
|
|
5944
7063
|
return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
|
|
5945
7064
|
}
|
|
@@ -5961,7 +7080,7 @@ async function openProject(ctx, rawPath) {
|
|
|
5961
7080
|
const name = basename2(repoPath) || repoPath;
|
|
5962
7081
|
const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
|
|
5963
7082
|
if (existing) {
|
|
5964
|
-
ctx.db.update(projects).set({ mainBranch, repoPath, closedAt: null }).where(
|
|
7083
|
+
ctx.db.update(projects).set({ mainBranch, repoPath, closedAt: null }).where(eq11(projects.id, existing.id)).run();
|
|
5965
7084
|
const reopened = { ...existing, mainBranch, repoPath };
|
|
5966
7085
|
emitProject(ctx, existing.id, {
|
|
5967
7086
|
type: "project.opened",
|
|
@@ -5985,14 +7104,25 @@ async function openProject(ctx, rawPath) {
|
|
|
5985
7104
|
message: `project ${name} at ${repoPath} (${mainBranch})`
|
|
5986
7105
|
});
|
|
5987
7106
|
warnIfTranslatedMount(ctx, project.id, repoPath);
|
|
7107
|
+
maybeAutoPrepare(ctx, project);
|
|
5988
7108
|
return project;
|
|
5989
7109
|
}
|
|
7110
|
+
function maybeAutoPrepare(ctx, project) {
|
|
7111
|
+
if (!ctx.config.autoPrepare)
|
|
7112
|
+
return;
|
|
7113
|
+
startPrep(ctx, project.id).catch((e) => {
|
|
7114
|
+
emitProject(ctx, project.id, {
|
|
7115
|
+
type: "prep.run.failed",
|
|
7116
|
+
message: `could not start preparation: ${e instanceof Error ? e.message : String(e)}`
|
|
7117
|
+
});
|
|
7118
|
+
});
|
|
7119
|
+
}
|
|
5990
7120
|
function closeProject(ctx, projectId) {
|
|
5991
7121
|
const project = requireProjectById(ctx, projectId);
|
|
5992
7122
|
if (projectHasActiveRun(ctx, projectId)) {
|
|
5993
7123
|
throw new InvalidInputError(`cannot close ${project.name}: a run is in flight \u2014 wait for it to finish or cancel it first`);
|
|
5994
7124
|
}
|
|
5995
|
-
ctx.db.update(projects).set({ closedAt: Date.now() }).where(
|
|
7125
|
+
ctx.db.update(projects).set({ closedAt: Date.now() }).where(eq11(projects.id, projectId)).run();
|
|
5996
7126
|
emitProject(ctx, projectId, {
|
|
5997
7127
|
type: "project.closed",
|
|
5998
7128
|
message: `project ${project.name} closed`
|
|
@@ -6001,7 +7131,7 @@ function closeProject(ctx, projectId) {
|
|
|
6001
7131
|
}
|
|
6002
7132
|
function renameProject(ctx, projectId, name) {
|
|
6003
7133
|
const project = requireProjectById(ctx, projectId);
|
|
6004
|
-
ctx.db.update(projects).set({ name }).where(
|
|
7134
|
+
ctx.db.update(projects).set({ name }).where(eq11(projects.id, projectId)).run();
|
|
6005
7135
|
emitProject(ctx, projectId, {
|
|
6006
7136
|
type: "project.renamed",
|
|
6007
7137
|
message: `project renamed ${project.name} \u2192 ${name}`,
|
|
@@ -6019,7 +7149,7 @@ async function assertRepoTolerant(ctx, repoPath) {
|
|
|
6019
7149
|
} catch (e) {
|
|
6020
7150
|
if (!isNotImplemented(e))
|
|
6021
7151
|
throw e;
|
|
6022
|
-
if (!
|
|
7152
|
+
if (!existsSync16(join16(repoPath, ".git"))) {
|
|
6023
7153
|
throw new InvalidInputError(`not a git repository: ${repoPath}`);
|
|
6024
7154
|
}
|
|
6025
7155
|
}
|
|
@@ -6040,31 +7170,59 @@ var init_projects = __esm(() => {
|
|
|
6040
7170
|
init_events();
|
|
6041
7171
|
init_fsbrowse();
|
|
6042
7172
|
init_git();
|
|
7173
|
+
init_prep();
|
|
6043
7174
|
init_repo();
|
|
6044
7175
|
});
|
|
6045
7176
|
|
|
6046
7177
|
// src/trpc/routers/project.ts
|
|
6047
|
-
import * as
|
|
7178
|
+
import * as z8 from "zod";
|
|
6048
7179
|
var projectRouter;
|
|
6049
7180
|
var init_project = __esm(() => {
|
|
7181
|
+
init_src();
|
|
7182
|
+
init_findings();
|
|
6050
7183
|
init_fsbrowse();
|
|
6051
7184
|
init_git();
|
|
7185
|
+
init_prep();
|
|
6052
7186
|
init_projects();
|
|
6053
7187
|
init_repo();
|
|
6054
7188
|
init_context();
|
|
6055
7189
|
projectRouter = router({
|
|
6056
7190
|
list: publicProcedure.query(({ ctx }) => listProjects(ctx)),
|
|
6057
7191
|
roots: publicProcedure.query(() => listRoots()),
|
|
6058
|
-
browse: publicProcedure.input(
|
|
6059
|
-
branches: publicProcedure.input(
|
|
6060
|
-
open: publicProcedure.input(
|
|
6061
|
-
close: publicProcedure.input(
|
|
6062
|
-
rename: publicProcedure.input(
|
|
7192
|
+
browse: publicProcedure.input(z8.object({ dir: z8.string().optional(), showHidden: z8.boolean().optional() }).optional()).query(({ input }) => browseDir(input?.dir, input?.showHidden ?? false)),
|
|
7193
|
+
branches: publicProcedure.input(z8.object({ projectId: z8.string() })).query(({ ctx, input }) => listBranches(requireProjectById(ctx, input.projectId))),
|
|
7194
|
+
open: publicProcedure.input(z8.object({ repoPath: z8.string().min(1) })).mutation(({ ctx, input }) => openProject(ctx, input.repoPath)),
|
|
7195
|
+
close: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ ctx, input }) => closeProject(ctx, input.projectId)),
|
|
7196
|
+
rename: publicProcedure.input(z8.object({ projectId: z8.string(), name: z8.string().min(1) })).mutation(({ ctx, input }) => renameProject(ctx, input.projectId, input.name)),
|
|
7197
|
+
prepare: publicProcedure.input(z8.object({
|
|
7198
|
+
projectId: z8.string(),
|
|
7199
|
+
refresh: z8.boolean().optional(),
|
|
7200
|
+
keys: z8.array(PreparedKey).optional()
|
|
7201
|
+
})).mutation(async ({ ctx, input }) => {
|
|
7202
|
+
const { prepId, keys } = await startPrep(ctx, input.projectId, {
|
|
7203
|
+
...input.refresh !== undefined ? { refresh: input.refresh } : {},
|
|
7204
|
+
...input.keys ? { keys: input.keys } : {}
|
|
7205
|
+
});
|
|
7206
|
+
return { prepId, keys };
|
|
7207
|
+
}),
|
|
7208
|
+
cancelPrepare: publicProcedure.input(z8.object({ projectId: z8.string() })).mutation(({ input }) => {
|
|
7209
|
+
cancelPrep(input.projectId);
|
|
7210
|
+
return { ok: true };
|
|
7211
|
+
}),
|
|
7212
|
+
prep: publicProcedure.input(z8.object({ projectId: z8.string() })).query(async ({ ctx, input }) => {
|
|
7213
|
+
const project = requireProjectById(ctx, input.projectId);
|
|
7214
|
+
return {
|
|
7215
|
+
latest: latestPrep(ctx, project.id),
|
|
7216
|
+
running: isPreparing(project.id),
|
|
7217
|
+
pendingKeys: keysToPrepare(ctx, project),
|
|
7218
|
+
findings: await listFindings(ctx, project)
|
|
7219
|
+
};
|
|
7220
|
+
})
|
|
6063
7221
|
});
|
|
6064
7222
|
});
|
|
6065
7223
|
|
|
6066
7224
|
// src/trpc/routers/run.ts
|
|
6067
|
-
import * as
|
|
7225
|
+
import * as z9 from "zod";
|
|
6068
7226
|
var runRouter;
|
|
6069
7227
|
var init_run = __esm(() => {
|
|
6070
7228
|
init_agent_stream();
|
|
@@ -6072,9 +7230,9 @@ var init_run = __esm(() => {
|
|
|
6072
7230
|
init_runner();
|
|
6073
7231
|
init_context();
|
|
6074
7232
|
runRouter = router({
|
|
6075
|
-
get: publicProcedure.input(
|
|
6076
|
-
agentTranscript: publicProcedure.input(
|
|
6077
|
-
cancel: publicProcedure.input(
|
|
7233
|
+
get: publicProcedure.input(z9.object({ runId: z9.string() })).query(({ ctx, input }) => getRunRow(ctx, input.runId)),
|
|
7234
|
+
agentTranscript: publicProcedure.input(z9.object({ ticketId: z9.string(), after: z9.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
|
|
7235
|
+
cancel: publicProcedure.input(z9.object({ runId: z9.string() })).mutation(({ input }) => {
|
|
6078
7236
|
cancelRun(input.runId);
|
|
6079
7237
|
return { ok: true };
|
|
6080
7238
|
})
|
|
@@ -6082,15 +7240,15 @@ var init_run = __esm(() => {
|
|
|
6082
7240
|
});
|
|
6083
7241
|
|
|
6084
7242
|
// src/services/settings.ts
|
|
6085
|
-
import { existsSync as
|
|
6086
|
-
import { dirname as
|
|
6087
|
-
import * as
|
|
6088
|
-
import { eq as
|
|
7243
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
7244
|
+
import { dirname as dirname8 } from "path";
|
|
7245
|
+
import * as z10 from "zod";
|
|
7246
|
+
import { eq as eq12 } from "drizzle-orm";
|
|
6089
7247
|
function readRawConfig(configFile) {
|
|
6090
|
-
if (!
|
|
7248
|
+
if (!existsSync17(configFile))
|
|
6091
7249
|
return {};
|
|
6092
7250
|
try {
|
|
6093
|
-
const parsed = JSON.parse(
|
|
7251
|
+
const parsed = JSON.parse(readFileSync7(configFile, "utf8"));
|
|
6094
7252
|
if (typeof parsed !== "object" || parsed === null)
|
|
6095
7253
|
return {};
|
|
6096
7254
|
return foldLegacyModelConfig(parsed);
|
|
@@ -6114,33 +7272,45 @@ function stepModelFields(fileRaw) {
|
|
|
6114
7272
|
});
|
|
6115
7273
|
}
|
|
6116
7274
|
function projectOverrides(ctx, projectId) {
|
|
6117
|
-
const row = ctx.db.select({
|
|
7275
|
+
const row = ctx.db.select({
|
|
7276
|
+
model: projects.model,
|
|
7277
|
+
sandbox: projects.sandbox,
|
|
7278
|
+
devCommand: projects.devCommand,
|
|
7279
|
+
setupCommand: projects.setupCommand,
|
|
7280
|
+
verifyCommands: projects.verifyCommands,
|
|
7281
|
+
knownFailures: projects.knownFailures,
|
|
7282
|
+
dbResetCommand: projects.dbResetCommand
|
|
7283
|
+
}).from(projects).where(eq12(projects.id, projectId)).get();
|
|
6118
7284
|
return {
|
|
6119
7285
|
model: row?.model ?? null,
|
|
6120
7286
|
sandbox: row?.sandbox ?? null,
|
|
6121
|
-
devCommand: row?.devCommand ?? null
|
|
7287
|
+
devCommand: row?.devCommand ?? null,
|
|
7288
|
+
setupCommand: row?.setupCommand ?? null,
|
|
7289
|
+
verifyCommands: row?.verifyCommands ?? null,
|
|
7290
|
+
knownFailures: row?.knownFailures ?? null,
|
|
7291
|
+
dbResetCommand: row?.dbResetCommand ?? null
|
|
6122
7292
|
};
|
|
6123
7293
|
}
|
|
6124
|
-
function resolveField(
|
|
7294
|
+
function resolveField(desc5, layers) {
|
|
6125
7295
|
const scoped = layers.overrides !== null;
|
|
6126
|
-
const scope = scoped &&
|
|
6127
|
-
const base = { key:
|
|
6128
|
-
if (
|
|
6129
|
-
const raw = layers.env[
|
|
7296
|
+
const scope = scoped && desc5.projectColumn ? "project" : "global";
|
|
7297
|
+
const base = { key: desc5.key, restartRequired: desc5.restartRequired, scope };
|
|
7298
|
+
if (desc5.envVar) {
|
|
7299
|
+
const raw = layers.env[desc5.envVar];
|
|
6130
7300
|
if (raw !== undefined && raw !== "") {
|
|
6131
|
-
return { ...base, value:
|
|
7301
|
+
return { ...base, value: desc5.parseEnv(raw), source: "env", editable: false };
|
|
6132
7302
|
}
|
|
6133
7303
|
}
|
|
6134
|
-
if (layers.overrides &&
|
|
6135
|
-
const ov = layers.overrides[
|
|
7304
|
+
if (layers.overrides && desc5.projectColumn) {
|
|
7305
|
+
const ov = layers.overrides[desc5.projectColumn];
|
|
6136
7306
|
if (ov !== null && ov !== "") {
|
|
6137
7307
|
return { ...base, value: ov, source: "project", editable: true };
|
|
6138
7308
|
}
|
|
6139
7309
|
}
|
|
6140
|
-
if (
|
|
6141
|
-
return { ...base, value: layers.fileRaw[
|
|
7310
|
+
if (desc5.configKey && Object.prototype.hasOwnProperty.call(layers.fileRaw, desc5.configKey)) {
|
|
7311
|
+
return { ...base, value: layers.fileRaw[desc5.configKey], source: "file", editable: true };
|
|
6142
7312
|
}
|
|
6143
|
-
const value =
|
|
7313
|
+
const value = desc5.configKey ? DEFAULTS[desc5.configKey] ?? null : null;
|
|
6144
7314
|
return { ...base, value, source: "default", editable: true };
|
|
6145
7315
|
}
|
|
6146
7316
|
function getSettings(ctx, projectId, io = {}) {
|
|
@@ -6157,59 +7327,61 @@ function updateSettings(ctx, input, io = {}) {
|
|
|
6157
7327
|
if (input.key.startsWith("stepModels.")) {
|
|
6158
7328
|
return updateStepModel(ctx, input, configFile, io);
|
|
6159
7329
|
}
|
|
6160
|
-
const
|
|
6161
|
-
if (!
|
|
7330
|
+
const desc5 = DESCRIPTORS.find((d) => d.key === input.key);
|
|
7331
|
+
if (!desc5)
|
|
6162
7332
|
throw new InvalidInputError(`unknown setting: ${input.key}`);
|
|
6163
|
-
if (
|
|
6164
|
-
const raw = env[
|
|
7333
|
+
if (desc5.envVar) {
|
|
7334
|
+
const raw = env[desc5.envVar];
|
|
6165
7335
|
if (raw !== undefined && raw !== "") {
|
|
6166
|
-
throw new InvalidInputError(`${
|
|
7336
|
+
throw new InvalidInputError(`${desc5.key} is set by environment variable ${desc5.envVar}; unset it to edit`);
|
|
6167
7337
|
}
|
|
6168
7338
|
}
|
|
6169
|
-
const toProject = input.projectId !== undefined &&
|
|
7339
|
+
const toProject = input.projectId !== undefined && desc5.projectColumn !== undefined;
|
|
6170
7340
|
if (input.value === null) {
|
|
6171
7341
|
if (!toProject)
|
|
6172
|
-
throw new InvalidInputError(`${
|
|
7342
|
+
throw new InvalidInputError(`${desc5.key} cannot be cleared`);
|
|
6173
7343
|
const project = requireProjectById(ctx, input.projectId);
|
|
6174
|
-
ctx.db.update(projects).set({ [
|
|
7344
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(eq12(projects.id, project.id)).run();
|
|
7345
|
+
recordHuman(ctx, project.id, desc5.key, null);
|
|
6175
7346
|
emitProject(ctx, project.id, {
|
|
6176
7347
|
type: "settings.updated",
|
|
6177
|
-
message: `${
|
|
6178
|
-
data: { key:
|
|
7348
|
+
message: `${desc5.key} override cleared`,
|
|
7349
|
+
data: { key: desc5.key, scope: "project", value: null }
|
|
6179
7350
|
});
|
|
6180
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
7351
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
6181
7352
|
}
|
|
6182
|
-
const parsed =
|
|
7353
|
+
const parsed = desc5.valueSchema.safeParse(input.value);
|
|
6183
7354
|
if (!parsed.success) {
|
|
6184
|
-
throw new InvalidInputError(`invalid value for ${
|
|
7355
|
+
throw new InvalidInputError(`invalid value for ${desc5.key}: ${parsed.error.issues[0]?.message}`);
|
|
6185
7356
|
}
|
|
6186
7357
|
const value = parsed.data;
|
|
6187
7358
|
if (toProject) {
|
|
6188
7359
|
const project = requireProjectById(ctx, input.projectId);
|
|
6189
|
-
ctx.db.update(projects).set({ [
|
|
7360
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(eq12(projects.id, project.id)).run();
|
|
7361
|
+
recordHuman(ctx, project.id, desc5.key, String(value));
|
|
6190
7362
|
emitProject(ctx, project.id, {
|
|
6191
7363
|
type: "settings.updated",
|
|
6192
|
-
message: `${
|
|
6193
|
-
data: { key:
|
|
7364
|
+
message: `${desc5.key} override set to ${String(value)}`,
|
|
7365
|
+
data: { key: desc5.key, scope: "project", value }
|
|
6194
7366
|
});
|
|
6195
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
7367
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
6196
7368
|
}
|
|
6197
|
-
if (!
|
|
6198
|
-
throw new InvalidInputError(`${
|
|
7369
|
+
if (!desc5.configKey) {
|
|
7370
|
+
throw new InvalidInputError(`${desc5.key} is a per-project setting; provide a projectId`);
|
|
6199
7371
|
}
|
|
6200
|
-
writeGlobal(configFile,
|
|
6201
|
-
ctx.config[
|
|
7372
|
+
writeGlobal(configFile, desc5.configKey, value);
|
|
7373
|
+
ctx.config[desc5.configKey] = value;
|
|
6202
7374
|
emitProject(ctx, GLOBAL_EVENT_KEY, {
|
|
6203
7375
|
type: "settings.updated",
|
|
6204
|
-
message: `${
|
|
6205
|
-
data: { key:
|
|
7376
|
+
message: `${desc5.key} set to ${String(value)}`,
|
|
7377
|
+
data: { key: desc5.key, scope: "global", value }
|
|
6206
7378
|
});
|
|
6207
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
7379
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
6208
7380
|
}
|
|
6209
7381
|
function writeGlobal(configFile, configKey, value) {
|
|
6210
7382
|
const raw = readRawConfig(configFile);
|
|
6211
7383
|
raw[configKey] = value;
|
|
6212
|
-
|
|
7384
|
+
mkdirSync8(dirname8(configFile), { recursive: true });
|
|
6213
7385
|
writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
|
|
6214
7386
|
`);
|
|
6215
7387
|
}
|
|
@@ -6220,7 +7392,7 @@ function updateStepModel(ctx, input, configFile, io) {
|
|
|
6220
7392
|
const modelStep = step;
|
|
6221
7393
|
let value = null;
|
|
6222
7394
|
if (input.value !== null) {
|
|
6223
|
-
const parsed =
|
|
7395
|
+
const parsed = z10.string().min(1).safeParse(input.value);
|
|
6224
7396
|
if (!parsed.success) {
|
|
6225
7397
|
throw new InvalidInputError(`invalid value for ${input.key}: ${parsed.error.issues[0]?.message}`);
|
|
6226
7398
|
}
|
|
@@ -6248,7 +7420,7 @@ function writeStepModel(configFile, step, value) {
|
|
|
6248
7420
|
else
|
|
6249
7421
|
stepModels[step] = value;
|
|
6250
7422
|
raw.stepModels = stepModels;
|
|
6251
|
-
|
|
7423
|
+
mkdirSync8(dirname8(configFile), { recursive: true });
|
|
6252
7424
|
writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
|
|
6253
7425
|
`);
|
|
6254
7426
|
}
|
|
@@ -6265,6 +7437,7 @@ var init_settings = __esm(() => {
|
|
|
6265
7437
|
init_schema();
|
|
6266
7438
|
init_errors();
|
|
6267
7439
|
init_events();
|
|
7440
|
+
init_findings();
|
|
6268
7441
|
init_repo();
|
|
6269
7442
|
DESCRIPTORS = [
|
|
6270
7443
|
{
|
|
@@ -6272,7 +7445,7 @@ var init_settings = __esm(() => {
|
|
|
6272
7445
|
configKey: "serverPort",
|
|
6273
7446
|
envVar: "RUNCASTLE_SERVER_PORT",
|
|
6274
7447
|
restartRequired: true,
|
|
6275
|
-
valueSchema:
|
|
7448
|
+
valueSchema: z10.number().int().positive(),
|
|
6276
7449
|
parseEnv: (raw) => Number(raw)
|
|
6277
7450
|
},
|
|
6278
7451
|
{
|
|
@@ -6281,7 +7454,7 @@ var init_settings = __esm(() => {
|
|
|
6281
7454
|
envVar: "RUNCASTLE_MODEL",
|
|
6282
7455
|
projectColumn: "model",
|
|
6283
7456
|
restartRequired: false,
|
|
6284
|
-
valueSchema:
|
|
7457
|
+
valueSchema: z10.string().min(1),
|
|
6285
7458
|
parseEnv: idEnv
|
|
6286
7459
|
},
|
|
6287
7460
|
{
|
|
@@ -6290,7 +7463,7 @@ var init_settings = __esm(() => {
|
|
|
6290
7463
|
envVar: "RUNCASTLE_SANDBOX",
|
|
6291
7464
|
projectColumn: "sandbox",
|
|
6292
7465
|
restartRequired: false,
|
|
6293
|
-
valueSchema:
|
|
7466
|
+
valueSchema: z10.enum(["docker", "podman", "noSandbox"]),
|
|
6294
7467
|
parseEnv: idEnv
|
|
6295
7468
|
},
|
|
6296
7469
|
{
|
|
@@ -6298,7 +7471,7 @@ var init_settings = __esm(() => {
|
|
|
6298
7471
|
configKey: "sandboxImage",
|
|
6299
7472
|
envVar: "RUNCASTLE_SANDBOX_IMAGE",
|
|
6300
7473
|
restartRequired: false,
|
|
6301
|
-
valueSchema:
|
|
7474
|
+
valueSchema: z10.string().min(1),
|
|
6302
7475
|
parseEnv: idEnv
|
|
6303
7476
|
},
|
|
6304
7477
|
{
|
|
@@ -6306,7 +7479,7 @@ var init_settings = __esm(() => {
|
|
|
6306
7479
|
configKey: "burnConcurrency",
|
|
6307
7480
|
envVar: "RUNCASTLE_BURN_CONCURRENCY",
|
|
6308
7481
|
restartRequired: false,
|
|
6309
|
-
valueSchema:
|
|
7482
|
+
valueSchema: z10.number().int().min(1).max(8),
|
|
6310
7483
|
parseEnv: (raw) => Number(raw)
|
|
6311
7484
|
},
|
|
6312
7485
|
{
|
|
@@ -6314,7 +7487,7 @@ var init_settings = __esm(() => {
|
|
|
6314
7487
|
configKey: "burnMaxIterations",
|
|
6315
7488
|
envVar: "RUNCASTLE_BURN_MAX_ITERATIONS",
|
|
6316
7489
|
restartRequired: false,
|
|
6317
|
-
valueSchema:
|
|
7490
|
+
valueSchema: z10.number().int().min(1).max(10),
|
|
6318
7491
|
parseEnv: (raw) => Number(raw)
|
|
6319
7492
|
},
|
|
6320
7493
|
{
|
|
@@ -6322,7 +7495,7 @@ var init_settings = __esm(() => {
|
|
|
6322
7495
|
configKey: "burnAttempts",
|
|
6323
7496
|
envVar: "RUNCASTLE_BURN_ATTEMPTS",
|
|
6324
7497
|
restartRequired: false,
|
|
6325
|
-
valueSchema:
|
|
7498
|
+
valueSchema: z10.number().int().min(1).max(5),
|
|
6326
7499
|
parseEnv: (raw) => Number(raw)
|
|
6327
7500
|
},
|
|
6328
7501
|
{
|
|
@@ -6330,15 +7503,50 @@ var init_settings = __esm(() => {
|
|
|
6330
7503
|
configKey: "burnConflictAttempts",
|
|
6331
7504
|
envVar: "RUNCASTLE_BURN_CONFLICT_ATTEMPTS",
|
|
6332
7505
|
restartRequired: false,
|
|
6333
|
-
valueSchema:
|
|
7506
|
+
valueSchema: z10.number().int().min(0).max(3),
|
|
7507
|
+
parseEnv: (raw) => Number(raw)
|
|
7508
|
+
},
|
|
7509
|
+
{
|
|
7510
|
+
key: "burnCpus",
|
|
7511
|
+
configKey: "burnCpus",
|
|
7512
|
+
envVar: "RUNCASTLE_BURN_CPUS",
|
|
7513
|
+
restartRequired: false,
|
|
7514
|
+
valueSchema: z10.number().positive().max(256),
|
|
6334
7515
|
parseEnv: (raw) => Number(raw)
|
|
6335
7516
|
},
|
|
7517
|
+
{
|
|
7518
|
+
key: "autoPrepare",
|
|
7519
|
+
configKey: "autoPrepare",
|
|
7520
|
+
envVar: "RUNCASTLE_AUTO_PREPARE",
|
|
7521
|
+
restartRequired: false,
|
|
7522
|
+
valueSchema: z10.boolean(),
|
|
7523
|
+
parseEnv: (raw) => raw !== "0" && raw.toLowerCase() !== "false"
|
|
7524
|
+
},
|
|
6336
7525
|
{
|
|
6337
7526
|
key: "setupCommand",
|
|
6338
7527
|
configKey: "setupCommand",
|
|
6339
7528
|
envVar: "RUNCASTLE_SETUP_COMMAND",
|
|
7529
|
+
projectColumn: "setupCommand",
|
|
7530
|
+
restartRequired: false,
|
|
7531
|
+
valueSchema: z10.string().min(1),
|
|
7532
|
+
parseEnv: idEnv
|
|
7533
|
+
},
|
|
7534
|
+
{
|
|
7535
|
+
key: "verifyCommands",
|
|
7536
|
+
configKey: "verifyCommands",
|
|
7537
|
+
envVar: "RUNCASTLE_VERIFY_COMMANDS",
|
|
7538
|
+
projectColumn: "verifyCommands",
|
|
7539
|
+
restartRequired: false,
|
|
7540
|
+
valueSchema: z10.string().min(1),
|
|
7541
|
+
parseEnv: idEnv
|
|
7542
|
+
},
|
|
7543
|
+
{
|
|
7544
|
+
key: "knownFailures",
|
|
7545
|
+
configKey: "knownFailures",
|
|
7546
|
+
envVar: "RUNCASTLE_KNOWN_FAILURES",
|
|
7547
|
+
projectColumn: "knownFailures",
|
|
6340
7548
|
restartRequired: false,
|
|
6341
|
-
valueSchema:
|
|
7549
|
+
valueSchema: z10.string().min(1),
|
|
6342
7550
|
parseEnv: idEnv
|
|
6343
7551
|
},
|
|
6344
7552
|
{
|
|
@@ -6346,14 +7554,21 @@ var init_settings = __esm(() => {
|
|
|
6346
7554
|
configKey: "mainBranch",
|
|
6347
7555
|
envVar: "RUNCASTLE_MAIN_BRANCH",
|
|
6348
7556
|
restartRequired: false,
|
|
6349
|
-
valueSchema:
|
|
7557
|
+
valueSchema: z10.string().min(1),
|
|
6350
7558
|
parseEnv: idEnv
|
|
6351
7559
|
},
|
|
6352
7560
|
{
|
|
6353
7561
|
key: "devCommand",
|
|
6354
7562
|
projectColumn: "devCommand",
|
|
6355
7563
|
restartRequired: false,
|
|
6356
|
-
valueSchema:
|
|
7564
|
+
valueSchema: z10.string().min(1),
|
|
7565
|
+
parseEnv: idEnv
|
|
7566
|
+
},
|
|
7567
|
+
{
|
|
7568
|
+
key: "dbResetCommand",
|
|
7569
|
+
projectColumn: "dbResetCommand",
|
|
7570
|
+
restartRequired: false,
|
|
7571
|
+
valueSchema: z10.string().min(1),
|
|
6357
7572
|
parseEnv: idEnv
|
|
6358
7573
|
}
|
|
6359
7574
|
];
|
|
@@ -6362,14 +7577,14 @@ var init_settings = __esm(() => {
|
|
|
6362
7577
|
});
|
|
6363
7578
|
|
|
6364
7579
|
// src/trpc/routers/settings.ts
|
|
6365
|
-
import * as
|
|
7580
|
+
import * as z11 from "zod";
|
|
6366
7581
|
var settingsRouter;
|
|
6367
7582
|
var init_settings2 = __esm(() => {
|
|
6368
7583
|
init_src();
|
|
6369
7584
|
init_settings();
|
|
6370
7585
|
init_context();
|
|
6371
7586
|
settingsRouter = router({
|
|
6372
|
-
get: publicProcedure.input(
|
|
7587
|
+
get: publicProcedure.input(z11.object({ projectId: z11.string().optional() }).optional()).query(({ ctx, input }) => getSettings(ctx, input?.projectId)),
|
|
6373
7588
|
update: publicProcedure.input(SettingsUpdateInput).mutation(({ ctx, input }) => updateSettings(ctx, input))
|
|
6374
7589
|
});
|
|
6375
7590
|
});
|
|
@@ -6539,10 +7754,7 @@ var init_doctor = __esm(() => {
|
|
|
6539
7754
|
import { spawn as spawn2 } from "child_process";
|
|
6540
7755
|
function createSystemExec(opts = {}) {
|
|
6541
7756
|
return (command, args) => new Promise((resolve9) => {
|
|
6542
|
-
const
|
|
6543
|
-
const isShim = /\.(cmd|bat)$/i.test(resolved);
|
|
6544
|
-
const file = isShim ? process.env.ComSpec ?? "cmd.exe" : resolved;
|
|
6545
|
-
const spawnArgs = isShim ? ["/c", resolved, ...args] : args;
|
|
7757
|
+
const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
|
|
6546
7758
|
let stdout = "";
|
|
6547
7759
|
let stderr = "";
|
|
6548
7760
|
const child = spawn2(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
|
|
@@ -6565,9 +7777,9 @@ var init_system_exec = __esm(() => {
|
|
|
6565
7777
|
});
|
|
6566
7778
|
|
|
6567
7779
|
// src/services/setup.ts
|
|
6568
|
-
import { copyFileSync, existsSync as
|
|
6569
|
-
import { dirname as
|
|
6570
|
-
import { fileURLToPath as
|
|
7780
|
+
import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
7781
|
+
import { dirname as dirname9, join as join17 } from "path";
|
|
7782
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
6571
7783
|
async function writeGitIdentity(exec, identity) {
|
|
6572
7784
|
const name = identity.name.trim();
|
|
6573
7785
|
const email = identity.email.trim();
|
|
@@ -6642,16 +7854,16 @@ async function resolveRuntime(exec, preferred) {
|
|
|
6642
7854
|
function resolveSandcastleBin() {
|
|
6643
7855
|
try {
|
|
6644
7856
|
const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
|
|
6645
|
-
let dir =
|
|
7857
|
+
let dir = dirname9(fileURLToPath7(mainUrl));
|
|
6646
7858
|
for (let hops = 0;hops < 6; hops++) {
|
|
6647
|
-
const manifestPath =
|
|
6648
|
-
if (
|
|
6649
|
-
const manifest = JSON.parse(
|
|
7859
|
+
const manifestPath = join17(dir, "package.json");
|
|
7860
|
+
if (existsSync18(manifestPath)) {
|
|
7861
|
+
const manifest = JSON.parse(readFileSync8(manifestPath, "utf8"));
|
|
6650
7862
|
if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
|
|
6651
|
-
return
|
|
7863
|
+
return join17(dir, manifest.bin.sandcastle);
|
|
6652
7864
|
}
|
|
6653
7865
|
}
|
|
6654
|
-
const parent =
|
|
7866
|
+
const parent = dirname9(dir);
|
|
6655
7867
|
if (parent === dir)
|
|
6656
7868
|
break;
|
|
6657
7869
|
dir = parent;
|
|
@@ -6673,32 +7885,49 @@ function terminalSpec(kind, opts) {
|
|
|
6673
7885
|
};
|
|
6674
7886
|
}
|
|
6675
7887
|
function sandcastleTemplateDir() {
|
|
6676
|
-
return resolveAsset(ASSET_ENV.sandcastleTemplate,
|
|
7888
|
+
return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath7(new URL("../assets/sandcastle", import.meta.url)));
|
|
6677
7889
|
}
|
|
6678
7890
|
function scaffoldSandcastleConfig(templateDir, targetDir) {
|
|
6679
|
-
const dir =
|
|
6680
|
-
if (
|
|
7891
|
+
const dir = join17(targetDir, ".sandcastle");
|
|
7892
|
+
if (existsSync18(dir))
|
|
6681
7893
|
return { scaffolded: false, dir };
|
|
6682
|
-
|
|
7894
|
+
mkdirSync9(dir, { recursive: true });
|
|
6683
7895
|
for (const name of readdirSync6(templateDir)) {
|
|
6684
|
-
copyFileSync(
|
|
7896
|
+
copyFileSync(join17(templateDir, name), join17(dir, name));
|
|
6685
7897
|
}
|
|
6686
7898
|
return { scaffolded: true, dir };
|
|
6687
7899
|
}
|
|
6688
7900
|
function prepareSandboxBuildContext() {
|
|
6689
7901
|
const target = sandboxBuildDir();
|
|
6690
|
-
|
|
7902
|
+
mkdirSync9(target, { recursive: true });
|
|
6691
7903
|
scaffoldSandcastleConfig(sandcastleTemplateDir(), target);
|
|
6692
7904
|
return target;
|
|
6693
7905
|
}
|
|
6694
7906
|
function createTokenVerifier(exec) {
|
|
6695
7907
|
return async (token) => {
|
|
7908
|
+
if (token.length < 8) {
|
|
7909
|
+
return {
|
|
7910
|
+
valid: false,
|
|
7911
|
+
detail: "token looks malformed (too short) \u2014 saved to ~/.runcastle/.env anyway",
|
|
7912
|
+
fix: "Re-run `claude setup-token` and paste the whole line it prints (it starts with `sk-ant-oat`)."
|
|
7913
|
+
};
|
|
7914
|
+
}
|
|
6696
7915
|
const out = await exec("claude", ["--version"]);
|
|
6697
|
-
if (!
|
|
6698
|
-
return {
|
|
7916
|
+
if (!out.ok) {
|
|
7917
|
+
return {
|
|
7918
|
+
valid: false,
|
|
7919
|
+
detail: "Token saved to ~/.runcastle/.env, but runcastle could not launch `claude` to verify it. " + "That is a PATH problem in this server process, not a missing install \u2014 a terminal that finds " + "`claude` proves nothing about the PATH runcastle was started with.",
|
|
7920
|
+
fix: "Quit runcastle and start it again from a terminal where `claude --version` works. " + "If it still fails, pin the path: set RUNCASTLE_CLAUDE_BIN to the full path from " + "`where.exe claude` (Windows) or `which claude` (macOS/Linux), then restart runcastle."
|
|
7921
|
+
};
|
|
6699
7922
|
}
|
|
6700
|
-
if (
|
|
6701
|
-
|
|
7923
|
+
if (out.code !== 0) {
|
|
7924
|
+
const why = (out.stderr.trim() || out.stdout.trim()).split(`
|
|
7925
|
+
`)[0] ?? "";
|
|
7926
|
+
return {
|
|
7927
|
+
valid: false,
|
|
7928
|
+
detail: `Token saved to ~/.runcastle/.env, but \`claude --version\` exited ${out.code}` + `${why ? `: ${why}` : " with no output"}.`,
|
|
7929
|
+
fix: "Run `claude --version` yourself and fix what it reports \u2014 the Claude Code install is broken, not the token."
|
|
7930
|
+
};
|
|
6702
7931
|
}
|
|
6703
7932
|
return { valid: true, detail: "token captured to ~/.runcastle/.env" };
|
|
6704
7933
|
};
|
|
@@ -6706,9 +7935,9 @@ function createTokenVerifier(exec) {
|
|
|
6706
7935
|
function fileAfkTokenIo(exec) {
|
|
6707
7936
|
const path = envPath();
|
|
6708
7937
|
return {
|
|
6709
|
-
read: () =>
|
|
7938
|
+
read: () => existsSync18(path) ? readFileSync8(path, "utf8") : "",
|
|
6710
7939
|
write: (content) => {
|
|
6711
|
-
|
|
7940
|
+
mkdirSync9(dirname9(path), { recursive: true });
|
|
6712
7941
|
writeFileSync4(path, content, "utf8");
|
|
6713
7942
|
},
|
|
6714
7943
|
verify: createTokenVerifier(exec)
|
|
@@ -6723,7 +7952,7 @@ var init_setup = __esm(() => {
|
|
|
6723
7952
|
});
|
|
6724
7953
|
|
|
6725
7954
|
// src/trpc/routers/setup.ts
|
|
6726
|
-
import * as
|
|
7955
|
+
import * as z12 from "zod";
|
|
6727
7956
|
var setupRouter;
|
|
6728
7957
|
var init_setup2 = __esm(() => {
|
|
6729
7958
|
init_src();
|
|
@@ -6739,9 +7968,9 @@ var init_setup2 = __esm(() => {
|
|
|
6739
7968
|
...ctx.config.sandboxImage ? { imageName: ctx.config.sandboxImage } : {}
|
|
6740
7969
|
})),
|
|
6741
7970
|
runtimeGuide: publicProcedure.query(() => runtimeInstallGuide(process.platform)),
|
|
6742
|
-
gitIdentity: publicProcedure.input(
|
|
6743
|
-
afkToken: publicProcedure.input(
|
|
6744
|
-
startTerminal: publicProcedure.input(
|
|
7971
|
+
gitIdentity: publicProcedure.input(z12.object({ name: z12.string(), email: z12.string() })).mutation(({ input }) => writeGitIdentity(createSystemExec(), input)),
|
|
7972
|
+
afkToken: publicProcedure.input(z12.object({ token: z12.string() })).mutation(({ input }) => saveAfkToken(fileAfkTokenIo(createSystemExec()), input.token)),
|
|
7973
|
+
startTerminal: publicProcedure.input(z12.object({ kind: z12.enum(["setup-token", "build-image"]) })).mutation(async ({ ctx, input }) => {
|
|
6745
7974
|
const exec = createSystemExec();
|
|
6746
7975
|
const preferred = ctx.config.sandbox === "podman" ? "podman" : "docker";
|
|
6747
7976
|
const runtime = await resolveRuntime(exec, preferred);
|
|
@@ -6750,10 +7979,7 @@ var init_setup2 = __esm(() => {
|
|
|
6750
7979
|
const spec = terminalSpec(input.kind, { runtime, imageName, sandcastleBin });
|
|
6751
7980
|
const sessionId = newId("setup");
|
|
6752
7981
|
const cwd = input.kind === "build-image" ? prepareSandboxBuildContext() : process.cwd();
|
|
6753
|
-
const
|
|
6754
|
-
const isShim = /\.(cmd|bat)$/i.test(resolved);
|
|
6755
|
-
const file = isShim ? process.env.ComSpec ?? "cmd.exe" : resolved;
|
|
6756
|
-
const args = isShim ? ["/c", resolved, ...spec.args] : spec.args;
|
|
7982
|
+
const { file, args } = resolveSpawnTarget(spec.cmd, spec.args);
|
|
6757
7983
|
ptyRegistry().create({
|
|
6758
7984
|
sessionId,
|
|
6759
7985
|
cmd: file,
|
|
@@ -6844,16 +8070,16 @@ var init_update_check = __esm(() => {
|
|
|
6844
8070
|
});
|
|
6845
8071
|
|
|
6846
8072
|
// src/version.ts
|
|
6847
|
-
import { readFileSync as
|
|
6848
|
-
import { dirname as
|
|
6849
|
-
import { fileURLToPath as
|
|
8073
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
8074
|
+
import { dirname as dirname10, join as join18 } from "path";
|
|
8075
|
+
import { fileURLToPath as fileURLToPath8 } from "url";
|
|
6850
8076
|
function runcastleVersion() {
|
|
6851
8077
|
if (cached2)
|
|
6852
8078
|
return cached2;
|
|
6853
|
-
const here =
|
|
6854
|
-
for (const pkgPath of [
|
|
8079
|
+
const here = dirname10(fileURLToPath8(import.meta.url));
|
|
8080
|
+
for (const pkgPath of [join18(here, "..", "package.json"), join18(here, "package.json")]) {
|
|
6855
8081
|
try {
|
|
6856
|
-
const pkg = JSON.parse(
|
|
8082
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
6857
8083
|
if (typeof pkg.version === "string") {
|
|
6858
8084
|
cached2 = pkg.version;
|
|
6859
8085
|
return cached2;
|
|
@@ -6879,17 +8105,27 @@ var init_system = __esm(() => {
|
|
|
6879
8105
|
});
|
|
6880
8106
|
|
|
6881
8107
|
// src/trpc/routers/ticket.ts
|
|
6882
|
-
import * as
|
|
8108
|
+
import * as z13 from "zod";
|
|
6883
8109
|
var ticketRouter;
|
|
6884
8110
|
var init_ticket = __esm(() => {
|
|
6885
8111
|
init_features();
|
|
8112
|
+
init_repo();
|
|
6886
8113
|
init_tickets();
|
|
6887
8114
|
init_ticket_burner();
|
|
6888
8115
|
init_context();
|
|
6889
8116
|
ticketRouter = router({
|
|
6890
|
-
retry: publicProcedure.input(
|
|
6891
|
-
stop: publicProcedure.input(
|
|
6892
|
-
|
|
8117
|
+
retry: publicProcedure.input(z13.object({ ticketId: z13.string(), fresh: z13.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
|
|
8118
|
+
stop: publicProcedure.input(z13.object({ ticketId: z13.string() })).mutation(({ ctx, input }) => {
|
|
8119
|
+
if (stopTicketRun(input.ticketId))
|
|
8120
|
+
return { stopped: true, swept: false };
|
|
8121
|
+
const ticket = getTicket(ctx, input.ticketId);
|
|
8122
|
+
if (ticket.status === "burning" && !hasActiveRun(ctx, ticket.featureId)) {
|
|
8123
|
+
sweepOrphanedBurning(ctx, ticket.featureId, "orphaned \u2014 the run that was burning it is gone");
|
|
8124
|
+
return { stopped: false, swept: true };
|
|
8125
|
+
}
|
|
8126
|
+
return { stopped: false, swept: false };
|
|
8127
|
+
}),
|
|
8128
|
+
cancel: publicProcedure.input(z13.object({ ticketId: z13.string(), reason: z13.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason))
|
|
6893
8129
|
});
|
|
6894
8130
|
});
|
|
6895
8131
|
|
|
@@ -6920,17 +8156,18 @@ var init_router = __esm(() => {
|
|
|
6920
8156
|
});
|
|
6921
8157
|
|
|
6922
8158
|
// src/workflows/reconcile-runs.ts
|
|
6923
|
-
import { eq as
|
|
8159
|
+
import { eq as eq13 } from "drizzle-orm";
|
|
6924
8160
|
async function reconcileStaleRuns(ctx) {
|
|
6925
|
-
const stale = ctx.db.select().from(runs).where(
|
|
8161
|
+
const stale = ctx.db.select().from(runs).where(eq13(runs.status, "running")).all().map(rowToRun);
|
|
6926
8162
|
const reconciled = [];
|
|
6927
|
-
for (const
|
|
6928
|
-
if (isRunActive(
|
|
8163
|
+
for (const run4 of stale) {
|
|
8164
|
+
if (isRunActive(run4.id))
|
|
6929
8165
|
continue;
|
|
6930
|
-
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(
|
|
6931
|
-
const released = releaseForSession(ctx,
|
|
6932
|
-
|
|
6933
|
-
|
|
8166
|
+
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq13(runs.id, run4.id)).run();
|
|
8167
|
+
const released = releaseForSession(ctx, run4.id);
|
|
8168
|
+
const swept = workflowClaimsFeatureBranch(run4.workflow) ? sweepOrphanedBurning(ctx, run4.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
|
|
8169
|
+
if (workflowClaimsFeatureBranch(run4.workflow)) {
|
|
8170
|
+
const feature = tryGetFeature(ctx, run4.featureId);
|
|
6934
8171
|
const project = feature ? getProjectById(ctx, feature.projectId) : null;
|
|
6935
8172
|
if (feature && project) {
|
|
6936
8173
|
try {
|
|
@@ -6938,17 +8175,18 @@ async function reconcileStaleRuns(ctx) {
|
|
|
6938
8175
|
} catch {}
|
|
6939
8176
|
}
|
|
6940
8177
|
}
|
|
6941
|
-
emit(ctx,
|
|
8178
|
+
emit(ctx, run4.featureId, {
|
|
6942
8179
|
type: "run.reconciled",
|
|
6943
|
-
message: `run marked failed at boot \u2014 the server restarted while it was running (${
|
|
6944
|
-
runId:
|
|
8180
|
+
message: `run marked failed at boot \u2014 the server restarted while it was running (${run4.workflow})`,
|
|
8181
|
+
runId: run4.id,
|
|
6945
8182
|
data: {
|
|
6946
|
-
runId:
|
|
6947
|
-
workflow:
|
|
6948
|
-
releasedWaypointIds: released.map((w) => w.id)
|
|
8183
|
+
runId: run4.id,
|
|
8184
|
+
workflow: run4.workflow,
|
|
8185
|
+
releasedWaypointIds: released.map((w) => w.id),
|
|
8186
|
+
sweptTicketSeqs: swept.map((t2) => t2.seq)
|
|
6949
8187
|
}
|
|
6950
8188
|
});
|
|
6951
|
-
reconciled.push(
|
|
8189
|
+
reconciled.push(run4);
|
|
6952
8190
|
}
|
|
6953
8191
|
for (const project of allProjects(ctx)) {
|
|
6954
8192
|
try {
|
|
@@ -6963,6 +8201,7 @@ var init_reconcile_runs = __esm(() => {
|
|
|
6963
8201
|
init_events();
|
|
6964
8202
|
init_git();
|
|
6965
8203
|
init_repo();
|
|
8204
|
+
init_tickets();
|
|
6966
8205
|
init_waypoints();
|
|
6967
8206
|
init_runner();
|
|
6968
8207
|
});
|
|
@@ -7010,6 +8249,10 @@ async function startServer() {
|
|
|
7010
8249
|
if (staleRuns.length > 0) {
|
|
7011
8250
|
console.log(`reconciled ${staleRuns.length} stale run(s) from a previous server run`);
|
|
7012
8251
|
}
|
|
8252
|
+
const stalePreps = reconcilePreps(ctx);
|
|
8253
|
+
if (stalePreps > 0) {
|
|
8254
|
+
console.log(`reconciled ${stalePreps} stale preparation run(s) from a previous server run`);
|
|
8255
|
+
}
|
|
7013
8256
|
const server = Bun.serve({
|
|
7014
8257
|
port: config2.serverPort,
|
|
7015
8258
|
fetch(req, srv) {
|
|
@@ -7039,6 +8282,7 @@ var init_src2 = __esm(() => {
|
|
|
7039
8282
|
init_client();
|
|
7040
8283
|
init_migrate();
|
|
7041
8284
|
init_reconcile();
|
|
8285
|
+
init_prep();
|
|
7042
8286
|
init_server();
|
|
7043
8287
|
init_registry();
|
|
7044
8288
|
init_ws();
|
|
@@ -7103,7 +8347,7 @@ __export(exports_cli, {
|
|
|
7103
8347
|
resolveDoctorEnv: () => resolveDoctorEnv,
|
|
7104
8348
|
parseMode: () => parseMode
|
|
7105
8349
|
});
|
|
7106
|
-
import { existsSync as
|
|
8350
|
+
import { existsSync as existsSync19, readFileSync as readFileSync10 } from "fs";
|
|
7107
8351
|
function parseMode(argv) {
|
|
7108
8352
|
return argv.includes("--gate") || argv.includes("--boot") ? "gate" : "diagnostic";
|
|
7109
8353
|
}
|
|
@@ -7111,8 +8355,8 @@ function envWithToken() {
|
|
|
7111
8355
|
const merged = { ...process.env };
|
|
7112
8356
|
try {
|
|
7113
8357
|
const path = envPath();
|
|
7114
|
-
if (
|
|
7115
|
-
const fromFile = parseEnvFile(
|
|
8358
|
+
if (existsSync19(path)) {
|
|
8359
|
+
const fromFile = parseEnvFile(readFileSync10(path, "utf8")).CLAUDE_CODE_OAUTH_TOKEN;
|
|
7116
8360
|
if (fromFile && fromFile.length > 0)
|
|
7117
8361
|
merged.CLAUDE_CODE_OAUTH_TOKEN = fromFile;
|
|
7118
8362
|
}
|
|
@@ -7154,8 +8398,8 @@ var init_cli = __esm(() => {
|
|
|
7154
8398
|
// src/bin/runcastle.ts
|
|
7155
8399
|
init_asset_paths();
|
|
7156
8400
|
init_version();
|
|
7157
|
-
import { dirname as
|
|
7158
|
-
import { fileURLToPath as
|
|
8401
|
+
import { dirname as dirname11, resolve as resolve9 } from "path";
|
|
8402
|
+
import { fileURLToPath as fileURLToPath9 } from "url";
|
|
7159
8403
|
function parseCommand(argv) {
|
|
7160
8404
|
const [first, ...rest] = argv;
|
|
7161
8405
|
if (first === undefined)
|
|
@@ -7179,7 +8423,7 @@ Usage:
|
|
|
7179
8423
|
runcastle --help Show this help
|
|
7180
8424
|
`;
|
|
7181
8425
|
async function main(argv) {
|
|
7182
|
-
applyInstalledAssetEnv(resolve9(
|
|
8426
|
+
applyInstalledAssetEnv(resolve9(dirname11(fileURLToPath9(import.meta.url)), ".."));
|
|
7183
8427
|
const { command, args } = parseCommand(argv);
|
|
7184
8428
|
switch (command) {
|
|
7185
8429
|
case "version":
|