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/index.js
CHANGED
|
@@ -26,7 +26,8 @@ __export(exports_paths, {
|
|
|
26
26
|
envPath: () => envPath,
|
|
27
27
|
dbPath: () => dbPath,
|
|
28
28
|
dataDir: () => dataDir,
|
|
29
|
-
configPath: () => configPath
|
|
29
|
+
configPath: () => configPath,
|
|
30
|
+
burnCacheDir: () => burnCacheDir
|
|
30
31
|
});
|
|
31
32
|
import { homedir } from "os";
|
|
32
33
|
import { join } from "path";
|
|
@@ -51,6 +52,9 @@ function sessionDir(id) {
|
|
|
51
52
|
function worktreeDir(projectId, slug) {
|
|
52
53
|
return join(dataDir(), "worktrees", projectId, slug);
|
|
53
54
|
}
|
|
55
|
+
function burnCacheDir(pm) {
|
|
56
|
+
return join(dataDir(), "cache", pm);
|
|
57
|
+
}
|
|
54
58
|
function sandboxBuildDir() {
|
|
55
59
|
return join(dataDir(), "sandbox-build");
|
|
56
60
|
}
|
|
@@ -99,7 +103,9 @@ var init_config = __esm(() => {
|
|
|
99
103
|
sandbox: z.enum(["docker", "podman", "noSandbox"]).default("docker"),
|
|
100
104
|
mainBranch: z.string().default("main"),
|
|
101
105
|
sandboxImage: z.string().optional(),
|
|
102
|
-
burnConcurrency: z.number().int().min(1).max(8).default(3)
|
|
106
|
+
burnConcurrency: z.number().int().min(1).max(8).default(3),
|
|
107
|
+
burnMaxIterations: z.number().int().min(1).max(10).default(3),
|
|
108
|
+
setupCommand: z.string().optional()
|
|
103
109
|
}));
|
|
104
110
|
});
|
|
105
111
|
|
|
@@ -135,6 +141,11 @@ function loadConfig(env = process.env) {
|
|
|
135
141
|
if (env.RUNCASTLE_BURN_CONCURRENCY) {
|
|
136
142
|
overrides.burnConcurrency = Number(env.RUNCASTLE_BURN_CONCURRENCY);
|
|
137
143
|
}
|
|
144
|
+
if (env.RUNCASTLE_BURN_MAX_ITERATIONS) {
|
|
145
|
+
overrides.burnMaxIterations = Number(env.RUNCASTLE_BURN_MAX_ITERATIONS);
|
|
146
|
+
}
|
|
147
|
+
if (env.RUNCASTLE_SETUP_COMMAND)
|
|
148
|
+
overrides.setupCommand = env.RUNCASTLE_SETUP_COMMAND;
|
|
138
149
|
const base = typeof fileConfig === "object" && fileConfig !== null ? fileConfig : {};
|
|
139
150
|
return RuncastleConfig.parse({ ...base, ...overrides });
|
|
140
151
|
}
|
|
@@ -2115,11 +2126,14 @@ async function reattachWorktree(path, branch) {
|
|
|
2115
2126
|
await git(path).checkout(branch);
|
|
2116
2127
|
} catch {}
|
|
2117
2128
|
}
|
|
2129
|
+
function tempBranchSlugSegment(slug) {
|
|
2130
|
+
return slug.slice(0, TEMP_BRANCH_SLUG_MAX).replace(/-+$/, "");
|
|
2131
|
+
}
|
|
2118
2132
|
function researchBranchName(slug, waypointSeq, unique) {
|
|
2119
|
-
return `${RESEARCH_BRANCH_PREFIX}${slug}/${waypointSeq}-${unique}`;
|
|
2133
|
+
return `${RESEARCH_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${waypointSeq}-${unique}`;
|
|
2120
2134
|
}
|
|
2121
2135
|
function ticketBranchName(slug, ticketSeq, unique) {
|
|
2122
|
-
return `${TICKET_BRANCH_PREFIX}${slug}/${ticketSeq}-${unique}`;
|
|
2136
|
+
return `${TICKET_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${ticketSeq}-${unique}`;
|
|
2123
2137
|
}
|
|
2124
2138
|
async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
|
|
2125
2139
|
const g = git(repoPath);
|
|
@@ -2187,17 +2201,23 @@ async function cleanupTempBranches(repoPath) {
|
|
|
2187
2201
|
const prefix = tempBranchPrefix(name);
|
|
2188
2202
|
if (!prefix)
|
|
2189
2203
|
continue;
|
|
2190
|
-
const
|
|
2191
|
-
const
|
|
2204
|
+
const seg = name.slice(prefix.length).split("/")[0];
|
|
2205
|
+
const targets = seg ? all.filter((b) => {
|
|
2206
|
+
if (!b.startsWith("feature/"))
|
|
2207
|
+
return false;
|
|
2208
|
+
const featureSlug = b.slice("feature/".length);
|
|
2209
|
+
return featureSlug === seg || tempBranchSlugSegment(featureSlug) === seg;
|
|
2210
|
+
}) : [];
|
|
2192
2211
|
let merged = false;
|
|
2193
|
-
|
|
2212
|
+
for (const target of targets) {
|
|
2194
2213
|
try {
|
|
2195
2214
|
const tip = (await g.revparse([name])).trim();
|
|
2196
2215
|
const base = (await g.raw(["merge-base", name, target])).trim();
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2216
|
+
if (tip.length > 0 && tip === base) {
|
|
2217
|
+
merged = true;
|
|
2218
|
+
break;
|
|
2219
|
+
}
|
|
2220
|
+
} catch {}
|
|
2201
2221
|
}
|
|
2202
2222
|
if (merged && await deleteBranchDetachingWorktrees(g, repoPath, name)) {
|
|
2203
2223
|
deleted.push(name);
|
|
@@ -2344,7 +2364,7 @@ async function mergeInProgress(g) {
|
|
|
2344
2364
|
return false;
|
|
2345
2365
|
}
|
|
2346
2366
|
}
|
|
2347
|
-
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;
|
|
2367
|
+
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;
|
|
2348
2368
|
var init_git = __esm(() => {
|
|
2349
2369
|
init_paths();
|
|
2350
2370
|
init_errors();
|
|
@@ -2482,6 +2502,46 @@ var init_skills_root = __esm(() => {
|
|
|
2482
2502
|
SKILLS_MARKER = join7("packs", "runcastle");
|
|
2483
2503
|
});
|
|
2484
2504
|
|
|
2505
|
+
// src/services/agent-stream.ts
|
|
2506
|
+
function beginTranscript(ticketId) {
|
|
2507
|
+
transcripts.set(ticketId, { live: true, chunks: [], nextIndex: 0, bytes: 0 });
|
|
2508
|
+
}
|
|
2509
|
+
function appendTranscript(ticketId, chunk) {
|
|
2510
|
+
const t = transcripts.get(ticketId);
|
|
2511
|
+
if (!t)
|
|
2512
|
+
return;
|
|
2513
|
+
const full = { ...chunk, i: t.nextIndex, ts: chunk.ts ?? Date.now() };
|
|
2514
|
+
t.nextIndex += 1;
|
|
2515
|
+
t.chunks.push(full);
|
|
2516
|
+
t.bytes += full.text.length;
|
|
2517
|
+
while (t.chunks.length > MAX_CHUNKS || t.bytes > MAX_BYTES && t.chunks.length > 1) {
|
|
2518
|
+
const dropped = t.chunks.shift();
|
|
2519
|
+
if (!dropped)
|
|
2520
|
+
break;
|
|
2521
|
+
t.bytes -= dropped.text.length;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
function endTranscript(ticketId) {
|
|
2525
|
+
const t = transcripts.get(ticketId);
|
|
2526
|
+
if (t)
|
|
2527
|
+
t.live = false;
|
|
2528
|
+
}
|
|
2529
|
+
function readTranscript(ticketId, after = -1) {
|
|
2530
|
+
const t = transcripts.get(ticketId);
|
|
2531
|
+
if (!t)
|
|
2532
|
+
return { live: false, chunks: [], firstIndex: 0, nextIndex: 0 };
|
|
2533
|
+
return {
|
|
2534
|
+
live: t.live,
|
|
2535
|
+
chunks: after < 0 ? [...t.chunks] : t.chunks.filter((c) => c.i > after),
|
|
2536
|
+
firstIndex: t.chunks[0]?.i ?? t.nextIndex,
|
|
2537
|
+
nextIndex: t.nextIndex
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
var MAX_CHUNKS = 4000, MAX_BYTES = 1500000, transcripts;
|
|
2541
|
+
var init_agent_stream = __esm(() => {
|
|
2542
|
+
transcripts = new Map;
|
|
2543
|
+
});
|
|
2544
|
+
|
|
2485
2545
|
// src/workflows/ticket-burner.ts
|
|
2486
2546
|
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
|
|
2487
2547
|
import { dirname as dirname3, join as join8 } from "path";
|
|
@@ -2594,6 +2654,64 @@ function parseEnvFile(content) {
|
|
|
2594
2654
|
}
|
|
2595
2655
|
return out;
|
|
2596
2656
|
}
|
|
2657
|
+
function detectPackageManager(tc) {
|
|
2658
|
+
const pinned = tc.packageManagerField?.split("@")[0];
|
|
2659
|
+
if (pinned && PACKAGE_MANAGERS.includes(pinned)) {
|
|
2660
|
+
return pinned;
|
|
2661
|
+
}
|
|
2662
|
+
if (tc.lockfiles.bun)
|
|
2663
|
+
return "bun";
|
|
2664
|
+
if (tc.lockfiles.pnpm)
|
|
2665
|
+
return "pnpm";
|
|
2666
|
+
if (tc.lockfiles.yarn)
|
|
2667
|
+
return "yarn";
|
|
2668
|
+
if (tc.lockfiles.npm)
|
|
2669
|
+
return "npm";
|
|
2670
|
+
return tc.hasPackageJson ? "npm" : undefined;
|
|
2671
|
+
}
|
|
2672
|
+
function resolveSetupCommand(tc, override) {
|
|
2673
|
+
const trimmed = override?.trim();
|
|
2674
|
+
if (trimmed)
|
|
2675
|
+
return trimmed;
|
|
2676
|
+
const pm = detectPackageManager(tc);
|
|
2677
|
+
if (!pm)
|
|
2678
|
+
return;
|
|
2679
|
+
switch (pm) {
|
|
2680
|
+
case "bun":
|
|
2681
|
+
return tc.lockfiles.bun ? "bun install --frozen-lockfile" : "bun install";
|
|
2682
|
+
case "pnpm":
|
|
2683
|
+
return tc.lockfiles.pnpm ? "corepack pnpm install --frozen-lockfile" : "corepack pnpm install";
|
|
2684
|
+
case "yarn":
|
|
2685
|
+
return tc.lockfiles.yarn ? "corepack yarn install --frozen-lockfile" : "corepack yarn install";
|
|
2686
|
+
case "npm":
|
|
2687
|
+
return tc.lockfiles.npm ? "npm ci" : "npm install";
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
function cacheMountFor(pm, hostPath) {
|
|
2691
|
+
return { hostPath, sandboxPath: PM_CACHE_SANDBOX_PATHS[pm] };
|
|
2692
|
+
}
|
|
2693
|
+
function readRepoToolchain(repoPath) {
|
|
2694
|
+
const pkgPath = join8(repoPath, "package.json");
|
|
2695
|
+
const hasPackageJson = existsSync10(pkgPath);
|
|
2696
|
+
let packageManagerField;
|
|
2697
|
+
if (hasPackageJson) {
|
|
2698
|
+
try {
|
|
2699
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
2700
|
+
if (typeof pkg.packageManager === "string")
|
|
2701
|
+
packageManagerField = pkg.packageManager;
|
|
2702
|
+
} catch {}
|
|
2703
|
+
}
|
|
2704
|
+
return {
|
|
2705
|
+
hasPackageJson,
|
|
2706
|
+
...packageManagerField !== undefined ? { packageManagerField } : {},
|
|
2707
|
+
lockfiles: {
|
|
2708
|
+
bun: existsSync10(join8(repoPath, "bun.lock")) || existsSync10(join8(repoPath, "bun.lockb")),
|
|
2709
|
+
pnpm: existsSync10(join8(repoPath, "pnpm-lock.yaml")),
|
|
2710
|
+
yarn: existsSync10(join8(repoPath, "yarn.lock")),
|
|
2711
|
+
npm: existsSync10(join8(repoPath, "package-lock.json"))
|
|
2712
|
+
}
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2597
2715
|
function createStreamThrottle(emit2, opts = {}) {
|
|
2598
2716
|
const intervalMs = opts.intervalMs ?? 2000;
|
|
2599
2717
|
const maxChars = opts.maxChars ?? 500;
|
|
@@ -2652,10 +2770,11 @@ function createSerialQueue() {
|
|
|
2652
2770
|
return next;
|
|
2653
2771
|
};
|
|
2654
2772
|
}
|
|
2655
|
-
function
|
|
2656
|
-
const
|
|
2657
|
-
`);
|
|
2658
|
-
|
|
2773
|
+
function errorHeadline(s) {
|
|
2774
|
+
const lines = s.split(`
|
|
2775
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
2776
|
+
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
2777
|
+
return causes.at(-1) ?? lines[0] ?? "";
|
|
2659
2778
|
}
|
|
2660
2779
|
async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
2661
2780
|
const width = Math.max(1, Math.floor(concurrency));
|
|
@@ -2710,7 +2829,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
|
2710
2829
|
failTicket(seq, outcome.error, outcome.event);
|
|
2711
2830
|
ctx.emitEvent({
|
|
2712
2831
|
type: "ticket.failed",
|
|
2713
|
-
message: `ticket ${t.seq} failed: ${
|
|
2832
|
+
message: `ticket ${t.seq} failed: ${errorHeadline(outcome.error)}`,
|
|
2714
2833
|
ticketId: t.id,
|
|
2715
2834
|
data: { error: outcome.error }
|
|
2716
2835
|
});
|
|
@@ -2844,8 +2963,11 @@ function buildBurnAgent(config2, token, model) {
|
|
|
2844
2963
|
}
|
|
2845
2964
|
return agent;
|
|
2846
2965
|
}
|
|
2847
|
-
function selectSandbox(config2) {
|
|
2848
|
-
const imageOpts = {
|
|
2966
|
+
function selectSandbox(config2, mounts = []) {
|
|
2967
|
+
const imageOpts = {
|
|
2968
|
+
imageName: resolveSandboxImage(config2),
|
|
2969
|
+
...mounts.length > 0 ? { mounts } : {}
|
|
2970
|
+
};
|
|
2849
2971
|
switch (config2.sandbox) {
|
|
2850
2972
|
case "docker":
|
|
2851
2973
|
return docker(imageOpts);
|
|
@@ -2865,24 +2987,62 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
|
|
|
2865
2987
|
DOCS_DIGEST: readDocsDigestFromDisk(project.id, feature.slug),
|
|
2866
2988
|
COMMIT_CONVENTION: `ticket(${ticket.seq}): <summary>`
|
|
2867
2989
|
});
|
|
2990
|
+
const toolchain = readRepoToolchain(project.repoPath);
|
|
2991
|
+
const pm = detectPackageManager(toolchain);
|
|
2992
|
+
const setupCommand = resolveSetupCommand(toolchain, config2.setupCommand);
|
|
2993
|
+
const mounts = [];
|
|
2994
|
+
if (config2.sandbox !== "noSandbox" && pm) {
|
|
2995
|
+
const hostCache = burnCacheDir(pm);
|
|
2996
|
+
mkdirSync4(hostCache, { recursive: true });
|
|
2997
|
+
mounts.push(cacheMountFor(pm, hostCache));
|
|
2998
|
+
}
|
|
2999
|
+
if (setupCommand) {
|
|
3000
|
+
ctx.emitEvent({
|
|
3001
|
+
type: "burn.setup",
|
|
3002
|
+
message: `installing deps before agent start: ${setupCommand}`,
|
|
3003
|
+
ticketId: ticket.id
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
2868
3006
|
mkdirSync4(logsDir(), { recursive: true });
|
|
2869
3007
|
const logFilePath = join8(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
|
|
2870
3008
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
3009
|
+
beginTranscript(ticket.id);
|
|
3010
|
+
const onStreamEvent = (event) => {
|
|
3011
|
+
throttle.onEvent(event);
|
|
3012
|
+
if (event.type === "text") {
|
|
3013
|
+
appendTranscript(ticket.id, { kind: "text", text: event.message });
|
|
3014
|
+
} else if (event.type === "toolCall") {
|
|
3015
|
+
appendTranscript(ticket.id, {
|
|
3016
|
+
kind: "tool",
|
|
3017
|
+
text: event.formattedArgs ?? "",
|
|
3018
|
+
name: event.name
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
};
|
|
2871
3022
|
const runOptions = {
|
|
2872
3023
|
agent: buildBurnAgent(config2, token, model),
|
|
2873
|
-
sandbox: selectSandbox(config2),
|
|
3024
|
+
sandbox: selectSandbox(config2, mounts),
|
|
2874
3025
|
cwd: project.repoPath,
|
|
2875
3026
|
prompt,
|
|
2876
3027
|
branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
|
|
2877
3028
|
signal: ctx.signal,
|
|
2878
3029
|
name: `ticket-${ticket.seq}`,
|
|
2879
|
-
|
|
3030
|
+
maxIterations: config2.burnMaxIterations,
|
|
3031
|
+
...setupCommand ? {
|
|
3032
|
+
hooks: {
|
|
3033
|
+
sandbox: {
|
|
3034
|
+
onSandboxReady: [{ command: setupCommand, timeoutMs: SETUP_HOOK_TIMEOUT_MS }]
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
} : {},
|
|
3038
|
+
logging: { type: "file", path: logFilePath, onAgentStreamEvent: onStreamEvent }
|
|
2880
3039
|
};
|
|
2881
3040
|
let result;
|
|
2882
3041
|
try {
|
|
2883
3042
|
result = await run(runOptions);
|
|
2884
3043
|
} catch (err) {
|
|
2885
3044
|
throttle.flush();
|
|
3045
|
+
endTranscript(ticket.id);
|
|
2886
3046
|
if (ctx.signal.aborted)
|
|
2887
3047
|
throw err;
|
|
2888
3048
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2899,6 +3059,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
|
|
|
2899
3059
|
return { status: "failed", error: msg };
|
|
2900
3060
|
}
|
|
2901
3061
|
throttle.flush();
|
|
3062
|
+
endTranscript(ticket.id);
|
|
2902
3063
|
const blocked = readBlockedFile([result.preservedWorktreePath, project.repoPath]);
|
|
2903
3064
|
const outcome = interpretRunResult(result, blocked);
|
|
2904
3065
|
if (outcome.status !== "done")
|
|
@@ -2918,7 +3079,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
|
|
|
2918
3079
|
}
|
|
2919
3080
|
return {
|
|
2920
3081
|
status: "failed",
|
|
2921
|
-
error: `ticket ${ticket.seq} committed to ${tempBranch} but landing on ${feature.branch} failed: ${
|
|
3082
|
+
error: `ticket ${ticket.seq} committed to ${tempBranch} but landing on ${feature.branch} failed: ${errorHeadline(detail)} \u2014 the branch is preserved for manual recovery`
|
|
2922
3083
|
};
|
|
2923
3084
|
}
|
|
2924
3085
|
return outcome;
|
|
@@ -2947,14 +3108,23 @@ function readTokenFromEnvFile(path) {
|
|
|
2947
3108
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
2948
3109
|
return token && token.length > 0 ? token : undefined;
|
|
2949
3110
|
}
|
|
2950
|
-
var AUTH_MISSING_EVENT = "auth.missing", AUTH_MISSING_MESSAGE = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PLACEHOLDERS, ticketBurner;
|
|
3111
|
+
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;
|
|
2951
3112
|
var init_ticket_burner = __esm(() => {
|
|
2952
3113
|
init_src();
|
|
2953
3114
|
init_config_load();
|
|
2954
3115
|
init_paths();
|
|
2955
3116
|
init_skills_root();
|
|
3117
|
+
init_agent_stream();
|
|
2956
3118
|
init_git();
|
|
2957
3119
|
PLACEHOLDERS = ["TICKET_JSON", "FEATURE_BRIEF", "DOCS_DIGEST", "COMMIT_CONVENTION"];
|
|
3120
|
+
PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
3121
|
+
PM_CACHE_SANDBOX_PATHS = {
|
|
3122
|
+
bun: "~/.bun/install/cache",
|
|
3123
|
+
pnpm: "~/.local/share/pnpm/store",
|
|
3124
|
+
yarn: "~/.cache/yarn",
|
|
3125
|
+
npm: "~/.npm"
|
|
3126
|
+
};
|
|
3127
|
+
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
2958
3128
|
ticketBurner = {
|
|
2959
3129
|
id: "ticket-burner",
|
|
2960
3130
|
run: (ctx) => burnRun(ctx, resolveBurnDeps(ctx))
|
|
@@ -3003,15 +3173,16 @@ async function researchRun(ctx, deps) {
|
|
|
3003
3173
|
}
|
|
3004
3174
|
ctx.emitEvent({
|
|
3005
3175
|
type: "research.failed",
|
|
3006
|
-
message: `waypoint ${waypoint.seq} research failed: ${
|
|
3176
|
+
message: `waypoint ${waypoint.seq} research failed: ${errorHeadline2(outcome.error)}`,
|
|
3007
3177
|
data: { waypointId: waypoint.id, error: outcome.error }
|
|
3008
3178
|
});
|
|
3009
3179
|
return { status: "failed", summary: outcome.error };
|
|
3010
3180
|
}
|
|
3011
|
-
function
|
|
3012
|
-
const
|
|
3013
|
-
`);
|
|
3014
|
-
|
|
3181
|
+
function errorHeadline2(s) {
|
|
3182
|
+
const lines = s.split(`
|
|
3183
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
3184
|
+
const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
|
|
3185
|
+
return causes.at(-1) ?? lines[0] ?? "";
|
|
3015
3186
|
}
|
|
3016
3187
|
function createResearchStreamThrottle(emit2, opts = {}) {
|
|
3017
3188
|
return createStreamThrottle((e) => emit2({ ...e, type: e.type.replace(/^burn\./, "research.") }), opts);
|
|
@@ -3088,7 +3259,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
3088
3259
|
const detail = merge.error ?? "merge failed";
|
|
3089
3260
|
ctx.emitEvent({
|
|
3090
3261
|
type: merge.conflict ? "merge.conflict.needs-human" : "research.merge_failed",
|
|
3091
|
-
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}: ${
|
|
3262
|
+
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`,
|
|
3092
3263
|
data: { tempBranch, featureBranch: feature.branch, conflict: merge.conflict ?? false, error: detail }
|
|
3093
3264
|
});
|
|
3094
3265
|
return {
|
|
@@ -4832,11 +5003,13 @@ var init_project = __esm(() => {
|
|
|
4832
5003
|
import * as z8 from "zod";
|
|
4833
5004
|
var runRouter;
|
|
4834
5005
|
var init_run = __esm(() => {
|
|
5006
|
+
init_agent_stream();
|
|
4835
5007
|
init_repo();
|
|
4836
5008
|
init_runner();
|
|
4837
5009
|
init_context();
|
|
4838
5010
|
runRouter = router({
|
|
4839
5011
|
get: publicProcedure.input(z8.object({ runId: z8.string() })).query(({ ctx, input }) => getRunRow(ctx, input.runId)),
|
|
5012
|
+
agentTranscript: publicProcedure.input(z8.object({ ticketId: z8.string(), after: z8.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
|
|
4840
5013
|
cancel: publicProcedure.input(z8.object({ runId: z8.string() })).mutation(({ input }) => {
|
|
4841
5014
|
cancelRun(input.runId);
|
|
4842
5015
|
return { ok: true };
|
|
@@ -5072,6 +5245,22 @@ var init_settings = __esm(() => {
|
|
|
5072
5245
|
valueSchema: z9.number().int().min(1).max(8),
|
|
5073
5246
|
parseEnv: (raw) => Number(raw)
|
|
5074
5247
|
},
|
|
5248
|
+
{
|
|
5249
|
+
key: "burnMaxIterations",
|
|
5250
|
+
configKey: "burnMaxIterations",
|
|
5251
|
+
envVar: "RUNCASTLE_BURN_MAX_ITERATIONS",
|
|
5252
|
+
restartRequired: false,
|
|
5253
|
+
valueSchema: z9.number().int().min(1).max(10),
|
|
5254
|
+
parseEnv: (raw) => Number(raw)
|
|
5255
|
+
},
|
|
5256
|
+
{
|
|
5257
|
+
key: "setupCommand",
|
|
5258
|
+
configKey: "setupCommand",
|
|
5259
|
+
envVar: "RUNCASTLE_SETUP_COMMAND",
|
|
5260
|
+
restartRequired: false,
|
|
5261
|
+
valueSchema: z9.string().min(1),
|
|
5262
|
+
parseEnv: idEnv
|
|
5263
|
+
},
|
|
5075
5264
|
{
|
|
5076
5265
|
key: "mainBranch",
|
|
5077
5266
|
configKey: "mainBranch",
|
package/package.json
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
You are a single agent in a sandbox on branch `feature/<slug>`. You have **one ticket**. There is **no human to ask** — no follow-up questions are possible. Everything you need is in this prompt and in the repo. Work carefully, commit only green work, and stop when the ticket is done.
|
|
6
6
|
|
|
7
|
+
## How you run
|
|
8
|
+
|
|
9
|
+
You run **non-interactively** (`claude --print`), for up to a few fresh iterations against the same worktree:
|
|
10
|
+
|
|
11
|
+
- **Ending your turn ends your process.** There are no background-task completion notifications in print mode — a "the notification will re-invoke me" plan never fires. Never end your turn to wait on a background command; run long commands (dependency installs, full test suites) in the foreground with a generous timeout, or poll a backgrounded command to completion *within* the same turn.
|
|
12
|
+
- **You may not be the first iteration.** A previous iteration may have left commits or uncommitted work on this branch. Check `git status` and `git log` before starting; continue the work, don't redo it.
|
|
13
|
+
- **Signal completion.** When the ticket is fully done — every acceptance criterion verified, self-review finished, all work committed — print exactly `<promise>COMPLETE</promise>` as the last line of your final message. Do the same after writing `BLOCKED.md` (see hard rules). Without the signal, the harness assumes you were cut off and spends another iteration.
|
|
14
|
+
|
|
7
15
|
## The ticket
|
|
8
16
|
|
|
9
17
|
```json
|
|
@@ -39,4 +47,4 @@ You are a single agent in a sandbox on branch `feature/<slug>`. You have **one t
|
|
|
39
47
|
|
|
40
48
|
- **Never expand scope beyond the ticket.** If you notice adjacent work, worthwhile refactors, or another ticket's territory, leave it. Note it in your final commit body if it matters; do not do it.
|
|
41
49
|
- **No questions, no guessing into the void.** Resolve ambiguity from the ticket context and the code. If two readings both satisfy the acceptance criteria, take the smaller one.
|
|
42
|
-
- **If genuinely blocked** (a dependency ticket's output is not there, the environment fails, or a requirement truly cannot be resolved from context + code): **commit nothing for the blocked part**, write `BLOCKED.md` at the repo root stating precisely what blocked you and what is needed to unblock, and stop. Green, complete parts may still be committed; the blocked part must not be. The orchestrator reads your exit state from the commits — a part with no commits is read as not done.
|
|
50
|
+
- **If genuinely blocked** (a dependency ticket's output is not there, the environment fails, or a requirement truly cannot be resolved from context + code): **commit nothing for the blocked part**, write `BLOCKED.md` at the repo root stating precisely what blocked you and what is needed to unblock, print `<promise>COMPLETE</promise>`, and stop. Green, complete parts may still be committed; the blocked part must not be. The orchestrator reads your exit state from the commits — a part with no commits is read as not done.
|