runcastle 1.0.8 → 1.0.10
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 +246 -9
- package/index.js +246 -9
- package/package.json +1 -1
- package/skills/burner/implement-ticket.md +15 -1
- package/web/assets/{index-Y3ehCYIg.js → index-B0l9lJT5.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,10 @@ 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(),
|
|
109
|
+
burnWorkspace: z.enum(["auto", "mounted", "isolated"]).default("auto")
|
|
103
110
|
}));
|
|
104
111
|
});
|
|
105
112
|
|
|
@@ -135,6 +142,13 @@ function loadConfig(env = process.env) {
|
|
|
135
142
|
if (env.RUNCASTLE_BURN_CONCURRENCY) {
|
|
136
143
|
overrides.burnConcurrency = Number(env.RUNCASTLE_BURN_CONCURRENCY);
|
|
137
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;
|
|
150
|
+
if (env.RUNCASTLE_BURN_WORKSPACE)
|
|
151
|
+
overrides.burnWorkspace = env.RUNCASTLE_BURN_WORKSPACE;
|
|
138
152
|
const base = typeof fileConfig === "object" && fileConfig !== null ? fileConfig : {};
|
|
139
153
|
return RuncastleConfig.parse({ ...base, ...overrides });
|
|
140
154
|
}
|
|
@@ -2491,6 +2505,46 @@ var init_skills_root = __esm(() => {
|
|
|
2491
2505
|
SKILLS_MARKER = join7("packs", "runcastle");
|
|
2492
2506
|
});
|
|
2493
2507
|
|
|
2508
|
+
// src/services/agent-stream.ts
|
|
2509
|
+
function beginTranscript(ticketId) {
|
|
2510
|
+
transcripts.set(ticketId, { live: true, chunks: [], nextIndex: 0, bytes: 0 });
|
|
2511
|
+
}
|
|
2512
|
+
function appendTranscript(ticketId, chunk) {
|
|
2513
|
+
const t = transcripts.get(ticketId);
|
|
2514
|
+
if (!t)
|
|
2515
|
+
return;
|
|
2516
|
+
const full = { ...chunk, i: t.nextIndex, ts: chunk.ts ?? Date.now() };
|
|
2517
|
+
t.nextIndex += 1;
|
|
2518
|
+
t.chunks.push(full);
|
|
2519
|
+
t.bytes += full.text.length;
|
|
2520
|
+
while (t.chunks.length > MAX_CHUNKS || t.bytes > MAX_BYTES && t.chunks.length > 1) {
|
|
2521
|
+
const dropped = t.chunks.shift();
|
|
2522
|
+
if (!dropped)
|
|
2523
|
+
break;
|
|
2524
|
+
t.bytes -= dropped.text.length;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
function endTranscript(ticketId) {
|
|
2528
|
+
const t = transcripts.get(ticketId);
|
|
2529
|
+
if (t)
|
|
2530
|
+
t.live = false;
|
|
2531
|
+
}
|
|
2532
|
+
function readTranscript(ticketId, after = -1) {
|
|
2533
|
+
const t = transcripts.get(ticketId);
|
|
2534
|
+
if (!t)
|
|
2535
|
+
return { live: false, chunks: [], firstIndex: 0, nextIndex: 0 };
|
|
2536
|
+
return {
|
|
2537
|
+
live: t.live,
|
|
2538
|
+
chunks: after < 0 ? [...t.chunks] : t.chunks.filter((c) => c.i > after),
|
|
2539
|
+
firstIndex: t.chunks[0]?.i ?? t.nextIndex,
|
|
2540
|
+
nextIndex: t.nextIndex
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
var MAX_CHUNKS = 4000, MAX_BYTES = 1500000, transcripts;
|
|
2544
|
+
var init_agent_stream = __esm(() => {
|
|
2545
|
+
transcripts = new Map;
|
|
2546
|
+
});
|
|
2547
|
+
|
|
2494
2548
|
// src/workflows/ticket-burner.ts
|
|
2495
2549
|
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
|
|
2496
2550
|
import { dirname as dirname3, join as join8 } from "path";
|
|
@@ -2603,6 +2657,97 @@ function parseEnvFile(content) {
|
|
|
2603
2657
|
}
|
|
2604
2658
|
return out;
|
|
2605
2659
|
}
|
|
2660
|
+
function detectPackageManager(tc) {
|
|
2661
|
+
const pinned = tc.packageManagerField?.split("@")[0];
|
|
2662
|
+
if (pinned && PACKAGE_MANAGERS.includes(pinned)) {
|
|
2663
|
+
return pinned;
|
|
2664
|
+
}
|
|
2665
|
+
if (tc.lockfiles.bun)
|
|
2666
|
+
return "bun";
|
|
2667
|
+
if (tc.lockfiles.pnpm)
|
|
2668
|
+
return "pnpm";
|
|
2669
|
+
if (tc.lockfiles.yarn)
|
|
2670
|
+
return "yarn";
|
|
2671
|
+
if (tc.lockfiles.npm)
|
|
2672
|
+
return "npm";
|
|
2673
|
+
return tc.hasPackageJson ? "npm" : undefined;
|
|
2674
|
+
}
|
|
2675
|
+
function resolveSetupCommand(tc, override) {
|
|
2676
|
+
const trimmed = override?.trim();
|
|
2677
|
+
if (trimmed)
|
|
2678
|
+
return trimmed;
|
|
2679
|
+
const pm = detectPackageManager(tc);
|
|
2680
|
+
if (!pm)
|
|
2681
|
+
return;
|
|
2682
|
+
switch (pm) {
|
|
2683
|
+
case "bun":
|
|
2684
|
+
return tc.lockfiles.bun ? "bun install --frozen-lockfile" : "bun install";
|
|
2685
|
+
case "pnpm":
|
|
2686
|
+
return tc.lockfiles.pnpm ? "corepack pnpm install --frozen-lockfile" : "corepack pnpm install";
|
|
2687
|
+
case "yarn":
|
|
2688
|
+
return tc.lockfiles.yarn ? "corepack yarn install --frozen-lockfile" : "corepack yarn install";
|
|
2689
|
+
case "npm":
|
|
2690
|
+
return tc.lockfiles.npm ? "npm ci" : "npm install";
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
function cacheMountFor(pm, hostPath) {
|
|
2694
|
+
const sandboxPath = PM_CACHE_SANDBOX_PATHS[pm];
|
|
2695
|
+
return sandboxPath ? { hostPath, sandboxPath } : undefined;
|
|
2696
|
+
}
|
|
2697
|
+
function resolveBurnWorkspaceMode(config2, platform = process.platform) {
|
|
2698
|
+
if (config2.sandbox === "noSandbox")
|
|
2699
|
+
return "mounted";
|
|
2700
|
+
if (config2.burnWorkspace === "auto")
|
|
2701
|
+
return platform === "linux" ? "mounted" : "isolated";
|
|
2702
|
+
return config2.burnWorkspace;
|
|
2703
|
+
}
|
|
2704
|
+
function buildIsolatedSetupCommand(tempBranch, setupCommand) {
|
|
2705
|
+
const hookFile = `${ISOLATED_REPO_PATH}/.git/hooks/post-commit`;
|
|
2706
|
+
const parts = [
|
|
2707
|
+
`git -C ${SANDBOX_WORKSPACE_PATH} config receive.denyCurrentBranch updateInstead`,
|
|
2708
|
+
`git clone ${SANDBOX_WORKSPACE_PATH} ${ISOLATED_REPO_PATH}`,
|
|
2709
|
+
`printf '#!/bin/sh\\nexec git push --quiet origin HEAD:%s\\n' '${tempBranch}' > ${hookFile}`,
|
|
2710
|
+
`chmod +x ${hookFile}`
|
|
2711
|
+
];
|
|
2712
|
+
if (setupCommand)
|
|
2713
|
+
parts.push(`cd ${ISOLATED_REPO_PATH} && ${setupCommand}`);
|
|
2714
|
+
return parts.join(" && ");
|
|
2715
|
+
}
|
|
2716
|
+
function buildWorkspaceNotes(mode) {
|
|
2717
|
+
if (mode === "mounted") {
|
|
2718
|
+
return "Work in the current directory \u2014 it is the repo checkout on your branch.";
|
|
2719
|
+
}
|
|
2720
|
+
return [
|
|
2721
|
+
`Your working repository is \`${ISOLATED_REPO_PATH}\` \u2014 a clone on the container's fast native filesystem, with dependencies already installed. Do ALL work there: \`cd ${ISOLATED_REPO_PATH}\` first; every file you read, edit, test, and commit lives under it.`,
|
|
2722
|
+
"",
|
|
2723
|
+
`The directory you start in (\`${SANDBOX_WORKSPACE_PATH}\`) is a slow mounted mirror used only to collect your commits \u2014 never edit files, install, or run tests there. Your commits sync back automatically (a post-commit hook pushes them); just commit as normal.`,
|
|
2724
|
+
"",
|
|
2725
|
+
`If you are blocked and write \`BLOCKED.md\`, write it at \`${ISOLATED_REPO_PATH}/BLOCKED.md\` AND copy it to \`${SANDBOX_WORKSPACE_PATH}/BLOCKED.md\` so the orchestrator can see it.`
|
|
2726
|
+
].join(`
|
|
2727
|
+
`);
|
|
2728
|
+
}
|
|
2729
|
+
function readRepoToolchain(repoPath) {
|
|
2730
|
+
const pkgPath = join8(repoPath, "package.json");
|
|
2731
|
+
const hasPackageJson = existsSync10(pkgPath);
|
|
2732
|
+
let packageManagerField;
|
|
2733
|
+
if (hasPackageJson) {
|
|
2734
|
+
try {
|
|
2735
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
2736
|
+
if (typeof pkg.packageManager === "string")
|
|
2737
|
+
packageManagerField = pkg.packageManager;
|
|
2738
|
+
} catch {}
|
|
2739
|
+
}
|
|
2740
|
+
return {
|
|
2741
|
+
hasPackageJson,
|
|
2742
|
+
...packageManagerField !== undefined ? { packageManagerField } : {},
|
|
2743
|
+
lockfiles: {
|
|
2744
|
+
bun: existsSync10(join8(repoPath, "bun.lock")) || existsSync10(join8(repoPath, "bun.lockb")),
|
|
2745
|
+
pnpm: existsSync10(join8(repoPath, "pnpm-lock.yaml")),
|
|
2746
|
+
yarn: existsSync10(join8(repoPath, "yarn.lock")),
|
|
2747
|
+
npm: existsSync10(join8(repoPath, "package-lock.json"))
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2606
2751
|
function createStreamThrottle(emit2, opts = {}) {
|
|
2607
2752
|
const intervalMs = opts.intervalMs ?? 2000;
|
|
2608
2753
|
const maxChars = opts.maxChars ?? 500;
|
|
@@ -2854,8 +2999,11 @@ function buildBurnAgent(config2, token, model) {
|
|
|
2854
2999
|
}
|
|
2855
3000
|
return agent;
|
|
2856
3001
|
}
|
|
2857
|
-
function selectSandbox(config2) {
|
|
2858
|
-
const imageOpts = {
|
|
3002
|
+
function selectSandbox(config2, mounts = []) {
|
|
3003
|
+
const imageOpts = {
|
|
3004
|
+
imageName: resolveSandboxImage(config2),
|
|
3005
|
+
...mounts.length > 0 ? { mounts } : {}
|
|
3006
|
+
};
|
|
2859
3007
|
switch (config2.sandbox) {
|
|
2860
3008
|
case "docker":
|
|
2861
3009
|
return docker(imageOpts);
|
|
@@ -2868,31 +3016,74 @@ function selectSandbox(config2) {
|
|
|
2868
3016
|
async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
|
|
2869
3017
|
const { project, feature } = ctx;
|
|
2870
3018
|
const tempBranch = ticketBranchName(feature.slug, ticket.seq, newId("b").slice(2, 10));
|
|
3019
|
+
const workspaceMode = resolveBurnWorkspaceMode(config2);
|
|
2871
3020
|
const template = readFileSync4(burnerTemplatePath(), "utf8");
|
|
2872
3021
|
const prompt = renderTicketPrompt(template, {
|
|
2873
3022
|
TICKET_JSON: buildTicketJson(ticket),
|
|
2874
3023
|
FEATURE_BRIEF: buildFeatureBrief(feature),
|
|
2875
3024
|
DOCS_DIGEST: readDocsDigestFromDisk(project.id, feature.slug),
|
|
2876
|
-
COMMIT_CONVENTION: `ticket(${ticket.seq}): <summary
|
|
3025
|
+
COMMIT_CONVENTION: `ticket(${ticket.seq}): <summary>`,
|
|
3026
|
+
WORKSPACE_NOTES: buildWorkspaceNotes(workspaceMode)
|
|
2877
3027
|
});
|
|
3028
|
+
const toolchain = readRepoToolchain(project.repoPath);
|
|
3029
|
+
const pm = detectPackageManager(toolchain);
|
|
3030
|
+
const setupCommand = resolveSetupCommand(toolchain, config2.setupCommand);
|
|
3031
|
+
const mounts = [];
|
|
3032
|
+
if (config2.sandbox !== "noSandbox" && pm) {
|
|
3033
|
+
const mount = cacheMountFor(pm, burnCacheDir(pm));
|
|
3034
|
+
if (mount) {
|
|
3035
|
+
mkdirSync4(mount.hostPath, { recursive: true });
|
|
3036
|
+
mounts.push(mount);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
const hookCommand = workspaceMode === "isolated" ? buildIsolatedSetupCommand(tempBranch, setupCommand) : setupCommand;
|
|
3040
|
+
if (hookCommand) {
|
|
3041
|
+
ctx.emitEvent({
|
|
3042
|
+
type: "burn.setup",
|
|
3043
|
+
message: workspaceMode === "isolated" ? `preparing isolated workspace (native-FS clone)${setupCommand ? ` + deps install: ${setupCommand}` : ""}` : `installing deps before agent start: ${setupCommand}`,
|
|
3044
|
+
ticketId: ticket.id
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
2878
3047
|
mkdirSync4(logsDir(), { recursive: true });
|
|
2879
3048
|
const logFilePath = join8(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
|
|
2880
3049
|
const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
|
|
3050
|
+
beginTranscript(ticket.id);
|
|
3051
|
+
const onStreamEvent = (event) => {
|
|
3052
|
+
throttle.onEvent(event);
|
|
3053
|
+
if (event.type === "text") {
|
|
3054
|
+
appendTranscript(ticket.id, { kind: "text", text: event.message });
|
|
3055
|
+
} else if (event.type === "toolCall") {
|
|
3056
|
+
appendTranscript(ticket.id, {
|
|
3057
|
+
kind: "tool",
|
|
3058
|
+
text: event.formattedArgs ?? "",
|
|
3059
|
+
name: event.name
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
};
|
|
2881
3063
|
const runOptions = {
|
|
2882
3064
|
agent: buildBurnAgent(config2, token, model),
|
|
2883
|
-
sandbox: selectSandbox(config2),
|
|
3065
|
+
sandbox: selectSandbox(config2, mounts),
|
|
2884
3066
|
cwd: project.repoPath,
|
|
2885
3067
|
prompt,
|
|
2886
3068
|
branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
|
|
2887
3069
|
signal: ctx.signal,
|
|
2888
3070
|
name: `ticket-${ticket.seq}`,
|
|
2889
|
-
|
|
3071
|
+
maxIterations: config2.burnMaxIterations,
|
|
3072
|
+
...hookCommand ? {
|
|
3073
|
+
hooks: {
|
|
3074
|
+
sandbox: {
|
|
3075
|
+
onSandboxReady: [{ command: hookCommand, timeoutMs: SETUP_HOOK_TIMEOUT_MS }]
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
} : {},
|
|
3079
|
+
logging: { type: "file", path: logFilePath, onAgentStreamEvent: onStreamEvent }
|
|
2890
3080
|
};
|
|
2891
3081
|
let result;
|
|
2892
3082
|
try {
|
|
2893
3083
|
result = await run(runOptions);
|
|
2894
3084
|
} catch (err) {
|
|
2895
3085
|
throttle.flush();
|
|
3086
|
+
endTranscript(ticket.id);
|
|
2896
3087
|
if (ctx.signal.aborted)
|
|
2897
3088
|
throw err;
|
|
2898
3089
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2909,6 +3100,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
|
|
|
2909
3100
|
return { status: "failed", error: msg };
|
|
2910
3101
|
}
|
|
2911
3102
|
throttle.flush();
|
|
3103
|
+
endTranscript(ticket.id);
|
|
2912
3104
|
const blocked = readBlockedFile([result.preservedWorktreePath, project.repoPath]);
|
|
2913
3105
|
const outcome = interpretRunResult(result, blocked);
|
|
2914
3106
|
if (outcome.status !== "done")
|
|
@@ -2957,14 +3149,28 @@ function readTokenFromEnvFile(path) {
|
|
|
2957
3149
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
2958
3150
|
return token && token.length > 0 ? token : undefined;
|
|
2959
3151
|
}
|
|
2960
|
-
var AUTH_MISSING_EVENT = "auth.missing", AUTH_MISSING_MESSAGE = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PLACEHOLDERS, ticketBurner;
|
|
3152
|
+
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, SANDBOX_WORKSPACE_PATH = "/home/agent/workspace", ISOLATED_REPO_PATH = "/home/agent/repo", SETUP_HOOK_TIMEOUT_MS, ticketBurner;
|
|
2961
3153
|
var init_ticket_burner = __esm(() => {
|
|
2962
3154
|
init_src();
|
|
2963
3155
|
init_config_load();
|
|
2964
3156
|
init_paths();
|
|
2965
3157
|
init_skills_root();
|
|
3158
|
+
init_agent_stream();
|
|
2966
3159
|
init_git();
|
|
2967
|
-
PLACEHOLDERS = [
|
|
3160
|
+
PLACEHOLDERS = [
|
|
3161
|
+
"TICKET_JSON",
|
|
3162
|
+
"FEATURE_BRIEF",
|
|
3163
|
+
"DOCS_DIGEST",
|
|
3164
|
+
"COMMIT_CONVENTION",
|
|
3165
|
+
"WORKSPACE_NOTES"
|
|
3166
|
+
];
|
|
3167
|
+
PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
|
|
3168
|
+
PM_CACHE_SANDBOX_PATHS = {
|
|
3169
|
+
bun: "~/.bun/install/cache",
|
|
3170
|
+
yarn: "~/.cache/yarn",
|
|
3171
|
+
npm: "~/.npm"
|
|
3172
|
+
};
|
|
3173
|
+
SETUP_HOOK_TIMEOUT_MS = 15 * 60000;
|
|
2968
3174
|
ticketBurner = {
|
|
2969
3175
|
id: "ticket-burner",
|
|
2970
3176
|
run: (ctx) => burnRun(ctx, resolveBurnDeps(ctx))
|
|
@@ -4741,6 +4947,17 @@ import { eq as eq9, isNull } from "drizzle-orm";
|
|
|
4741
4947
|
function listProjects(ctx) {
|
|
4742
4948
|
return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
|
|
4743
4949
|
}
|
|
4950
|
+
function isTranslatedWindowsMount(repoPath, platform = process.platform) {
|
|
4951
|
+
return platform === "linux" && /^\/mnt\/[a-z](\/|$)/i.test(repoPath);
|
|
4952
|
+
}
|
|
4953
|
+
function warnIfTranslatedMount(ctx, projectId, repoPath) {
|
|
4954
|
+
if (!isTranslatedWindowsMount(repoPath))
|
|
4955
|
+
return;
|
|
4956
|
+
emitProject(ctx, projectId, {
|
|
4957
|
+
type: "project.slow-path",
|
|
4958
|
+
message: `${repoPath} is a Windows drive mounted into Linux (/mnt) \u2014 git and burns pay a per-file translation tax here; clone the repo into the Linux filesystem (e.g. ~/projects) for native speed`
|
|
4959
|
+
});
|
|
4960
|
+
}
|
|
4744
4961
|
async function openProject(ctx, repoPath) {
|
|
4745
4962
|
await assertRepoTolerant(ctx, repoPath);
|
|
4746
4963
|
const mainBranch = await detectMainBranchTolerant(ctx, repoPath);
|
|
@@ -4753,6 +4970,7 @@ async function openProject(ctx, repoPath) {
|
|
|
4753
4970
|
type: "project.opened",
|
|
4754
4971
|
message: `project ${existing.name} re-opened at ${repoPath} (${mainBranch})`
|
|
4755
4972
|
});
|
|
4973
|
+
warnIfTranslatedMount(ctx, existing.id, repoPath);
|
|
4756
4974
|
return reopened;
|
|
4757
4975
|
}
|
|
4758
4976
|
const row = {
|
|
@@ -4769,6 +4987,7 @@ async function openProject(ctx, repoPath) {
|
|
|
4769
4987
|
type: "project.opened",
|
|
4770
4988
|
message: `project ${name} at ${repoPath} (${mainBranch})`
|
|
4771
4989
|
});
|
|
4990
|
+
warnIfTranslatedMount(ctx, project.id, repoPath);
|
|
4772
4991
|
return project;
|
|
4773
4992
|
}
|
|
4774
4993
|
function closeProject(ctx, projectId) {
|
|
@@ -4843,11 +5062,13 @@ var init_project = __esm(() => {
|
|
|
4843
5062
|
import * as z8 from "zod";
|
|
4844
5063
|
var runRouter;
|
|
4845
5064
|
var init_run = __esm(() => {
|
|
5065
|
+
init_agent_stream();
|
|
4846
5066
|
init_repo();
|
|
4847
5067
|
init_runner();
|
|
4848
5068
|
init_context();
|
|
4849
5069
|
runRouter = router({
|
|
4850
5070
|
get: publicProcedure.input(z8.object({ runId: z8.string() })).query(({ ctx, input }) => getRunRow(ctx, input.runId)),
|
|
5071
|
+
agentTranscript: publicProcedure.input(z8.object({ ticketId: z8.string(), after: z8.number().int().optional() })).query(({ input }) => readTranscript(input.ticketId, input.after ?? -1)),
|
|
4851
5072
|
cancel: publicProcedure.input(z8.object({ runId: z8.string() })).mutation(({ input }) => {
|
|
4852
5073
|
cancelRun(input.runId);
|
|
4853
5074
|
return { ok: true };
|
|
@@ -5083,6 +5304,22 @@ var init_settings = __esm(() => {
|
|
|
5083
5304
|
valueSchema: z9.number().int().min(1).max(8),
|
|
5084
5305
|
parseEnv: (raw) => Number(raw)
|
|
5085
5306
|
},
|
|
5307
|
+
{
|
|
5308
|
+
key: "burnMaxIterations",
|
|
5309
|
+
configKey: "burnMaxIterations",
|
|
5310
|
+
envVar: "RUNCASTLE_BURN_MAX_ITERATIONS",
|
|
5311
|
+
restartRequired: false,
|
|
5312
|
+
valueSchema: z9.number().int().min(1).max(10),
|
|
5313
|
+
parseEnv: (raw) => Number(raw)
|
|
5314
|
+
},
|
|
5315
|
+
{
|
|
5316
|
+
key: "setupCommand",
|
|
5317
|
+
configKey: "setupCommand",
|
|
5318
|
+
envVar: "RUNCASTLE_SETUP_COMMAND",
|
|
5319
|
+
restartRequired: false,
|
|
5320
|
+
valueSchema: z9.string().min(1),
|
|
5321
|
+
parseEnv: idEnv
|
|
5322
|
+
},
|
|
5086
5323
|
{
|
|
5087
5324
|
key: "mainBranch",
|
|
5088
5325
|
configKey: "mainBranch",
|
package/package.json
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
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
|
+
|
|
15
|
+
## Where to work
|
|
16
|
+
|
|
17
|
+
{{WORKSPACE_NOTES}}
|
|
18
|
+
|
|
7
19
|
## The ticket
|
|
8
20
|
|
|
9
21
|
```json
|
|
@@ -30,6 +42,8 @@ You are a single agent in a sandbox on branch `feature/<slug>`. You have **one t
|
|
|
30
42
|
|
|
31
43
|
`{{COMMIT_CONVENTION}}`
|
|
32
44
|
|
|
45
|
+
Run any full suite **once**, capturing output to a file (`<test command> > /tmp/test-run.log 2>&1`; check the exit code), then grep the file for failures and details — never re-run the whole suite just to re-read or filter its output.
|
|
46
|
+
|
|
33
47
|
5. **Self-review before you finish (forked code-review — two axes).** When all acceptance criteria pass, review your own diff along both axes, then fix what you find and re-run typecheck + tests:
|
|
34
48
|
- **Standards** — does the diff follow the conventions of the surrounding code? Watch for the smells: duplicated logic (extract it), mysterious names (rename), primitive obsession (give the concept a type), speculative generality (delete anything the ticket did not ask for), feature envy, data clumps.
|
|
35
49
|
- **Spec = this ticket** — is every acceptance criterion actually met, and is there **nothing in the diff the ticket did not ask for**? Missing and extra both count.
|
|
@@ -39,4 +53,4 @@ You are a single agent in a sandbox on branch `feature/<slug>`. You have **one t
|
|
|
39
53
|
|
|
40
54
|
- **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
55
|
- **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.
|
|
56
|
+
- **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.
|