runcastle 1.0.23 → 1.0.24
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 +422 -57
- package/index.js +422 -57
- package/package.json +1 -1
- package/pty-host.cjs +5 -1
- package/skills/burner/implement-ticket.md +32 -3
- package/skills/burner/resolve-conflict.md +10 -1
- package/web/assets/{index-BcWKL2hj.js → index-Cpwb7Nqn.js} +2 -2
- package/web/assets/{index-fWdYT4eC.css → index-DBVAMsFt.css} +1 -1
- package/web/index.html +2 -2
package/bin/runcastle.js
CHANGED
|
@@ -105,10 +105,14 @@ var init_config = __esm(() => {
|
|
|
105
105
|
mainBranch: z.string().default("main"),
|
|
106
106
|
sandboxImage: z.string().optional(),
|
|
107
107
|
burnConcurrency: z.number().int().min(1).max(8).default(3),
|
|
108
|
+
burnCpus: z.number().positive().max(256).optional(),
|
|
109
|
+
burnGuard: z.boolean().default(true),
|
|
108
110
|
burnMaxIterations: z.number().int().min(1).max(10).default(3),
|
|
109
111
|
burnAttempts: z.number().int().min(1).max(5).default(3),
|
|
110
112
|
burnConflictAttempts: z.number().int().min(0).max(3).default(2),
|
|
111
113
|
setupCommand: z.string().optional(),
|
|
114
|
+
verifyCommands: z.string().optional(),
|
|
115
|
+
knownFailures: z.string().optional(),
|
|
112
116
|
burnWorkspace: z.enum(["auto", "mounted", "isolated"]).default("auto")
|
|
113
117
|
}));
|
|
114
118
|
});
|
|
@@ -154,8 +158,17 @@ function loadConfig(env = process.env) {
|
|
|
154
158
|
if (env.RUNCASTLE_BURN_CONFLICT_ATTEMPTS !== undefined) {
|
|
155
159
|
overrides.burnConflictAttempts = Number(env.RUNCASTLE_BURN_CONFLICT_ATTEMPTS);
|
|
156
160
|
}
|
|
161
|
+
if (env.RUNCASTLE_BURN_CPUS)
|
|
162
|
+
overrides.burnCpus = Number(env.RUNCASTLE_BURN_CPUS);
|
|
163
|
+
if (env.RUNCASTLE_BURN_GUARD !== undefined) {
|
|
164
|
+
overrides.burnGuard = env.RUNCASTLE_BURN_GUARD !== "0" && env.RUNCASTLE_BURN_GUARD !== "false";
|
|
165
|
+
}
|
|
157
166
|
if (env.RUNCASTLE_SETUP_COMMAND)
|
|
158
167
|
overrides.setupCommand = env.RUNCASTLE_SETUP_COMMAND;
|
|
168
|
+
if (env.RUNCASTLE_VERIFY_COMMANDS)
|
|
169
|
+
overrides.verifyCommands = env.RUNCASTLE_VERIFY_COMMANDS;
|
|
170
|
+
if (env.RUNCASTLE_KNOWN_FAILURES)
|
|
171
|
+
overrides.knownFailures = env.RUNCASTLE_KNOWN_FAILURES;
|
|
159
172
|
if (env.RUNCASTLE_BURN_WORKSPACE)
|
|
160
173
|
overrides.burnWorkspace = env.RUNCASTLE_BURN_WORKSPACE;
|
|
161
174
|
const base = typeof fileConfig === "object" && fileConfig !== null ? fileConfig : {};
|
|
@@ -660,6 +673,72 @@ var init_migrate = __esm(() => {
|
|
|
660
673
|
// src/util/resolve-executable.ts
|
|
661
674
|
import { existsSync as existsSync3 } from "fs";
|
|
662
675
|
import { win32, posix } from "path";
|
|
676
|
+
function wellKnownBinDirs(opts = {}) {
|
|
677
|
+
const env = opts.env ?? process.env;
|
|
678
|
+
const isWin = (opts.platform ?? process.platform) === "win32";
|
|
679
|
+
const dirs = [];
|
|
680
|
+
if (isWin) {
|
|
681
|
+
const home2 = env.USERPROFILE;
|
|
682
|
+
if (home2) {
|
|
683
|
+
dirs.push(win32.join(home2, ".local", "bin"), win32.join(home2, ".bun", "bin"));
|
|
684
|
+
}
|
|
685
|
+
if (env.APPDATA)
|
|
686
|
+
dirs.push(win32.join(env.APPDATA, "npm"));
|
|
687
|
+
if (env.LOCALAPPDATA)
|
|
688
|
+
dirs.push(win32.join(env.LOCALAPPDATA, "Programs", "claude"));
|
|
689
|
+
return dirs;
|
|
690
|
+
}
|
|
691
|
+
const home = env.HOME;
|
|
692
|
+
if (home) {
|
|
693
|
+
dirs.push(posix.join(home, ".local", "bin"), posix.join(home, ".bun", "bin"), posix.join(home, ".npm-global", "bin"));
|
|
694
|
+
}
|
|
695
|
+
dirs.push("/usr/local/bin", "/opt/homebrew/bin");
|
|
696
|
+
return dirs;
|
|
697
|
+
}
|
|
698
|
+
function resolveTool(name, opts = {}) {
|
|
699
|
+
const env = opts.env ?? process.env;
|
|
700
|
+
const overrideKey = BIN_OVERRIDE_ENV[name];
|
|
701
|
+
const override = opts.override ?? (overrideKey ? env[overrideKey] : undefined);
|
|
702
|
+
const extraDirs = opts.extraDirs ?? wellKnownBinDirs({ ...opts.platform ? { platform: opts.platform } : {}, env });
|
|
703
|
+
return resolveExecutable(name, { ...opts, extraDirs, ...override ? { override } : {} });
|
|
704
|
+
}
|
|
705
|
+
function spawnTargetFor(resolved, args) {
|
|
706
|
+
if (/\.(cmd|bat)$/i.test(resolved)) {
|
|
707
|
+
return { file: process.env.ComSpec ?? "cmd.exe", args: ["/c", resolved, ...args] };
|
|
708
|
+
}
|
|
709
|
+
if (/\.ps1$/i.test(resolved)) {
|
|
710
|
+
return {
|
|
711
|
+
file: "powershell.exe",
|
|
712
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", resolved, ...args]
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
return { file: resolved, args };
|
|
716
|
+
}
|
|
717
|
+
function resolveSpawnTarget(name, args) {
|
|
718
|
+
return spawnTargetFor(resolveTool(name), args);
|
|
719
|
+
}
|
|
720
|
+
function isBareName(cmd) {
|
|
721
|
+
return !cmd.includes("/") && !cmd.includes("\\");
|
|
722
|
+
}
|
|
723
|
+
function explainSpawnFailure(cmd, raw) {
|
|
724
|
+
const detail = raw.trim();
|
|
725
|
+
if (!isBareName(cmd)) {
|
|
726
|
+
return `Could not launch ${cmd}${detail ? ` \u2014 ${detail}` : ""}`;
|
|
727
|
+
}
|
|
728
|
+
const overrideKey = BIN_OVERRIDE_ENV[cmd];
|
|
729
|
+
const lines = [
|
|
730
|
+
`Could not launch \`${cmd}\`: not on the PATH this runcastle server inherited, nor in the usual install locations (~/.local/bin, npm global, ~/.bun/bin).`,
|
|
731
|
+
`A terminal where \`${cmd}\` works does not prove the server can see it: the server's PATH was captured when it started.`,
|
|
732
|
+
`Fix: quit runcastle and start it again from a terminal where \`${cmd} --version\` works.`
|
|
733
|
+
];
|
|
734
|
+
if (overrideKey) {
|
|
735
|
+
lines.push(`If that fails, set ${overrideKey} to the full path (\`where.exe ${cmd}\` / \`which ${cmd}\`) and restart.`);
|
|
736
|
+
}
|
|
737
|
+
if (detail)
|
|
738
|
+
lines.push(`(underlying error: ${detail})`);
|
|
739
|
+
return lines.join(`
|
|
740
|
+
`);
|
|
741
|
+
}
|
|
663
742
|
function resolveExecutable(name, opts = {}) {
|
|
664
743
|
const exists = opts.exists ?? existsSync3;
|
|
665
744
|
if (opts.override && exists(opts.override))
|
|
@@ -667,22 +746,28 @@ function resolveExecutable(name, opts = {}) {
|
|
|
667
746
|
const isWin = (opts.platform ?? process.platform) === "win32";
|
|
668
747
|
const exts = opts.exts ?? (isWin ? WIN_EXTS : [""]);
|
|
669
748
|
const pathEnv = opts.pathEnv ?? process.env.PATH ?? "";
|
|
670
|
-
const dirs = pathEnv.split(isWin ? ";" : ":");
|
|
671
749
|
const join5 = isWin ? win32.join : posix.join;
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
750
|
+
const scan = (dirs) => {
|
|
751
|
+
for (const dir of dirs) {
|
|
752
|
+
if (!dir)
|
|
753
|
+
continue;
|
|
754
|
+
for (const ext of exts) {
|
|
755
|
+
const candidate = join5(dir, `${name}${ext}`);
|
|
756
|
+
if (exists(candidate))
|
|
757
|
+
return candidate;
|
|
758
|
+
}
|
|
679
759
|
}
|
|
680
|
-
|
|
681
|
-
|
|
760
|
+
return null;
|
|
761
|
+
};
|
|
762
|
+
return scan(pathEnv.split(isWin ? ";" : ":")) ?? scan(opts.extraDirs ?? []) ?? name;
|
|
682
763
|
}
|
|
683
|
-
var WIN_EXTS;
|
|
764
|
+
var WIN_EXTS, BIN_OVERRIDE_ENV;
|
|
684
765
|
var init_resolve_executable = __esm(() => {
|
|
685
|
-
WIN_EXTS = [".exe", ".cmd", ".bat", ""];
|
|
766
|
+
WIN_EXTS = [".exe", ".cmd", ".bat", ".ps1", ""];
|
|
767
|
+
BIN_OVERRIDE_ENV = {
|
|
768
|
+
claude: "RUNCASTLE_CLAUDE_BIN",
|
|
769
|
+
node: "RUNCASTLE_NODE_BIN"
|
|
770
|
+
};
|
|
686
771
|
});
|
|
687
772
|
|
|
688
773
|
// src/pty/pty-sidecar.ts
|
|
@@ -699,7 +784,7 @@ function resolveNodeExecutable() {
|
|
|
699
784
|
return override;
|
|
700
785
|
if (!isBun())
|
|
701
786
|
return process.execPath;
|
|
702
|
-
return
|
|
787
|
+
return resolveTool("node", { exts: process.platform === "win32" ? [".exe", ".cmd", ""] : [""] });
|
|
703
788
|
}
|
|
704
789
|
function resolveNodePtyEntry() {
|
|
705
790
|
try {
|
|
@@ -767,11 +852,17 @@ function createSidecarPtySession(cmd, args, opts) {
|
|
|
767
852
|
case "exit":
|
|
768
853
|
fireExit(typeof msg.code === "number" ? msg.code : 0, msg.signal ?? undefined);
|
|
769
854
|
break;
|
|
770
|
-
case "error":
|
|
771
|
-
|
|
855
|
+
case "error": {
|
|
856
|
+
const why = explainSpawnFailure(cmd, msg.message ?? "unknown");
|
|
857
|
+
process.stderr.write(`[pty-sidecar] host error: ${why}
|
|
772
858
|
`);
|
|
859
|
+
emitData(Buffer.from(`\r
|
|
860
|
+
${why.replace(/\n/g, `\r
|
|
861
|
+
`)}\r
|
|
862
|
+
`, "utf8"));
|
|
773
863
|
fireExit(1);
|
|
774
864
|
break;
|
|
865
|
+
}
|
|
775
866
|
}
|
|
776
867
|
}
|
|
777
868
|
});
|
|
@@ -780,8 +871,13 @@ function createSidecarPtySession(cmd, args, opts) {
|
|
|
780
871
|
process.stderr.write(chunk);
|
|
781
872
|
});
|
|
782
873
|
child.on("error", (err) => {
|
|
783
|
-
|
|
874
|
+
const why = explainSpawnFailure(nodeExe, err.message);
|
|
875
|
+
process.stderr.write(`[pty-sidecar] failed to spawn node host: ${why}
|
|
784
876
|
`);
|
|
877
|
+
emitData(Buffer.from(`\r
|
|
878
|
+
${why.replace(/\n/g, `\r
|
|
879
|
+
`)}\r
|
|
880
|
+
`, "utf8"));
|
|
785
881
|
fireExit(1);
|
|
786
882
|
});
|
|
787
883
|
child.on("exit", (code, signal) => {
|
|
@@ -916,14 +1012,19 @@ function createPtySession(cmd, args, opts) {
|
|
|
916
1012
|
}
|
|
917
1013
|
function createNativePtySession(cmd, args, opts) {
|
|
918
1014
|
const pty = loadNodePty();
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1015
|
+
let proc;
|
|
1016
|
+
try {
|
|
1017
|
+
proc = pty.spawn(cmd, args, {
|
|
1018
|
+
name: "xterm-256color",
|
|
1019
|
+
cols: opts.cols ?? 80,
|
|
1020
|
+
rows: opts.rows ?? 24,
|
|
1021
|
+
cwd: opts.cwd,
|
|
1022
|
+
env: cleanEnv(opts.env),
|
|
1023
|
+
useConpty: opts.useConpty ?? true
|
|
1024
|
+
});
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
throw new Error(explainSpawnFailure(cmd, err instanceof Error ? err.message : String(err)));
|
|
1027
|
+
}
|
|
927
1028
|
let killed = false;
|
|
928
1029
|
proc.onExit(() => {
|
|
929
1030
|
killed = true;
|
|
@@ -958,6 +1059,7 @@ function createNativePtySession(cmd, args, opts) {
|
|
|
958
1059
|
}
|
|
959
1060
|
var cached = null, BACKEND_LOGGED;
|
|
960
1061
|
var init_pty = __esm(() => {
|
|
1062
|
+
init_resolve_executable();
|
|
961
1063
|
init_pty_sidecar();
|
|
962
1064
|
BACKEND_LOGGED = Symbol.for("runcastle.pty.backend.logged");
|
|
963
1065
|
});
|
|
@@ -1698,7 +1800,7 @@ var init_feature_docs = __esm(() => {
|
|
|
1698
1800
|
});
|
|
1699
1801
|
|
|
1700
1802
|
// src/services/tickets.ts
|
|
1701
|
-
import { asc as asc3, eq as eq5 } from "drizzle-orm";
|
|
1803
|
+
import { and as and5, asc as asc3, eq as eq5 } from "drizzle-orm";
|
|
1702
1804
|
function rowToTicket(row) {
|
|
1703
1805
|
return {
|
|
1704
1806
|
id: row.id,
|
|
@@ -1808,6 +1910,19 @@ function cancelTicket(ctx, id, reason) {
|
|
|
1808
1910
|
});
|
|
1809
1911
|
return getTicket(ctx, id);
|
|
1810
1912
|
}
|
|
1913
|
+
function sweepOrphanedBurning(ctx, featureId, reason) {
|
|
1914
|
+
const orphaned = ctx.db.select().from(tickets).where(and5(eq5(tickets.featureId, featureId), eq5(tickets.status, "burning"))).all().map(rowToTicket);
|
|
1915
|
+
for (const t of orphaned) {
|
|
1916
|
+
updateTicket(ctx, t.id, { status: "failed", error: reason });
|
|
1917
|
+
emit(ctx, featureId, {
|
|
1918
|
+
type: "ticket.failed",
|
|
1919
|
+
message: `ticket ${t.seq} failed: ${reason}`,
|
|
1920
|
+
ticketId: t.id,
|
|
1921
|
+
data: { error: reason, orphaned: true }
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
return orphaned.map((t) => ({ ...t, status: "failed", error: reason }));
|
|
1925
|
+
}
|
|
1811
1926
|
function updateTicket(ctx, id, patch) {
|
|
1812
1927
|
const current2 = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
1813
1928
|
if (!current2)
|
|
@@ -2814,6 +2929,89 @@ var init_agent_stream = __esm(() => {
|
|
|
2814
2929
|
transcripts = new Map;
|
|
2815
2930
|
});
|
|
2816
2931
|
|
|
2932
|
+
// src/workflows/burn-guard.ts
|
|
2933
|
+
function renderGuardScript(rules = GUARD_RULES) {
|
|
2934
|
+
const lines = [
|
|
2935
|
+
"#!/bin/sh",
|
|
2936
|
+
"# runcastle burn guard (PreToolUse). Generated \u2014 edit burn-guard.ts, not this.",
|
|
2937
|
+
"set -u",
|
|
2938
|
+
"payload=$(cat 2>/dev/null || true)",
|
|
2939
|
+
'[ -n "$payload" ] || exit 0',
|
|
2940
|
+
"command -v jq >/dev/null 2>&1 || exit 0",
|
|
2941
|
+
'tool=$(printf %s "$payload" | jq -r ".tool_name // empty" 2>/dev/null || true)',
|
|
2942
|
+
'[ "$tool" = "Bash" ] || exit 0',
|
|
2943
|
+
'cmd=$(printf %s "$payload" | jq -r ".tool_input.command // empty" 2>/dev/null || true)',
|
|
2944
|
+
'[ -n "$cmd" ] || exit 0',
|
|
2945
|
+
"",
|
|
2946
|
+
"# Match against the command with quoted spans blanked, so an argument is",
|
|
2947
|
+
'# never read as a command: `grep -rn "git stash" docs/` searches for a',
|
|
2948
|
+
"# string, it does not run one.",
|
|
2949
|
+
`cmd=$(printf %s "$cmd" | sed "s/'[^']*'/ /g; s/\\"[^\\"]*\\"/ /g")`,
|
|
2950
|
+
"",
|
|
2951
|
+
"deny() {",
|
|
2952
|
+
` printf %s "$1" | jq -R -s '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:.}}'`,
|
|
2953
|
+
" exit 0",
|
|
2954
|
+
"}",
|
|
2955
|
+
""
|
|
2956
|
+
];
|
|
2957
|
+
for (const rule of rules) {
|
|
2958
|
+
lines.push(`# ${rule.id}`, `if printf %s "$cmd" | grep -Eq -- ${shSingleQuote(rule.pattern)}; then`, ` deny ${shSingleQuote(rule.reason)}`, "fi", "");
|
|
2959
|
+
}
|
|
2960
|
+
lines.push("exit 0", "");
|
|
2961
|
+
return lines.join(`
|
|
2962
|
+
`);
|
|
2963
|
+
}
|
|
2964
|
+
function shSingleQuote(value) {
|
|
2965
|
+
return `'${value.split("'").join(`'\\''`)}'`;
|
|
2966
|
+
}
|
|
2967
|
+
function renderGuardSettings() {
|
|
2968
|
+
return {
|
|
2969
|
+
hooks: {
|
|
2970
|
+
PreToolUse: [
|
|
2971
|
+
{
|
|
2972
|
+
matcher: "Bash",
|
|
2973
|
+
hooks: [{ type: "command", command: `sh ${GUARD_SCRIPT_PATH}`, timeout: 5 }]
|
|
2974
|
+
}
|
|
2975
|
+
]
|
|
2976
|
+
}
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
function buildGuardInstallCommand() {
|
|
2980
|
+
const script = Buffer.from(renderGuardScript(), "utf8").toString("base64");
|
|
2981
|
+
const settings = Buffer.from(JSON.stringify(renderGuardSettings(), null, 2), "utf8").toString("base64");
|
|
2982
|
+
return [
|
|
2983
|
+
`mkdir -p "$HOME/.claude/hooks"`,
|
|
2984
|
+
`printf %s '${script}' | base64 -d > "${GUARD_SCRIPT_PATH}"`,
|
|
2985
|
+
`chmod +x "${GUARD_SCRIPT_PATH}"`,
|
|
2986
|
+
`printf %s '${settings}' | base64 -d > "${GUARD_SETTINGS_PATH}"`
|
|
2987
|
+
].join(" && ");
|
|
2988
|
+
}
|
|
2989
|
+
var CMD_START = "(^|[;&|(]|&&)[[:space:]]*", GUARD_RULES, GUARD_SCRIPT_PATH = "$HOME/.claude/hooks/burn-guard.sh", GUARD_SETTINGS_PATH = "$HOME/.claude/settings.json";
|
|
2990
|
+
var init_burn_guard = __esm(() => {
|
|
2991
|
+
GUARD_RULES = [
|
|
2992
|
+
{
|
|
2993
|
+
id: "no-stash",
|
|
2994
|
+
pattern: `${CMD_START}git[[:space:]]+stash`,
|
|
2995
|
+
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."
|
|
2996
|
+
},
|
|
2997
|
+
{
|
|
2998
|
+
id: "no-serialised-tests",
|
|
2999
|
+
pattern: "--maxWorkers|--runInBand|--pool=|--shard=|singleFork|--poolOptions",
|
|
3000
|
+
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.`
|
|
3001
|
+
},
|
|
3002
|
+
{
|
|
3003
|
+
id: "no-interpreter-heredoc-edit",
|
|
3004
|
+
pattern: `${CMD_START}(python3?|node|bun|perl|ruby)[[:space:]]+-[[:space:]]*<<`,
|
|
3005
|
+
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."
|
|
3006
|
+
},
|
|
3007
|
+
{
|
|
3008
|
+
id: "no-cat-heredoc-edit",
|
|
3009
|
+
pattern: `${CMD_START}cat[[:space:]]+>>?[[:space:]]*[^[:space:]|]+[[:space:]]*<<`,
|
|
3010
|
+
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."
|
|
3011
|
+
}
|
|
3012
|
+
];
|
|
3013
|
+
});
|
|
3014
|
+
|
|
2817
3015
|
// src/workflows/ticket-burner.ts
|
|
2818
3016
|
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
|
|
2819
3017
|
import { dirname as dirname3, join as join9 } from "path";
|
|
@@ -3020,6 +3218,24 @@ function buildWorkspaceNotes(mode) {
|
|
|
3020
3218
|
].join(`
|
|
3021
3219
|
`);
|
|
3022
3220
|
}
|
|
3221
|
+
function buildVerifyNotes(config2) {
|
|
3222
|
+
const commands = config2.verifyCommands?.trim();
|
|
3223
|
+
const failures = config2.knownFailures?.trim();
|
|
3224
|
+
const out = [];
|
|
3225
|
+
if (commands) {
|
|
3226
|
+
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, "```");
|
|
3227
|
+
} else {
|
|
3228
|
+
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.");
|
|
3229
|
+
}
|
|
3230
|
+
out.push("");
|
|
3231
|
+
if (failures) {
|
|
3232
|
+
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.");
|
|
3233
|
+
} else {
|
|
3234
|
+
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.");
|
|
3235
|
+
}
|
|
3236
|
+
return out.join(`
|
|
3237
|
+
`);
|
|
3238
|
+
}
|
|
3023
3239
|
function readRepoToolchain(repoPath) {
|
|
3024
3240
|
const pkgPath = join9(repoPath, "package.json");
|
|
3025
3241
|
const hasPackageJson = existsSync10(pkgPath);
|
|
@@ -3076,6 +3292,63 @@ function createStreamThrottle(emit2, opts = {}) {
|
|
|
3076
3292
|
};
|
|
3077
3293
|
return { onEvent, flush: flushText };
|
|
3078
3294
|
}
|
|
3295
|
+
function normalizeCommandForClassification(command) {
|
|
3296
|
+
return command.replace(/<<\s*['"]?(\w+)['"]?[\s\S]*?(^|\n)\s*\1\b/gm, "<<$1 ").replace(/<<\s*['"]?(\w+)['"]?[\s\S]*$/, "<<$1 ").replace(/'[^']*'/g, " ").replace(/"[^"]*"/g, " ");
|
|
3297
|
+
}
|
|
3298
|
+
function classifyToolCall(name, args) {
|
|
3299
|
+
const byName = TOOL_NAME_CATEGORY[name];
|
|
3300
|
+
if (byName)
|
|
3301
|
+
return byName;
|
|
3302
|
+
if (name !== "Bash")
|
|
3303
|
+
return "other";
|
|
3304
|
+
const normalized = normalizeCommandForClassification(args);
|
|
3305
|
+
for (const [category, pattern] of BASH_PATTERNS) {
|
|
3306
|
+
if (pattern.test(normalized))
|
|
3307
|
+
return category;
|
|
3308
|
+
}
|
|
3309
|
+
return "other";
|
|
3310
|
+
}
|
|
3311
|
+
function createToolTimer() {
|
|
3312
|
+
const byCategory = {};
|
|
3313
|
+
let calls = 0;
|
|
3314
|
+
let totalMs = 0;
|
|
3315
|
+
let pending = null;
|
|
3316
|
+
let iteration = null;
|
|
3317
|
+
const charge = (category, ms) => {
|
|
3318
|
+
const slot = byCategory[category] ??= { calls: 0, ms: 0 };
|
|
3319
|
+
slot.ms += ms;
|
|
3320
|
+
totalMs += ms;
|
|
3321
|
+
};
|
|
3322
|
+
const onEvent = (event) => {
|
|
3323
|
+
if (event.type === "raw")
|
|
3324
|
+
return;
|
|
3325
|
+
const at = event.timestamp.getTime();
|
|
3326
|
+
if (iteration !== null && event.iteration !== iteration)
|
|
3327
|
+
pending = null;
|
|
3328
|
+
iteration = event.iteration;
|
|
3329
|
+
if (pending) {
|
|
3330
|
+
const gap = at - pending.at;
|
|
3331
|
+
if (gap > 0 && gap <= MAX_ATTRIBUTABLE_GAP_MS)
|
|
3332
|
+
charge(pending.category, gap);
|
|
3333
|
+
}
|
|
3334
|
+
if (event.type === "toolCall") {
|
|
3335
|
+
const category = classifyToolCall(event.name, event.formattedArgs ?? "");
|
|
3336
|
+
const slot = byCategory[category] ??= { calls: 0, ms: 0 };
|
|
3337
|
+
slot.calls += 1;
|
|
3338
|
+
calls += 1;
|
|
3339
|
+
pending = { category, at };
|
|
3340
|
+
} else {
|
|
3341
|
+
pending = { category: "model", at };
|
|
3342
|
+
}
|
|
3343
|
+
};
|
|
3344
|
+
return { onEvent, summary: () => ({ totalMs, calls, byCategory }) };
|
|
3345
|
+
}
|
|
3346
|
+
function formatTimingSummary(s) {
|
|
3347
|
+
if (s.totalMs === 0)
|
|
3348
|
+
return `${s.calls} tool call(s), no measurable time`;
|
|
3349
|
+
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)}%`);
|
|
3350
|
+
return `${Math.round(s.totalMs / 60000)}min across ${s.calls} tool call(s) \u2014 ${parts.join(", ")}`;
|
|
3351
|
+
}
|
|
3079
3352
|
function interpretRunResult(result, blockedContent) {
|
|
3080
3353
|
const commits = result.commits.map((c) => c.sha);
|
|
3081
3354
|
if (commits.length > 0)
|
|
@@ -3391,10 +3664,7 @@ function buildBurnAgent(config2, token, model) {
|
|
|
3391
3664
|
return agent;
|
|
3392
3665
|
}
|
|
3393
3666
|
function selectSandbox(config2, mounts = []) {
|
|
3394
|
-
const imageOpts =
|
|
3395
|
-
imageName: resolveSandboxImage(config2),
|
|
3396
|
-
...mounts.length > 0 ? { mounts } : {}
|
|
3397
|
-
};
|
|
3667
|
+
const imageOpts = buildSandboxOptions(config2, mounts);
|
|
3398
3668
|
switch (config2.sandbox) {
|
|
3399
3669
|
case "docker":
|
|
3400
3670
|
return docker(imageOpts);
|
|
@@ -3404,6 +3674,13 @@ function selectSandbox(config2, mounts = []) {
|
|
|
3404
3674
|
return noSandbox();
|
|
3405
3675
|
}
|
|
3406
3676
|
}
|
|
3677
|
+
function buildSandboxOptions(config2, mounts = []) {
|
|
3678
|
+
return {
|
|
3679
|
+
imageName: resolveSandboxImage(config2),
|
|
3680
|
+
...mounts.length > 0 ? { mounts } : {},
|
|
3681
|
+
...config2.burnCpus !== undefined ? { cpus: config2.burnCpus } : {}
|
|
3682
|
+
};
|
|
3683
|
+
}
|
|
3407
3684
|
async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget) {
|
|
3408
3685
|
const { project, feature } = ctx;
|
|
3409
3686
|
const workspaceMode = resolveBurnWorkspaceMode(config2);
|
|
@@ -3413,11 +3690,13 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3413
3690
|
const featureBrief = buildFeatureBrief(feature);
|
|
3414
3691
|
const docsDigest = readDocsDigestFromDisk(project.id, feature.slug);
|
|
3415
3692
|
const workspaceNotes = buildWorkspaceNotes(workspaceMode);
|
|
3693
|
+
const verifyNotes = buildVerifyNotes(config2);
|
|
3416
3694
|
const basePrompt = renderTicketPrompt(readFileSync4(burnerTemplatePath(), "utf8"), {
|
|
3417
3695
|
TICKET_JSON: ticketJson,
|
|
3418
3696
|
FEATURE_BRIEF: featureBrief,
|
|
3419
3697
|
DOCS_DIGEST: docsDigest,
|
|
3420
3698
|
COMMIT_CONVENTION: `ticket(${ticket.seq}): <summary>`,
|
|
3699
|
+
VERIFY_NOTES: verifyNotes,
|
|
3421
3700
|
WORKSPACE_NOTES: workspaceNotes
|
|
3422
3701
|
});
|
|
3423
3702
|
const toolchain = readRepoToolchain(project.repoPath);
|
|
@@ -3431,12 +3710,16 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3431
3710
|
mounts.push(mount);
|
|
3432
3711
|
}
|
|
3433
3712
|
}
|
|
3713
|
+
const guardInstall = config2.burnGuard && config2.sandbox !== "noSandbox" ? buildGuardInstallCommand() : undefined;
|
|
3714
|
+
const withGuard = (setup) => guardInstall === undefined ? setup : setup ? `${guardInstall} && ${setup}` : guardInstall;
|
|
3434
3715
|
mkdirSync4(logsDir(), { recursive: true });
|
|
3435
3716
|
const logFilePath = join9(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
|
|
3436
3717
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
3437
3718
|
beginTranscript(ticket.id);
|
|
3719
|
+
const timer = createToolTimer();
|
|
3438
3720
|
const onStreamEvent = (event) => {
|
|
3439
3721
|
throttle.onEvent(event);
|
|
3722
|
+
timer.onEvent(event);
|
|
3440
3723
|
if (event.type === "text") {
|
|
3441
3724
|
appendTranscript(ticket.id, { kind: "text", text: event.message });
|
|
3442
3725
|
} else if (event.type === "toolCall") {
|
|
@@ -3481,12 +3764,13 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
|
|
|
3481
3764
|
FEATURE_BRIEF: featureBrief,
|
|
3482
3765
|
DOCS_DIGEST: docsDigest,
|
|
3483
3766
|
WORKSPACE_NOTES: workspaceNotes,
|
|
3767
|
+
VERIFY_NOTES: verifyNotes,
|
|
3484
3768
|
FEATURE_BRANCH: feature.branch,
|
|
3485
3769
|
CONFLICT_FILES: buildConflictFilesBlock(input.files),
|
|
3486
3770
|
OTHER_SIDE: buildOtherSideBlock(otherSide),
|
|
3487
3771
|
MERGE_COMMAND: resolveMergeCommand(workspaceMode, feature.branch)
|
|
3488
3772
|
});
|
|
3489
|
-
const hookCommand = workspaceMode === "isolated" ? buildIsolatedSetupCommand(resolveBranch, setupCommand, pm) : setupCommand;
|
|
3773
|
+
const hookCommand = withGuard(workspaceMode === "isolated" ? buildIsolatedSetupCommand(resolveBranch, setupCommand, pm) : setupCommand);
|
|
3490
3774
|
appendTranscript(ticket.id, {
|
|
3491
3775
|
kind: "text",
|
|
3492
3776
|
text: `
|
|
@@ -3616,7 +3900,7 @@ ${blocked.trim()}`
|
|
|
3616
3900
|
let tempBranch = "";
|
|
3617
3901
|
for (let attempt = 1;; attempt++) {
|
|
3618
3902
|
tempBranch = ticketBranchName(feature.slug, ticket.seq, newId("b").slice(2, 10));
|
|
3619
|
-
const hookCommand = workspaceMode === "isolated" ? buildIsolatedSetupCommand(tempBranch, setupCommand, pm) : setupCommand;
|
|
3903
|
+
const hookCommand = withGuard(workspaceMode === "isolated" ? buildIsolatedSetupCommand(tempBranch, setupCommand, pm) : setupCommand);
|
|
3620
3904
|
if (hookCommand && attempt === 1) {
|
|
3621
3905
|
ctx.emitEvent({
|
|
3622
3906
|
type: "burn.setup",
|
|
@@ -3751,6 +4035,15 @@ ${blocked.trim()}` };
|
|
|
3751
4035
|
activeTicketAborts.delete(ticket.id);
|
|
3752
4036
|
throttle.flush();
|
|
3753
4037
|
endTranscript(ticket.id);
|
|
4038
|
+
const timing = timer.summary();
|
|
4039
|
+
if (timing.calls > 0) {
|
|
4040
|
+
ctx.emitEvent({
|
|
4041
|
+
type: "ticket.timing",
|
|
4042
|
+
message: `ticket ${ticket.seq} spent ${formatTimingSummary(timing)}`,
|
|
4043
|
+
ticketId: ticket.id,
|
|
4044
|
+
data: timing
|
|
4045
|
+
});
|
|
4046
|
+
}
|
|
3754
4047
|
}
|
|
3755
4048
|
}
|
|
3756
4049
|
function resolveBurnDeps(ctx) {
|
|
@@ -3779,7 +4072,7 @@ function readTokenFromEnvFile(path) {
|
|
|
3779
4072
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
3780
4073
|
return token && token.length > 0 ? token : undefined;
|
|
3781
4074
|
}
|
|
3782
|
-
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", BURN_WORKTREE_PATH, WORKTREE_REMOVAL_FAILURES, FATAL_ERROR_PATTERNS, RETRYABLE_ERROR_PATTERNS, activeTicketAborts, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
4075
|
+
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;
|
|
3783
4076
|
var init_ticket_burner = __esm(() => {
|
|
3784
4077
|
init_src();
|
|
3785
4078
|
init_config_load();
|
|
@@ -3787,12 +4080,35 @@ var init_ticket_burner = __esm(() => {
|
|
|
3787
4080
|
init_skills_root();
|
|
3788
4081
|
init_agent_stream();
|
|
3789
4082
|
init_git();
|
|
4083
|
+
init_burn_guard();
|
|
3790
4084
|
PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
3791
4085
|
PM_CACHE_SANDBOX_PATHS = {
|
|
3792
4086
|
bun: "~/.bun/install/cache",
|
|
3793
4087
|
yarn: "~/.cache/yarn",
|
|
3794
4088
|
npm: "~/.npm"
|
|
3795
4089
|
};
|
|
4090
|
+
RUN = String.raw`(?![\w./-])`;
|
|
4091
|
+
BASH_PATTERNS = [
|
|
4092
|
+
["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`)],
|
|
4093
|
+
["typecheck", /\btsc\b|\btypecheck\b|\bmypy\b|\bcargo check\b/],
|
|
4094
|
+
["build", new RegExp(String.raw`\bbuild${RUN}|\bprisma\s+(generate|migrate)\b|\bcodegen${RUN}`)],
|
|
4095
|
+
["lint", new RegExp(String.raw`\b(eslint|prettier|ruff|biome|lint|format)${RUN}`)],
|
|
4096
|
+
["install", /\b(pnpm|npm|yarn|bun|corepack)\s+\w*\s*install\b|\bnpm ci\b/],
|
|
4097
|
+
["git", /\bgit\b/],
|
|
4098
|
+
["search", /\b(grep|rg|ag|find)\b/],
|
|
4099
|
+
["file-read", /\b(cat|sed|head|tail|less|wc|ls)\b/],
|
|
4100
|
+
["file-edit", /<<\s*['"]?(PY|EOF|SH|JS|TS)\b|\bpython3?\s+-\s*<</]
|
|
4101
|
+
];
|
|
4102
|
+
TOOL_NAME_CATEGORY = {
|
|
4103
|
+
Read: "file-read",
|
|
4104
|
+
NotebookRead: "file-read",
|
|
4105
|
+
Grep: "search",
|
|
4106
|
+
Glob: "search",
|
|
4107
|
+
Edit: "file-edit",
|
|
4108
|
+
Write: "file-edit",
|
|
4109
|
+
NotebookEdit: "file-edit"
|
|
4110
|
+
};
|
|
4111
|
+
MAX_ATTRIBUTABLE_GAP_MS = 20 * 60000;
|
|
3796
4112
|
BURN_WORKTREE_PATH = /[\\/]\.sandcastle[\\/]worktrees[\\/]/i;
|
|
3797
4113
|
WORKTREE_REMOVAL_FAILURES = [
|
|
3798
4114
|
/failed to delete/i,
|
|
@@ -4093,7 +4409,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
4093
4409
|
},
|
|
4094
4410
|
signal: controller.signal
|
|
4095
4411
|
};
|
|
4096
|
-
const done = executeRun(ctx, runId, featureId, def.run(wctx), controller, async () => {
|
|
4412
|
+
const done = executeRun(ctx, runId, featureId, workflowId, def.run(wctx), controller, async () => {
|
|
4097
4413
|
if (talkDetached)
|
|
4098
4414
|
await reattachWorktree(talkWorktree, feature.branch);
|
|
4099
4415
|
});
|
|
@@ -4102,7 +4418,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
4102
4418
|
function cancelRun(runId) {
|
|
4103
4419
|
controllers.get(runId)?.abort();
|
|
4104
4420
|
}
|
|
4105
|
-
async function executeRun(ctx, runId, featureId, runPromise, controller, cleanup) {
|
|
4421
|
+
async function executeRun(ctx, runId, featureId, workflow2, runPromise, controller, cleanup) {
|
|
4106
4422
|
let status = "failed";
|
|
4107
4423
|
let summary = "run failed";
|
|
4108
4424
|
try {
|
|
@@ -4123,6 +4439,9 @@ async function executeRun(ctx, runId, featureId, runPromise, controller, cleanup
|
|
|
4123
4439
|
}
|
|
4124
4440
|
ctx.db.update(runs).set({ status, endedAt: Date.now(), summary }).where(eq6(runs.id, runId)).run();
|
|
4125
4441
|
releaseForSession(ctx, runId);
|
|
4442
|
+
if (workflowClaimsFeatureBranch(workflow2)) {
|
|
4443
|
+
sweepOrphanedBurning(ctx, featureId, `orphaned \u2014 the run ended (${status}) while it was burning`);
|
|
4444
|
+
}
|
|
4126
4445
|
emit(ctx, featureId, {
|
|
4127
4446
|
type: "run.finished",
|
|
4128
4447
|
message: `run ${status}: ${summary}`,
|
|
@@ -4264,7 +4583,7 @@ function advance(ctx, featureId) {
|
|
|
4264
4583
|
async function burn(ctx, featureId, opts = {}) {
|
|
4265
4584
|
const feature = getFeatureRow(ctx, featureId);
|
|
4266
4585
|
const running = hasActiveRun(ctx, featureId);
|
|
4267
|
-
|
|
4586
|
+
let tickets2 = listByFeature2(ctx, featureId);
|
|
4268
4587
|
const pending = tickets2.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
4269
4588
|
const restarting = feature.phase === "implementation" && !running;
|
|
4270
4589
|
const iterating = feature.phase === "review" && !running && pending.length >= 1;
|
|
@@ -4282,6 +4601,8 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
4282
4601
|
throw new GateError(tickets2.length > 0 ? "no burnable tickets \u2014 every ticket is cancelled" : "no tickets to burn");
|
|
4283
4602
|
}
|
|
4284
4603
|
if (restarting) {
|
|
4604
|
+
sweepOrphanedBurning(ctx, featureId, "orphaned \u2014 the previous run died while it was burning");
|
|
4605
|
+
tickets2 = listByFeature2(ctx, featureId);
|
|
4285
4606
|
const failed = opts.resetFailed === false ? [] : tickets2.filter((t) => t.status === "failed");
|
|
4286
4607
|
for (const t of failed) {
|
|
4287
4608
|
updateTicket(ctx, t.id, { status: "pending", error: null });
|
|
@@ -5385,7 +5706,7 @@ var init_artifacts = __esm(() => {
|
|
|
5385
5706
|
import { existsSync as existsSync13 } from "fs";
|
|
5386
5707
|
import { dirname as dirname5, join as join13, resolve as resolve7 } from "path";
|
|
5387
5708
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
5388
|
-
import { and as
|
|
5709
|
+
import { and as and6, eq as eq8 } from "drizzle-orm";
|
|
5389
5710
|
function ptyExitMessage(exitCode) {
|
|
5390
5711
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
5391
5712
|
return `terminal exited (code ${label})`;
|
|
@@ -5411,14 +5732,10 @@ function buildClaudeArgs(input) {
|
|
|
5411
5732
|
];
|
|
5412
5733
|
}
|
|
5413
5734
|
function resolveClaudeExecutable() {
|
|
5414
|
-
return
|
|
5735
|
+
return resolveTool("claude");
|
|
5415
5736
|
}
|
|
5416
5737
|
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 };
|
|
5738
|
+
return spawnTargetFor(resolveClaudeExecutable(), claudeArgs);
|
|
5422
5739
|
}
|
|
5423
5740
|
function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
|
|
5424
5741
|
const rel = join13("packages", "skills", "packs", "runcastle");
|
|
@@ -5446,7 +5763,7 @@ function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
|
|
|
5446
5763
|
`)}`);
|
|
5447
5764
|
}
|
|
5448
5765
|
function activeRunFor(ctx, featureId) {
|
|
5449
|
-
const row = ctx.db.select().from(runs).where(
|
|
5766
|
+
const row = ctx.db.select().from(runs).where(and6(eq8(runs.featureId, featureId), eq8(runs.status, "running"))).limit(1).get();
|
|
5450
5767
|
return row ? rowToRun(row) : null;
|
|
5451
5768
|
}
|
|
5452
5769
|
function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
@@ -6333,6 +6650,14 @@ var init_settings = __esm(() => {
|
|
|
6333
6650
|
valueSchema: z9.number().int().min(0).max(3),
|
|
6334
6651
|
parseEnv: (raw) => Number(raw)
|
|
6335
6652
|
},
|
|
6653
|
+
{
|
|
6654
|
+
key: "burnCpus",
|
|
6655
|
+
configKey: "burnCpus",
|
|
6656
|
+
envVar: "RUNCASTLE_BURN_CPUS",
|
|
6657
|
+
restartRequired: false,
|
|
6658
|
+
valueSchema: z9.number().positive().max(256),
|
|
6659
|
+
parseEnv: (raw) => Number(raw)
|
|
6660
|
+
},
|
|
6336
6661
|
{
|
|
6337
6662
|
key: "setupCommand",
|
|
6338
6663
|
configKey: "setupCommand",
|
|
@@ -6341,6 +6666,22 @@ var init_settings = __esm(() => {
|
|
|
6341
6666
|
valueSchema: z9.string().min(1),
|
|
6342
6667
|
parseEnv: idEnv
|
|
6343
6668
|
},
|
|
6669
|
+
{
|
|
6670
|
+
key: "verifyCommands",
|
|
6671
|
+
configKey: "verifyCommands",
|
|
6672
|
+
envVar: "RUNCASTLE_VERIFY_COMMANDS",
|
|
6673
|
+
restartRequired: false,
|
|
6674
|
+
valueSchema: z9.string().min(1),
|
|
6675
|
+
parseEnv: idEnv
|
|
6676
|
+
},
|
|
6677
|
+
{
|
|
6678
|
+
key: "knownFailures",
|
|
6679
|
+
configKey: "knownFailures",
|
|
6680
|
+
envVar: "RUNCASTLE_KNOWN_FAILURES",
|
|
6681
|
+
restartRequired: false,
|
|
6682
|
+
valueSchema: z9.string().min(1),
|
|
6683
|
+
parseEnv: idEnv
|
|
6684
|
+
},
|
|
6344
6685
|
{
|
|
6345
6686
|
key: "mainBranch",
|
|
6346
6687
|
configKey: "mainBranch",
|
|
@@ -6539,10 +6880,7 @@ var init_doctor = __esm(() => {
|
|
|
6539
6880
|
import { spawn as spawn2 } from "child_process";
|
|
6540
6881
|
function createSystemExec(opts = {}) {
|
|
6541
6882
|
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;
|
|
6883
|
+
const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
|
|
6546
6884
|
let stdout = "";
|
|
6547
6885
|
let stderr = "";
|
|
6548
6886
|
const child = spawn2(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
|
|
@@ -6693,12 +7031,29 @@ function prepareSandboxBuildContext() {
|
|
|
6693
7031
|
}
|
|
6694
7032
|
function createTokenVerifier(exec) {
|
|
6695
7033
|
return async (token) => {
|
|
7034
|
+
if (token.length < 8) {
|
|
7035
|
+
return {
|
|
7036
|
+
valid: false,
|
|
7037
|
+
detail: "token looks malformed (too short) \u2014 saved to ~/.runcastle/.env anyway",
|
|
7038
|
+
fix: "Re-run `claude setup-token` and paste the whole line it prints (it starts with `sk-ant-oat`)."
|
|
7039
|
+
};
|
|
7040
|
+
}
|
|
6696
7041
|
const out = await exec("claude", ["--version"]);
|
|
6697
|
-
if (!
|
|
6698
|
-
return {
|
|
7042
|
+
if (!out.ok) {
|
|
7043
|
+
return {
|
|
7044
|
+
valid: false,
|
|
7045
|
+
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.",
|
|
7046
|
+
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."
|
|
7047
|
+
};
|
|
6699
7048
|
}
|
|
6700
|
-
if (
|
|
6701
|
-
|
|
7049
|
+
if (out.code !== 0) {
|
|
7050
|
+
const why = (out.stderr.trim() || out.stdout.trim()).split(`
|
|
7051
|
+
`)[0] ?? "";
|
|
7052
|
+
return {
|
|
7053
|
+
valid: false,
|
|
7054
|
+
detail: `Token saved to ~/.runcastle/.env, but \`claude --version\` exited ${out.code}` + `${why ? `: ${why}` : " with no output"}.`,
|
|
7055
|
+
fix: "Run `claude --version` yourself and fix what it reports \u2014 the Claude Code install is broken, not the token."
|
|
7056
|
+
};
|
|
6702
7057
|
}
|
|
6703
7058
|
return { valid: true, detail: "token captured to ~/.runcastle/.env" };
|
|
6704
7059
|
};
|
|
@@ -6750,10 +7105,7 @@ var init_setup2 = __esm(() => {
|
|
|
6750
7105
|
const spec = terminalSpec(input.kind, { runtime, imageName, sandcastleBin });
|
|
6751
7106
|
const sessionId = newId("setup");
|
|
6752
7107
|
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;
|
|
7108
|
+
const { file, args } = resolveSpawnTarget(spec.cmd, spec.args);
|
|
6757
7109
|
ptyRegistry().create({
|
|
6758
7110
|
sessionId,
|
|
6759
7111
|
cmd: file,
|
|
@@ -6883,12 +7235,22 @@ import * as z12 from "zod";
|
|
|
6883
7235
|
var ticketRouter;
|
|
6884
7236
|
var init_ticket = __esm(() => {
|
|
6885
7237
|
init_features();
|
|
7238
|
+
init_repo();
|
|
6886
7239
|
init_tickets();
|
|
6887
7240
|
init_ticket_burner();
|
|
6888
7241
|
init_context();
|
|
6889
7242
|
ticketRouter = router({
|
|
6890
7243
|
retry: publicProcedure.input(z12.object({ ticketId: z12.string(), fresh: z12.boolean().optional() })).mutation(({ ctx, input }) => retryTicket(ctx, input.ticketId, { fresh: input.fresh })),
|
|
6891
|
-
stop: publicProcedure.input(z12.object({ ticketId: z12.string() })).mutation(({ input }) =>
|
|
7244
|
+
stop: publicProcedure.input(z12.object({ ticketId: z12.string() })).mutation(({ ctx, input }) => {
|
|
7245
|
+
if (stopTicketRun(input.ticketId))
|
|
7246
|
+
return { stopped: true, swept: false };
|
|
7247
|
+
const ticket = getTicket(ctx, input.ticketId);
|
|
7248
|
+
if (ticket.status === "burning" && !hasActiveRun(ctx, ticket.featureId)) {
|
|
7249
|
+
sweepOrphanedBurning(ctx, ticket.featureId, "orphaned \u2014 the run that was burning it is gone");
|
|
7250
|
+
return { stopped: false, swept: true };
|
|
7251
|
+
}
|
|
7252
|
+
return { stopped: false, swept: false };
|
|
7253
|
+
}),
|
|
6892
7254
|
cancel: publicProcedure.input(z12.object({ ticketId: z12.string(), reason: z12.string().optional() })).mutation(({ ctx, input }) => cancelTicket(ctx, input.ticketId, input.reason))
|
|
6893
7255
|
});
|
|
6894
7256
|
});
|
|
@@ -6929,6 +7291,7 @@ async function reconcileStaleRuns(ctx) {
|
|
|
6929
7291
|
continue;
|
|
6930
7292
|
ctx.db.update(runs).set({ status: "failed", endedAt: Date.now(), summary: "orphaned by server restart" }).where(eq11(runs.id, run3.id)).run();
|
|
6931
7293
|
const released = releaseForSession(ctx, run3.id);
|
|
7294
|
+
const swept = workflowClaimsFeatureBranch(run3.workflow) ? sweepOrphanedBurning(ctx, run3.featureId, "orphaned by server restart \u2014 retry to resume its commits") : [];
|
|
6932
7295
|
if (workflowClaimsFeatureBranch(run3.workflow)) {
|
|
6933
7296
|
const feature = tryGetFeature(ctx, run3.featureId);
|
|
6934
7297
|
const project = feature ? getProjectById(ctx, feature.projectId) : null;
|
|
@@ -6945,7 +7308,8 @@ async function reconcileStaleRuns(ctx) {
|
|
|
6945
7308
|
data: {
|
|
6946
7309
|
runId: run3.id,
|
|
6947
7310
|
workflow: run3.workflow,
|
|
6948
|
-
releasedWaypointIds: released.map((w) => w.id)
|
|
7311
|
+
releasedWaypointIds: released.map((w) => w.id),
|
|
7312
|
+
sweptTicketSeqs: swept.map((t2) => t2.seq)
|
|
6949
7313
|
}
|
|
6950
7314
|
});
|
|
6951
7315
|
reconciled.push(run3);
|
|
@@ -6963,6 +7327,7 @@ var init_reconcile_runs = __esm(() => {
|
|
|
6963
7327
|
init_events();
|
|
6964
7328
|
init_git();
|
|
6965
7329
|
init_repo();
|
|
7330
|
+
init_tickets();
|
|
6966
7331
|
init_waypoints();
|
|
6967
7332
|
init_runner();
|
|
6968
7333
|
});
|