runcastle 1.0.7 → 1.0.9
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 +218 -29
- package/index.js +218 -29
- package/package.json +1 -1
- package/skills/burner/implement-ticket.md +9 -1
- package/web/assets/{index-Y3ehCYIg.js → index-C1LxVTPy.js} +2 -2
- package/web/assets/index-JQYIwCj3.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-EKuI75rm.css +0 -1
package/bin/runcastle.js
CHANGED
|
@@ -27,7 +27,8 @@ __export(exports_paths, {
|
|
|
27
27
|
envPath: () => envPath,
|
|
28
28
|
dbPath: () => dbPath,
|
|
29
29
|
dataDir: () => dataDir,
|
|
30
|
-
configPath: () => configPath
|
|
30
|
+
configPath: () => configPath,
|
|
31
|
+
burnCacheDir: () => burnCacheDir
|
|
31
32
|
});
|
|
32
33
|
import { homedir } from "os";
|
|
33
34
|
import { join } from "path";
|
|
@@ -52,6 +53,9 @@ function sessionDir(id) {
|
|
|
52
53
|
function worktreeDir(projectId, slug) {
|
|
53
54
|
return join(dataDir(), "worktrees", projectId, slug);
|
|
54
55
|
}
|
|
56
|
+
function burnCacheDir(pm) {
|
|
57
|
+
return join(dataDir(), "cache", pm);
|
|
58
|
+
}
|
|
55
59
|
function sandboxBuildDir() {
|
|
56
60
|
return join(dataDir(), "sandbox-build");
|
|
57
61
|
}
|
|
@@ -100,7 +104,9 @@ var init_config = __esm(() => {
|
|
|
100
104
|
sandbox: z.enum(["docker", "podman", "noSandbox"]).default("docker"),
|
|
101
105
|
mainBranch: z.string().default("main"),
|
|
102
106
|
sandboxImage: z.string().optional(),
|
|
103
|
-
burnConcurrency: z.number().int().min(1).max(8).default(3)
|
|
107
|
+
burnConcurrency: z.number().int().min(1).max(8).default(3),
|
|
108
|
+
burnMaxIterations: z.number().int().min(1).max(10).default(3),
|
|
109
|
+
setupCommand: z.string().optional()
|
|
104
110
|
}));
|
|
105
111
|
});
|
|
106
112
|
|
|
@@ -136,6 +142,11 @@ function loadConfig(env = process.env) {
|
|
|
136
142
|
if (env.RUNCASTLE_BURN_CONCURRENCY) {
|
|
137
143
|
overrides.burnConcurrency = Number(env.RUNCASTLE_BURN_CONCURRENCY);
|
|
138
144
|
}
|
|
145
|
+
if (env.RUNCASTLE_BURN_MAX_ITERATIONS) {
|
|
146
|
+
overrides.burnMaxIterations = Number(env.RUNCASTLE_BURN_MAX_ITERATIONS);
|
|
147
|
+
}
|
|
148
|
+
if (env.RUNCASTLE_SETUP_COMMAND)
|
|
149
|
+
overrides.setupCommand = env.RUNCASTLE_SETUP_COMMAND;
|
|
139
150
|
const base = typeof fileConfig === "object" && fileConfig !== null ? fileConfig : {};
|
|
140
151
|
return RuncastleConfig.parse({ ...base, ...overrides });
|
|
141
152
|
}
|
|
@@ -2116,11 +2127,14 @@ async function reattachWorktree(path, branch) {
|
|
|
2116
2127
|
await git(path).checkout(branch);
|
|
2117
2128
|
} catch {}
|
|
2118
2129
|
}
|
|
2130
|
+
function tempBranchSlugSegment(slug) {
|
|
2131
|
+
return slug.slice(0, TEMP_BRANCH_SLUG_MAX).replace(/-+$/, "");
|
|
2132
|
+
}
|
|
2119
2133
|
function researchBranchName(slug, waypointSeq, unique) {
|
|
2120
|
-
return `${RESEARCH_BRANCH_PREFIX}${slug}/${waypointSeq}-${unique}`;
|
|
2134
|
+
return `${RESEARCH_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${waypointSeq}-${unique}`;
|
|
2121
2135
|
}
|
|
2122
2136
|
function ticketBranchName(slug, ticketSeq, unique) {
|
|
2123
|
-
return `${TICKET_BRANCH_PREFIX}${slug}/${ticketSeq}-${unique}`;
|
|
2137
|
+
return `${TICKET_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${ticketSeq}-${unique}`;
|
|
2124
2138
|
}
|
|
2125
2139
|
async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
|
|
2126
2140
|
const g = git(repoPath);
|
|
@@ -2188,17 +2202,23 @@ async function cleanupTempBranches(repoPath) {
|
|
|
2188
2202
|
const prefix = tempBranchPrefix(name);
|
|
2189
2203
|
if (!prefix)
|
|
2190
2204
|
continue;
|
|
2191
|
-
const
|
|
2192
|
-
const
|
|
2205
|
+
const seg = name.slice(prefix.length).split("/")[0];
|
|
2206
|
+
const targets = seg ? all.filter((b) => {
|
|
2207
|
+
if (!b.startsWith("feature/"))
|
|
2208
|
+
return false;
|
|
2209
|
+
const featureSlug = b.slice("feature/".length);
|
|
2210
|
+
return featureSlug === seg || tempBranchSlugSegment(featureSlug) === seg;
|
|
2211
|
+
}) : [];
|
|
2193
2212
|
let merged = false;
|
|
2194
|
-
|
|
2213
|
+
for (const target of targets) {
|
|
2195
2214
|
try {
|
|
2196
2215
|
const tip = (await g.revparse([name])).trim();
|
|
2197
2216
|
const base = (await g.raw(["merge-base", name, target])).trim();
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2217
|
+
if (tip.length > 0 && tip === base) {
|
|
2218
|
+
merged = true;
|
|
2219
|
+
break;
|
|
2220
|
+
}
|
|
2221
|
+
} catch {}
|
|
2202
2222
|
}
|
|
2203
2223
|
if (merged && await deleteBranchDetachingWorktrees(g, repoPath, name)) {
|
|
2204
2224
|
deleted.push(name);
|
|
@@ -2345,7 +2365,7 @@ async function mergeInProgress(g) {
|
|
|
2345
2365
|
return false;
|
|
2346
2366
|
}
|
|
2347
2367
|
}
|
|
2348
|
-
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, testDriveState;
|
|
2368
|
+
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;
|
|
2349
2369
|
var init_git = __esm(() => {
|
|
2350
2370
|
init_paths();
|
|
2351
2371
|
init_errors();
|
|
@@ -2483,6 +2503,46 @@ var init_skills_root = __esm(() => {
|
|
|
2483
2503
|
SKILLS_MARKER = join7("packs", "runcastle");
|
|
2484
2504
|
});
|
|
2485
2505
|
|
|
2506
|
+
// src/services/agent-stream.ts
|
|
2507
|
+
function beginTranscript(ticketId) {
|
|
2508
|
+
transcripts.set(ticketId, { live: true, chunks: [], nextIndex: 0, bytes: 0 });
|
|
2509
|
+
}
|
|
2510
|
+
function appendTranscript(ticketId, chunk) {
|
|
2511
|
+
const t = transcripts.get(ticketId);
|
|
2512
|
+
if (!t)
|
|
2513
|
+
return;
|
|
2514
|
+
const full = { ...chunk, i: t.nextIndex, ts: chunk.ts ?? Date.now() };
|
|
2515
|
+
t.nextIndex += 1;
|
|
2516
|
+
t.chunks.push(full);
|
|
2517
|
+
t.bytes += full.text.length;
|
|
2518
|
+
while (t.chunks.length > MAX_CHUNKS || t.bytes > MAX_BYTES && t.chunks.length > 1) {
|
|
2519
|
+
const dropped = t.chunks.shift();
|
|
2520
|
+
if (!dropped)
|
|
2521
|
+
break;
|
|
2522
|
+
t.bytes -= dropped.text.length;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
function endTranscript(ticketId) {
|
|
2526
|
+
const t = transcripts.get(ticketId);
|
|
2527
|
+
if (t)
|
|
2528
|
+
t.live = false;
|
|
2529
|
+
}
|
|
2530
|
+
function readTranscript(ticketId, after = -1) {
|
|
2531
|
+
const t = transcripts.get(ticketId);
|
|
2532
|
+
if (!t)
|
|
2533
|
+
return { live: false, chunks: [], firstIndex: 0, nextIndex: 0 };
|
|
2534
|
+
return {
|
|
2535
|
+
live: t.live,
|
|
2536
|
+
chunks: after < 0 ? [...t.chunks] : t.chunks.filter((c) => c.i > after),
|
|
2537
|
+
firstIndex: t.chunks[0]?.i ?? t.nextIndex,
|
|
2538
|
+
nextIndex: t.nextIndex
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
var MAX_CHUNKS = 4000, MAX_BYTES = 1500000, transcripts;
|
|
2542
|
+
var init_agent_stream = __esm(() => {
|
|
2543
|
+
transcripts = new Map;
|
|
2544
|
+
});
|
|
2545
|
+
|
|
2486
2546
|
// src/workflows/ticket-burner.ts
|
|
2487
2547
|
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
|
|
2488
2548
|
import { dirname as dirname3, join as join8 } from "path";
|
|
@@ -2595,6 +2655,64 @@ function parseEnvFile(content) {
|
|
|
2595
2655
|
}
|
|
2596
2656
|
return out;
|
|
2597
2657
|
}
|
|
2658
|
+
function detectPackageManager(tc) {
|
|
2659
|
+
const pinned = tc.packageManagerField?.split("@")[0];
|
|
2660
|
+
if (pinned && PACKAGE_MANAGERS.includes(pinned)) {
|
|
2661
|
+
return pinned;
|
|
2662
|
+
}
|
|
2663
|
+
if (tc.lockfiles.bun)
|
|
2664
|
+
return "bun";
|
|
2665
|
+
if (tc.lockfiles.pnpm)
|
|
2666
|
+
return "pnpm";
|
|
2667
|
+
if (tc.lockfiles.yarn)
|
|
2668
|
+
return "yarn";
|
|
2669
|
+
if (tc.lockfiles.npm)
|
|
2670
|
+
return "npm";
|
|
2671
|
+
return tc.hasPackageJson ? "npm" : undefined;
|
|
2672
|
+
}
|
|
2673
|
+
function resolveSetupCommand(tc, override) {
|
|
2674
|
+
const trimmed = override?.trim();
|
|
2675
|
+
if (trimmed)
|
|
2676
|
+
return trimmed;
|
|
2677
|
+
const pm = detectPackageManager(tc);
|
|
2678
|
+
if (!pm)
|
|
2679
|
+
return;
|
|
2680
|
+
switch (pm) {
|
|
2681
|
+
case "bun":
|
|
2682
|
+
return tc.lockfiles.bun ? "bun install --frozen-lockfile" : "bun install";
|
|
2683
|
+
case "pnpm":
|
|
2684
|
+
return tc.lockfiles.pnpm ? "corepack pnpm install --frozen-lockfile" : "corepack pnpm install";
|
|
2685
|
+
case "yarn":
|
|
2686
|
+
return tc.lockfiles.yarn ? "corepack yarn install --frozen-lockfile" : "corepack yarn install";
|
|
2687
|
+
case "npm":
|
|
2688
|
+
return tc.lockfiles.npm ? "npm ci" : "npm install";
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
function cacheMountFor(pm, hostPath) {
|
|
2692
|
+
return { hostPath, sandboxPath: PM_CACHE_SANDBOX_PATHS[pm] };
|
|
2693
|
+
}
|
|
2694
|
+
function readRepoToolchain(repoPath) {
|
|
2695
|
+
const pkgPath = join8(repoPath, "package.json");
|
|
2696
|
+
const hasPackageJson = existsSync10(pkgPath);
|
|
2697
|
+
let packageManagerField;
|
|
2698
|
+
if (hasPackageJson) {
|
|
2699
|
+
try {
|
|
2700
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
2701
|
+
if (typeof pkg.packageManager === "string")
|
|
2702
|
+
packageManagerField = pkg.packageManager;
|
|
2703
|
+
} catch {}
|
|
2704
|
+
}
|
|
2705
|
+
return {
|
|
2706
|
+
hasPackageJson,
|
|
2707
|
+
...packageManagerField !== undefined ? { packageManagerField } : {},
|
|
2708
|
+
lockfiles: {
|
|
2709
|
+
bun: existsSync10(join8(repoPath, "bun.lock")) || existsSync10(join8(repoPath, "bun.lockb")),
|
|
2710
|
+
pnpm: existsSync10(join8(repoPath, "pnpm-lock.yaml")),
|
|
2711
|
+
yarn: existsSync10(join8(repoPath, "yarn.lock")),
|
|
2712
|
+
npm: existsSync10(join8(repoPath, "package-lock.json"))
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2598
2716
|
function createStreamThrottle(emit2, opts = {}) {
|
|
2599
2717
|
const intervalMs = opts.intervalMs ?? 2000;
|
|
2600
2718
|
const maxChars = opts.maxChars ?? 500;
|
|
@@ -2653,10 +2771,11 @@ function createSerialQueue() {
|
|
|
2653
2771
|
return next;
|
|
2654
2772
|
};
|
|
2655
2773
|
}
|
|
2656
|
-
function
|
|
2657
|
-
const
|
|
2658
|
-
`);
|
|
2659
|
-
|
|
2774
|
+
function errorHeadline(s) {
|
|
2775
|
+
const lines = s.split(`
|
|
2776
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
2777
|
+
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
2778
|
+
return causes.at(-1) ?? lines[0] ?? "";
|
|
2660
2779
|
}
|
|
2661
2780
|
async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
2662
2781
|
const width = Math.max(1, Math.floor(concurrency));
|
|
@@ -2711,7 +2830,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
2711
2830
|
failTicket(seq, outcome.error, outcome.event);
|
|
2712
2831
|
ctx.emitEvent({
|
|
2713
2832
|
type: "ticket.failed",
|
|
2714
|
-
message: `ticket ${t.seq} failed: ${
|
|
2833
|
+
message: `ticket ${t.seq} failed: ${errorHeadline(outcome.error)}`,
|
|
2715
2834
|
ticketId: t.id,
|
|
2716
2835
|
data: { error: outcome.error }
|
|
2717
2836
|
});
|
|
@@ -2845,8 +2964,11 @@ function buildBurnAgent(config2, token, model) {
|
|
|
2845
2964
|
}
|
|
2846
2965
|
return agent;
|
|
2847
2966
|
}
|
|
2848
|
-
function selectSandbox(config2) {
|
|
2849
|
-
const imageOpts = {
|
|
2967
|
+
function selectSandbox(config2, mounts = []) {
|
|
2968
|
+
const imageOpts = {
|
|
2969
|
+
imageName: resolveSandboxImage(config2),
|
|
2970
|
+
...mounts.length > 0 ? { mounts } : {}
|
|
2971
|
+
};
|
|
2850
2972
|
switch (config2.sandbox) {
|
|
2851
2973
|
case "docker":
|
|
2852
2974
|
return docker(imageOpts);
|
|
@@ -2866,24 +2988,62 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
|
|
|
2866
2988
|
DOCS_DIGEST: readDocsDigestFromDisk(project.id, feature.slug),
|
|
2867
2989
|
COMMIT_CONVENTION: `ticket(${ticket.seq}): <summary>`
|
|
2868
2990
|
});
|
|
2991
|
+
const toolchain = readRepoToolchain(project.repoPath);
|
|
2992
|
+
const pm = detectPackageManager(toolchain);
|
|
2993
|
+
const setupCommand = resolveSetupCommand(toolchain, config2.setupCommand);
|
|
2994
|
+
const mounts = [];
|
|
2995
|
+
if (config2.sandbox !== "noSandbox" && pm) {
|
|
2996
|
+
const hostCache = burnCacheDir(pm);
|
|
2997
|
+
mkdirSync4(hostCache, { recursive: true });
|
|
2998
|
+
mounts.push(cacheMountFor(pm, hostCache));
|
|
2999
|
+
}
|
|
3000
|
+
if (setupCommand) {
|
|
3001
|
+
ctx.emitEvent({
|
|
3002
|
+
type: "burn.setup",
|
|
3003
|
+
message: `installing deps before agent start: ${setupCommand}`,
|
|
3004
|
+
ticketId: ticket.id
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
2869
3007
|
mkdirSync4(logsDir(), { recursive: true });
|
|
2870
3008
|
const logFilePath = join8(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
|
|
2871
3009
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
3010
|
+
beginTranscript(ticket.id);
|
|
3011
|
+
const onStreamEvent = (event) => {
|
|
3012
|
+
throttle.onEvent(event);
|
|
3013
|
+
if (event.type === "text") {
|
|
3014
|
+
appendTranscript(ticket.id, { kind: "text", text: event.message });
|
|
3015
|
+
} else if (event.type === "toolCall") {
|
|
3016
|
+
appendTranscript(ticket.id, {
|
|
3017
|
+
kind: "tool",
|
|
3018
|
+
text: event.formattedArgs ?? "",
|
|
3019
|
+
name: event.name
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
};
|
|
2872
3023
|
const runOptions = {
|
|
2873
3024
|
agent: buildBurnAgent(config2, token, model),
|
|
2874
|
-
sandbox: selectSandbox(config2),
|
|
3025
|
+
sandbox: selectSandbox(config2, mounts),
|
|
2875
3026
|
cwd: project.repoPath,
|
|
2876
3027
|
prompt,
|
|
2877
3028
|
branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
|
|
2878
3029
|
signal: ctx.signal,
|
|
2879
3030
|
name: `ticket-${ticket.seq}`,
|
|
2880
|
-
|
|
3031
|
+
maxIterations: config2.burnMaxIterations,
|
|
3032
|
+
...setupCommand ? {
|
|
3033
|
+
hooks: {
|
|
3034
|
+
sandbox: {
|
|
3035
|
+
onSandboxReady: [{ command: setupCommand, timeoutMs: SETUP_HOOK_TIMEOUT_MS }]
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
} : {},
|
|
3039
|
+
logging: { type: "file", path: logFilePath, onAgentStreamEvent: onStreamEvent }
|
|
2881
3040
|
};
|
|
2882
3041
|
let result;
|
|
2883
3042
|
try {
|
|
2884
3043
|
result = await run(runOptions);
|
|
2885
3044
|
} catch (err) {
|
|
2886
3045
|
throttle.flush();
|
|
3046
|
+
endTranscript(ticket.id);
|
|
2887
3047
|
if (ctx.signal.aborted)
|
|
2888
3048
|
throw err;
|
|
2889
3049
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2900,6 +3060,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
|
|
|
2900
3060
|
return { status: "failed", error: msg };
|
|
2901
3061
|
}
|
|
2902
3062
|
throttle.flush();
|
|
3063
|
+
endTranscript(ticket.id);
|
|
2903
3064
|
const blocked = readBlockedFile([result.preservedWorktreePath, project.repoPath]);
|
|
2904
3065
|
const outcome = interpretRunResult(result, blocked);
|
|
2905
3066
|
if (outcome.status !== "done")
|
|
@@ -2919,7 +3080,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
|
|
|
2919
3080
|
}
|
|
2920
3081
|
return {
|
|
2921
3082
|
status: "failed",
|
|
2922
|
-
error: `ticket ${ticket.seq} committed to ${tempBranch} but landing on ${feature.branch} failed: ${
|
|
3083
|
+
error: `ticket ${ticket.seq} committed to ${tempBranch} but landing on ${feature.branch} failed: ${errorHeadline(detail)} \u2014 the branch is preserved for manual recovery`
|
|
2923
3084
|
};
|
|
2924
3085
|
}
|
|
2925
3086
|
return outcome;
|
|
@@ -2948,14 +3109,23 @@ function readTokenFromEnvFile(path) {
|
|
|
2948
3109
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
2949
3110
|
return token && token.length > 0 ? token : undefined;
|
|
2950
3111
|
}
|
|
2951
|
-
var AUTH_MISSING_EVENT = "auth.missing", AUTH_MISSING_MESSAGE = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PLACEHOLDERS, ticketBurner;
|
|
3112
|
+
var AUTH_MISSING_EVENT = "auth.missing", AUTH_MISSING_MESSAGE = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PLACEHOLDERS, PACKAGE_MANAGERS, PM_CACHE_SANDBOX_PATHS, SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
2952
3113
|
var init_ticket_burner = __esm(() => {
|
|
2953
3114
|
init_src();
|
|
2954
3115
|
init_config_load();
|
|
2955
3116
|
init_paths();
|
|
2956
3117
|
init_skills_root();
|
|
3118
|
+
init_agent_stream();
|
|
2957
3119
|
init_git();
|
|
2958
3120
|
PLACEHOLDERS = ["TICKET_JSON", "FEATURE_BRIEF", "DOCS_DIGEST", "COMMIT_CONVENTION"];
|
|
3121
|
+
PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
3122
|
+
PM_CACHE_SANDBOX_PATHS = {
|
|
3123
|
+
bun: "~/.bun/install/cache",
|
|
3124
|
+
pnpm: "~/.local/share/pnpm/store",
|
|
3125
|
+
yarn: "~/.cache/yarn",
|
|
3126
|
+
npm: "~/.npm"
|
|
3127
|
+
};
|
|
3128
|
+
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
2959
3129
|
ticketBurner = {
|
|
2960
3130
|
id: "ticket-burner",
|
|
2961
3131
|
run: (ctx) => burnRun(ctx, resolveBurnDeps(ctx))
|
|
@@ -3004,15 +3174,16 @@ async function researchRun(ctx, deps) {
|
|
|
3004
3174
|
}
|
|
3005
3175
|
ctx.emitEvent({
|
|
3006
3176
|
type: "research.failed",
|
|
3007
|
-
message: `waypoint ${waypoint.seq} research failed: ${
|
|
3177
|
+
message: `waypoint ${waypoint.seq} research failed: ${errorHeadline2(outcome.error)}`,
|
|
3008
3178
|
data: { waypointId: waypoint.id, error: outcome.error }
|
|
3009
3179
|
});
|
|
3010
3180
|
return { status: "failed", summary: outcome.error };
|
|
3011
3181
|
}
|
|
3012
|
-
function
|
|
3013
|
-
const
|
|
3014
|
-
`);
|
|
3015
|
-
|
|
3182
|
+
function errorHeadline2(s) {
|
|
3183
|
+
const lines = s.split(`
|
|
3184
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
3185
|
+
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
3186
|
+
return causes.at(-1) ?? lines[0] ?? "";
|
|
3016
3187
|
}
|
|
3017
3188
|
function createResearchStreamThrottle(emit2, opts = {}) {
|
|
3018
3189
|
return createStreamThrottle((e) => emit2({ ...e, type: e.type.replace(/^burn\./, "research.") }), opts);
|
|
@@ -3089,7 +3260,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
3089
3260
|
const detail = merge.error ?? "merge failed";
|
|
3090
3261
|
ctx.emitEvent({
|
|
3091
3262
|
type: merge.conflict ? "merge.conflict.needs-human" : "research.merge_failed",
|
|
3092
|
-
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}: ${
|
|
3263
|
+
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}: ${errorHeadline2(detail)} \u2014 the branch is preserved for manual recovery`,
|
|
3093
3264
|
data: { tempBranch, featureBranch: feature.branch, conflict: merge.conflict ?? false, error: detail }
|
|
3094
3265
|
});
|
|
3095
3266
|
return {
|
|
@@ -4833,11 +5004,13 @@ var init_project = __esm(() => {
|
|
|
4833
5004
|
import * as z8 from "zod";
|
|
4834
5005
|
var runRouter;
|
|
4835
5006
|
var init_run = __esm(() => {
|
|
5007
|
+
init_agent_stream();
|
|
4836
5008
|
init_repo();
|
|
4837
5009
|
init_runner();
|
|
4838
5010
|
init_context();
|
|
4839
5011
|
runRouter = router({
|
|
4840
5012
|
get: publicProcedure.input(z8.object({ runId: z8.string() })).query(({ ctx, input }) => getRunRow(ctx, input.runId)),
|
|
5013
|
+
agentTranscript: publicProcedure.input(z8.object({ ticketId: z8.string(), after: z8.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
|
|
4841
5014
|
cancel: publicProcedure.input(z8.object({ runId: z8.string() })).mutation(({ input }) => {
|
|
4842
5015
|
cancelRun(input.runId);
|
|
4843
5016
|
return { ok: true };
|
|
@@ -5073,6 +5246,22 @@ var init_settings = __esm(() => {
|
|
|
5073
5246
|
valueSchema: z9.number().int().min(1).max(8),
|
|
5074
5247
|
parseEnv: (raw) => Number(raw)
|
|
5075
5248
|
},
|
|
5249
|
+
{
|
|
5250
|
+
key: "burnMaxIterations",
|
|
5251
|
+
configKey: "burnMaxIterations",
|
|
5252
|
+
envVar: "RUNCASTLE_BURN_MAX_ITERATIONS",
|
|
5253
|
+
restartRequired: false,
|
|
5254
|
+
valueSchema: z9.number().int().min(1).max(10),
|
|
5255
|
+
parseEnv: (raw) => Number(raw)
|
|
5256
|
+
},
|
|
5257
|
+
{
|
|
5258
|
+
key: "setupCommand",
|
|
5259
|
+
configKey: "setupCommand",
|
|
5260
|
+
envVar: "RUNCASTLE_SETUP_COMMAND",
|
|
5261
|
+
restartRequired: false,
|
|
5262
|
+
valueSchema: z9.string().min(1),
|
|
5263
|
+
parseEnv: idEnv
|
|
5264
|
+
},
|
|
5076
5265
|
{
|
|
5077
5266
|
key: "mainBranch",
|
|
5078
5267
|
configKey: "mainBranch",
|