runcastle 1.0.10 → 1.0.12

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.
Files changed (3) hide show
  1. package/bin/runcastle.js +94 -52
  2. package/index.js +94 -52
  3. package/package.json +1 -1
package/bin/runcastle.js CHANGED
@@ -1931,8 +1931,9 @@ var init_dev_pane = __esm(() => {
1931
1931
  });
1932
1932
 
1933
1933
  // src/services/git.ts
1934
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, realpathSync } from "fs";
1935
- import { dirname, resolve as resolve3 } from "path";
1934
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, mkdtempSync, realpathSync, rmSync } from "fs";
1935
+ import { tmpdir } from "os";
1936
+ import { dirname, join as join6, resolve as resolve3 } from "path";
1936
1937
  import { simpleGit } from "simple-git";
1937
1938
  function featureBranch(slug) {
1938
1939
  return `feature/${slug}`;
@@ -2139,6 +2140,9 @@ function researchBranchName(slug, waypointSeq, unique) {
2139
2140
  function ticketBranchName(slug, ticketSeq, unique) {
2140
2141
  return `${TICKET_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${ticketSeq}-${unique}`;
2141
2142
  }
2143
+ async function allowPushToCheckedOutBranches(repoPath) {
2144
+ await git(repoPath).addConfig("receive.denyCurrentBranch", "ignore");
2145
+ }
2142
2146
  async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
2143
2147
  const g = git(repoPath);
2144
2148
  let branches;
@@ -2168,13 +2172,47 @@ async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
2168
2172
  } else {
2169
2173
  try {
2170
2174
  await g.raw(["fetch", ".", `${tempBranch}:${featureBranchName}`]);
2171
- } catch (e) {
2172
- return { ok: false, error: errMsg(e) };
2175
+ } catch {
2176
+ const merged = await mergeInDisposableWorktree(g, featureBranchName, tempBranch);
2177
+ if (!merged.ok)
2178
+ return merged;
2173
2179
  }
2174
2180
  }
2175
2181
  await deleteBranchDetachingWorktrees(g, repoPath, tempBranch);
2176
2182
  return { ok: true };
2177
2183
  }
2184
+ async function mergeInDisposableWorktree(g, featureBranchName, tempBranch) {
2185
+ let dir;
2186
+ try {
2187
+ dir = mkdtempSync(join6(tmpdir(), "rc-land-"));
2188
+ } catch (e) {
2189
+ return { ok: false, error: errMsg(e) };
2190
+ }
2191
+ const wt = join6(dir, "wt");
2192
+ try {
2193
+ await g.raw(["worktree", "add", wt, featureBranchName]);
2194
+ const gw = git(wt);
2195
+ try {
2196
+ await gw.merge([tempBranch]);
2197
+ } catch (e) {
2198
+ if (await mergeInProgress(gw)) {
2199
+ await gw.raw(["merge", "--abort"]);
2200
+ return { ok: false, conflict: true, error: errMsg(e) };
2201
+ }
2202
+ return { ok: false, error: errMsg(e) };
2203
+ }
2204
+ return { ok: true };
2205
+ } catch (e) {
2206
+ return { ok: false, error: errMsg(e) };
2207
+ } finally {
2208
+ try {
2209
+ await g.raw(["worktree", "remove", "--force", wt]);
2210
+ } catch {}
2211
+ try {
2212
+ rmSync(dir, { recursive: true, force: true });
2213
+ } catch {}
2214
+ }
2215
+ }
2178
2216
  async function deleteBranchDetachingWorktrees(g, repoPath, branch) {
2179
2217
  for (const holder of await worktreesOnBranch(g, branch, repoPath)) {
2180
2218
  try {
@@ -2380,7 +2418,7 @@ var init_git = __esm(() => {
2380
2418
 
2381
2419
  // src/services/knowledge.ts
2382
2420
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync } from "fs";
2383
- import { join as join6, relative, resolve as resolve4, sep } from "path";
2421
+ import { join as join7, relative, resolve as resolve4, sep } from "path";
2384
2422
  function scaffoldDocs(ctx, feature) {
2385
2423
  const project = projectForFeature(ctx, feature);
2386
2424
  const dir = featureDocsDir(project, feature);
@@ -2435,7 +2473,7 @@ function listDocs(ctx, feature) {
2435
2473
  const dir = featureDocsDir(project, feature);
2436
2474
  if (!existsSync8(dir))
2437
2475
  return [];
2438
- return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join6(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
2476
+ return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join7(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
2439
2477
  }
2440
2478
  function readDoc(ctx, feature, relPath) {
2441
2479
  const project = projectForFeature(ctx, feature);
@@ -2476,21 +2514,21 @@ var init_knowledge = __esm(() => {
2476
2514
 
2477
2515
  // src/launcher/skills-root.ts
2478
2516
  import { existsSync as existsSync9 } from "fs";
2479
- import { dirname as dirname2, join as join7, resolve as resolve5 } from "path";
2517
+ import { dirname as dirname2, join as join8, resolve as resolve5 } from "path";
2480
2518
  function resolveSkillsRoot(fromDir) {
2481
2519
  const override = process.env[SKILLS_DIR_ENV];
2482
2520
  if (override) {
2483
2521
  const root = resolve5(override);
2484
- if (existsSync9(join7(root, SKILLS_MARKER)))
2522
+ if (existsSync9(join8(root, SKILLS_MARKER)))
2485
2523
  return root;
2486
2524
  throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
2487
2525
  }
2488
2526
  const searched = [];
2489
2527
  let dir = fromDir;
2490
2528
  for (let i = 0;i < 8; i += 1) {
2491
- const candidate = join7(dir, "packages", "skills");
2529
+ const candidate = join8(dir, "packages", "skills");
2492
2530
  searched.push(candidate);
2493
- if (existsSync9(join7(candidate, SKILLS_MARKER)))
2531
+ if (existsSync9(join8(candidate, SKILLS_MARKER)))
2494
2532
  return candidate;
2495
2533
  const parent = dirname2(dir);
2496
2534
  if (parent === dir)
@@ -2503,7 +2541,7 @@ function resolveSkillsRoot(fromDir) {
2503
2541
  }
2504
2542
  var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
2505
2543
  var init_skills_root = __esm(() => {
2506
- SKILLS_MARKER = join7("packs", "runcastle");
2544
+ SKILLS_MARKER = join8("packs", "runcastle");
2507
2545
  });
2508
2546
 
2509
2547
  // src/services/agent-stream.ts
@@ -2548,7 +2586,7 @@ var init_agent_stream = __esm(() => {
2548
2586
 
2549
2587
  // src/workflows/ticket-burner.ts
2550
2588
  import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
2551
- import { dirname as dirname3, join as join8 } from "path";
2589
+ import { dirname as dirname3, join as join9 } from "path";
2552
2590
  import { fileURLToPath as fileURLToPath2 } from "url";
2553
2591
  import { claudeCode, run } from "@ai-hero/sandcastle";
2554
2592
  import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
@@ -2705,9 +2743,9 @@ function resolveBurnWorkspaceMode(config2, platform = process.platform) {
2705
2743
  function buildIsolatedSetupCommand(tempBranch, setupCommand) {
2706
2744
  const hookFile = `${ISOLATED_REPO_PATH}/.git/hooks/post-commit`;
2707
2745
  const parts = [
2708
- `git -C ${SANDBOX_WORKSPACE_PATH} config receive.denyCurrentBranch updateInstead`,
2746
+ `git config --global --add safe.directory '*'`,
2709
2747
  `git clone ${SANDBOX_WORKSPACE_PATH} ${ISOLATED_REPO_PATH}`,
2710
- `printf '#!/bin/sh\\nexec git push --quiet origin HEAD:%s\\n' '${tempBranch}' > ${hookFile}`,
2748
+ `printf '#!/bin/sh\\nunset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE\\ngit push --quiet origin HEAD:%s && exec git -C ${SANDBOX_WORKSPACE_PATH} reset --hard --quiet %s\\n' '${tempBranch}' '${tempBranch}' > ${hookFile}`,
2711
2749
  `chmod +x ${hookFile}`
2712
2750
  ];
2713
2751
  if (setupCommand)
@@ -2728,7 +2766,7 @@ function buildWorkspaceNotes(mode) {
2728
2766
  `);
2729
2767
  }
2730
2768
  function readRepoToolchain(repoPath) {
2731
- const pkgPath = join8(repoPath, "package.json");
2769
+ const pkgPath = join9(repoPath, "package.json");
2732
2770
  const hasPackageJson = existsSync10(pkgPath);
2733
2771
  let packageManagerField;
2734
2772
  if (hasPackageJson) {
@@ -2742,10 +2780,10 @@ function readRepoToolchain(repoPath) {
2742
2780
  hasPackageJson,
2743
2781
  ...packageManagerField !== undefined ? { packageManagerField } : {},
2744
2782
  lockfiles: {
2745
- bun: existsSync10(join8(repoPath, "bun.lock")) || existsSync10(join8(repoPath, "bun.lockb")),
2746
- pnpm: existsSync10(join8(repoPath, "pnpm-lock.yaml")),
2747
- yarn: existsSync10(join8(repoPath, "yarn.lock")),
2748
- npm: existsSync10(join8(repoPath, "package-lock.json"))
2783
+ bun: existsSync10(join9(repoPath, "bun.lock")) || existsSync10(join9(repoPath, "bun.lockb")),
2784
+ pnpm: existsSync10(join9(repoPath, "pnpm-lock.yaml")),
2785
+ yarn: existsSync10(join9(repoPath, "yarn.lock")),
2786
+ npm: existsSync10(join9(repoPath, "package-lock.json"))
2749
2787
  }
2750
2788
  };
2751
2789
  }
@@ -2955,23 +2993,23 @@ async function burnRun(ctx, deps) {
2955
2993
  }
2956
2994
  function burnerTemplatePath() {
2957
2995
  const here = dirname3(fileURLToPath2(import.meta.url));
2958
- return join8(resolveSkillsRoot(here), "burner", "implement-ticket.md");
2996
+ return join9(resolveSkillsRoot(here), "burner", "implement-ticket.md");
2959
2997
  }
2960
2998
  function readDocsDigestFromDisk(projectId, slug) {
2961
2999
  const worktree = worktreeDir(projectId, slug);
2962
3000
  if (!existsSync10(worktree))
2963
3001
  return "_No talk worktree on disk \u2014 docs digest skipped._";
2964
- const docsDir = join8(worktree, ...featureDocsRel(slug).split("/"));
3002
+ const docsDir = join9(worktree, ...featureDocsRel(slug).split("/"));
2965
3003
  if (!existsSync10(docsDir))
2966
3004
  return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
2967
- const files = readdirSync3(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync4(join8(docsDir, e.name), "utf8") }));
3005
+ const files = readdirSync3(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync4(join9(docsDir, e.name), "utf8") }));
2968
3006
  return buildDocsDigest(files);
2969
3007
  }
2970
3008
  function readBlockedFile(dirs) {
2971
3009
  for (const dir of dirs) {
2972
3010
  if (!dir)
2973
3011
  continue;
2974
- const p = join8(dir, "BLOCKED.md");
3012
+ const p = join9(dir, "BLOCKED.md");
2975
3013
  if (existsSync10(p)) {
2976
3014
  try {
2977
3015
  return readFileSync4(p, "utf8");
@@ -3014,10 +3052,12 @@ function selectSandbox(config2, mounts = []) {
3014
3052
  return noSandbox();
3015
3053
  }
3016
3054
  }
3017
- async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
3055
+ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget) {
3018
3056
  const { project, feature } = ctx;
3019
3057
  const tempBranch = ticketBranchName(feature.slug, ticket.seq, newId("b").slice(2, 10));
3020
3058
  const workspaceMode = resolveBurnWorkspaceMode(config2);
3059
+ if (workspaceMode === "isolated")
3060
+ await ensureIsolatedPushTarget();
3021
3061
  const template = readFileSync4(burnerTemplatePath(), "utf8");
3022
3062
  const prompt = renderTicketPrompt(template, {
3023
3063
  TICKET_JSON: buildTicketJson(ticket),
@@ -3046,7 +3086,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
3046
3086
  });
3047
3087
  }
3048
3088
  mkdirSync4(logsDir(), { recursive: true });
3049
- const logFilePath = join8(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
3089
+ const logFilePath = join9(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
3050
3090
  const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
3051
3091
  beginTranscript(ticket.id);
3052
3092
  const onStreamEvent = (event) => {
@@ -3131,11 +3171,13 @@ function resolveBurnDeps(ctx) {
3131
3171
  const token = readTokenFromEnvFile(envPath());
3132
3172
  const model = resolveModel("implement", config2, ctx.project, ctx.modelOverride);
3133
3173
  const land = createSerialQueue();
3174
+ let pushTargetReady;
3175
+ const ensureIsolatedPushTarget = () => pushTargetReady ??= allowPushToCheckedOutBranches(ctx.project.repoPath);
3134
3176
  return {
3135
3177
  config: config2,
3136
3178
  hasAuthToken: token !== undefined,
3137
3179
  concurrency: config2.burnConcurrency,
3138
- executeTicketRun: (c, ticket) => realExecuteTicketRun(c, ticket, config2, token, model, land)
3180
+ executeTicketRun: (c, ticket) => realExecuteTicketRun(c, ticket, config2, token, model, land, ensureIsolatedPushTarget)
3139
3181
  };
3140
3182
  }
3141
3183
  function readTokenFromEnvFile(path) {
@@ -3180,7 +3222,7 @@ var init_ticket_burner = __esm(() => {
3180
3222
 
3181
3223
  // src/workflows/research.ts
3182
3224
  import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
3183
- import { dirname as dirname4, join as join9 } from "path";
3225
+ import { dirname as dirname4, join as join10 } from "path";
3184
3226
  import { fileURLToPath as fileURLToPath3 } from "url";
3185
3227
  import { run as run2 } from "@ai-hero/sandcastle";
3186
3228
  import { docker as docker2 } from "@ai-hero/sandcastle/sandboxes/docker";
@@ -3252,16 +3294,16 @@ function interpretResearchResult(result, docRelPath) {
3252
3294
  }
3253
3295
  function researchTemplatePath() {
3254
3296
  const here = dirname4(fileURLToPath3(import.meta.url));
3255
- return join9(resolveSkillsRoot(here), "burner", "research-waypoint.md");
3297
+ return join10(resolveSkillsRoot(here), "burner", "research-waypoint.md");
3256
3298
  }
3257
3299
  function readDocsDigestFromDisk2(projectId, slug) {
3258
3300
  const worktree = worktreeDir(projectId, slug);
3259
3301
  if (!existsSync11(worktree))
3260
3302
  return "_No talk worktree on disk \u2014 docs digest skipped._";
3261
- const docsDir = join9(worktree, ...featureDocsRel(slug).split("/"));
3303
+ const docsDir = join10(worktree, ...featureDocsRel(slug).split("/"));
3262
3304
  if (!existsSync11(docsDir))
3263
3305
  return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
3264
- const files = readdirSync4(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync5(join9(docsDir, e.name), "utf8") }));
3306
+ const files = readdirSync4(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync5(join10(docsDir, e.name), "utf8") }));
3265
3307
  return buildDocsDigest(files);
3266
3308
  }
3267
3309
  async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
@@ -3276,7 +3318,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
3276
3318
  RESEARCH_DOC_PATH: docRelPath
3277
3319
  });
3278
3320
  mkdirSync5(logsDir(), { recursive: true });
3279
- const logFilePath = join9(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
3321
+ const logFilePath = join10(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
3280
3322
  const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
3281
3323
  const runOptions = {
3282
3324
  agent: buildBurnAgent(config2, token, model),
@@ -4137,7 +4179,7 @@ var init_hooks = __esm(() => {
4137
4179
  // src/routes/web.ts
4138
4180
  import { existsSync as existsSync12, statSync } from "fs";
4139
4181
  import { readFile } from "fs/promises";
4140
- import { extname, join as join10, normalize, resolve as resolve6, sep as sep2 } from "path";
4182
+ import { extname, join as join11, normalize, resolve as resolve6, sep as sep2 } from "path";
4141
4183
  function contentType(path) {
4142
4184
  return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
4143
4185
  }
@@ -4155,7 +4197,7 @@ function safeJoin(root, pathname) {
4155
4197
  } catch {
4156
4198
  return null;
4157
4199
  }
4158
- const joined = normalize(join10(root, decoded));
4200
+ const joined = normalize(join11(root, decoded));
4159
4201
  if (joined !== root && !joined.startsWith(root + sep2))
4160
4202
  return null;
4161
4203
  return joined;
@@ -4168,7 +4210,7 @@ function resolveWebDist() {
4168
4210
  }
4169
4211
  function mountWebApp(app, distDir) {
4170
4212
  const root = resolve6(distDir);
4171
- const indexHtml = join10(root, "index.html");
4213
+ const indexHtml = join11(root, "index.html");
4172
4214
  app.get("*", async (c, next) => {
4173
4215
  const { pathname } = new URL(c.req.url);
4174
4216
  if (/^\/(api|mcp|ws|health)(\/|$)/.test(pathname))
@@ -4188,7 +4230,7 @@ function mountWebApp(app, distDir) {
4188
4230
  }
4189
4231
  function mountWebAppIfBuilt(app) {
4190
4232
  const dist = resolveWebDist();
4191
- if (existsSync12(join10(dist, "index.html")))
4233
+ if (existsSync12(join11(dist, "index.html")))
4192
4234
  mountWebApp(app, dist);
4193
4235
  }
4194
4236
  var MIME;
@@ -4264,7 +4306,7 @@ var init_events2 = __esm(() => {
4264
4306
  // src/launcher/artifacts.ts
4265
4307
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync2 } from "fs";
4266
4308
  import { fileURLToPath as fileURLToPath4 } from "url";
4267
- import { join as join11 } from "path";
4309
+ import { join as join12 } from "path";
4268
4310
  function hookClientPath() {
4269
4311
  return resolveAsset(ASSET_ENV.hookClient, fileURLToPath4(new URL("./hook-client.ts", import.meta.url)));
4270
4312
  }
@@ -4476,9 +4518,9 @@ async function writeSessionArtifacts(input) {
4476
4518
  const { session, feature, config: config2, waypoint } = input;
4477
4519
  const dir = sessionDir(session.id);
4478
4520
  mkdirSync6(dir, { recursive: true });
4479
- const systemPromptPath = join11(dir, "system-prompt.md");
4480
- const settingsPath = join11(dir, "settings.json");
4481
- const mcpConfigPath = join11(dir, "mcp.json");
4521
+ const systemPromptPath = join12(dir, "system-prompt.md");
4522
+ const settingsPath = join12(dir, "settings.json");
4523
+ const mcpConfigPath = join12(dir, "mcp.json");
4482
4524
  writeFileSync2(systemPromptPath, renderSystemPrompt(feature, session.kind, waypoint), "utf8");
4483
4525
  writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath()), null, 2), "utf8");
4484
4526
  writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
@@ -4540,7 +4582,7 @@ var init_end_session = __esm(() => {
4540
4582
 
4541
4583
  // src/launcher/launcher.ts
4542
4584
  import { existsSync as existsSync13 } from "fs";
4543
- import { dirname as dirname5, join as join12, resolve as resolve7 } from "path";
4585
+ import { dirname as dirname5, join as join13, resolve as resolve7 } from "path";
4544
4586
  import { fileURLToPath as fileURLToPath5 } from "url";
4545
4587
  import { and as and5, eq as eq8 } from "drizzle-orm";
4546
4588
  function ptyExitMessage(exitCode) {
@@ -4578,10 +4620,10 @@ function claudeSpawnTarget(claudeArgs) {
4578
4620
  return { file: exe, args: claudeArgs };
4579
4621
  }
4580
4622
  function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
4581
- const rel = join12("packages", "skills", "packs", "runcastle");
4623
+ const rel = join13("packages", "skills", "packs", "runcastle");
4582
4624
  const override = process.env[SKILLS_DIR_ENV];
4583
4625
  if (override) {
4584
- const dir2 = join12(resolve7(override), "packs", "runcastle");
4626
+ const dir2 = join13(resolve7(override), "packs", "runcastle");
4585
4627
  if (existsSync13(dir2))
4586
4628
  return dir2;
4587
4629
  throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
@@ -4589,7 +4631,7 @@ function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
4589
4631
  const searched = [];
4590
4632
  let dir = fromDir;
4591
4633
  for (let i = 0;i < 8; i += 1) {
4592
- const candidate = join12(dir, rel);
4634
+ const candidate = join13(dir, rel);
4593
4635
  searched.push(candidate);
4594
4636
  if (existsSync13(candidate))
4595
4637
  return candidate;
@@ -4943,7 +4985,7 @@ var init_feature = __esm(() => {
4943
4985
 
4944
4986
  // src/services/projects.ts
4945
4987
  import { existsSync as existsSync14 } from "fs";
4946
- import { basename, join as join13 } from "path";
4988
+ import { basename, join as join14 } from "path";
4947
4989
  import { eq as eq9, isNull } from "drizzle-orm";
4948
4990
  function listProjects(ctx) {
4949
4991
  return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
@@ -5019,7 +5061,7 @@ async function assertRepoTolerant(ctx, repoPath) {
5019
5061
  } catch (e) {
5020
5062
  if (!isNotImplemented(e))
5021
5063
  throw e;
5022
- if (!existsSync14(join13(repoPath, ".git"))) {
5064
+ if (!existsSync14(join14(repoPath, ".git"))) {
5023
5065
  throw new InvalidInputError(`not a git repository: ${repoPath}`);
5024
5066
  }
5025
5067
  }
@@ -5546,7 +5588,7 @@ var init_system_exec = __esm(() => {
5546
5588
 
5547
5589
  // src/services/setup.ts
5548
5590
  import { copyFileSync, existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
5549
- import { dirname as dirname7, join as join14 } from "path";
5591
+ import { dirname as dirname7, join as join15 } from "path";
5550
5592
  import { fileURLToPath as fileURLToPath6 } from "url";
5551
5593
  async function writeGitIdentity(exec, identity) {
5552
5594
  const name = identity.name.trim();
@@ -5624,11 +5666,11 @@ function resolveSandcastleBin() {
5624
5666
  const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
5625
5667
  let dir = dirname7(fileURLToPath6(mainUrl));
5626
5668
  for (let hops = 0;hops < 6; hops++) {
5627
- const manifestPath = join14(dir, "package.json");
5669
+ const manifestPath = join15(dir, "package.json");
5628
5670
  if (existsSync16(manifestPath)) {
5629
5671
  const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
5630
5672
  if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
5631
- return join14(dir, manifest.bin.sandcastle);
5673
+ return join15(dir, manifest.bin.sandcastle);
5632
5674
  }
5633
5675
  }
5634
5676
  const parent = dirname7(dir);
@@ -5656,12 +5698,12 @@ function sandcastleTemplateDir() {
5656
5698
  return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath6(new URL("../assets/sandcastle", import.meta.url)));
5657
5699
  }
5658
5700
  function scaffoldSandcastleConfig(templateDir, targetDir) {
5659
- const dir = join14(targetDir, ".sandcastle");
5701
+ const dir = join15(targetDir, ".sandcastle");
5660
5702
  if (existsSync16(dir))
5661
5703
  return { scaffolded: false, dir };
5662
5704
  mkdirSync8(dir, { recursive: true });
5663
5705
  for (const name of readdirSync5(templateDir)) {
5664
- copyFileSync(join14(templateDir, name), join14(dir, name));
5706
+ copyFileSync(join15(templateDir, name), join15(dir, name));
5665
5707
  }
5666
5708
  return { scaffolded: true, dir };
5667
5709
  }
@@ -5825,13 +5867,13 @@ var init_update_check = __esm(() => {
5825
5867
 
5826
5868
  // src/version.ts
5827
5869
  import { readFileSync as readFileSync8 } from "fs";
5828
- import { dirname as dirname8, join as join15 } from "path";
5870
+ import { dirname as dirname8, join as join16 } from "path";
5829
5871
  import { fileURLToPath as fileURLToPath7 } from "url";
5830
5872
  function runcastleVersion() {
5831
5873
  if (cached2)
5832
5874
  return cached2;
5833
5875
  const here = dirname8(fileURLToPath7(import.meta.url));
5834
- for (const pkgPath of [join15(here, "..", "package.json"), join15(here, "package.json")]) {
5876
+ for (const pkgPath of [join16(here, "..", "package.json"), join16(here, "package.json")]) {
5835
5877
  try {
5836
5878
  const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
5837
5879
  if (typeof pkg.version === "string") {
package/index.js CHANGED
@@ -1930,8 +1930,9 @@ var init_dev_pane = __esm(() => {
1930
1930
  });
1931
1931
 
1932
1932
  // src/services/git.ts
1933
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, realpathSync } from "fs";
1934
- import { dirname, resolve as resolve3 } from "path";
1933
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, mkdtempSync, realpathSync, rmSync } from "fs";
1934
+ import { tmpdir } from "os";
1935
+ import { dirname, join as join6, resolve as resolve3 } from "path";
1935
1936
  import { simpleGit } from "simple-git";
1936
1937
  function featureBranch(slug) {
1937
1938
  return `feature/${slug}`;
@@ -2138,6 +2139,9 @@ function researchBranchName(slug, waypointSeq, unique) {
2138
2139
  function ticketBranchName(slug, ticketSeq, unique) {
2139
2140
  return `${TICKET_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${ticketSeq}-${unique}`;
2140
2141
  }
2142
+ async function allowPushToCheckedOutBranches(repoPath) {
2143
+ await git(repoPath).addConfig("receive.denyCurrentBranch", "ignore");
2144
+ }
2141
2145
  async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
2142
2146
  const g = git(repoPath);
2143
2147
  let branches;
@@ -2167,13 +2171,47 @@ async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
2167
2171
  } else {
2168
2172
  try {
2169
2173
  await g.raw(["fetch", ".", `${tempBranch}:${featureBranchName}`]);
2170
- } catch (e) {
2171
- return { ok: false, error: errMsg(e) };
2174
+ } catch {
2175
+ const merged = await mergeInDisposableWorktree(g, featureBranchName, tempBranch);
2176
+ if (!merged.ok)
2177
+ return merged;
2172
2178
  }
2173
2179
  }
2174
2180
  await deleteBranchDetachingWorktrees(g, repoPath, tempBranch);
2175
2181
  return { ok: true };
2176
2182
  }
2183
+ async function mergeInDisposableWorktree(g, featureBranchName, tempBranch) {
2184
+ let dir;
2185
+ try {
2186
+ dir = mkdtempSync(join6(tmpdir(), "rc-land-"));
2187
+ } catch (e) {
2188
+ return { ok: false, error: errMsg(e) };
2189
+ }
2190
+ const wt = join6(dir, "wt");
2191
+ try {
2192
+ await g.raw(["worktree", "add", wt, featureBranchName]);
2193
+ const gw = git(wt);
2194
+ try {
2195
+ await gw.merge([tempBranch]);
2196
+ } catch (e) {
2197
+ if (await mergeInProgress(gw)) {
2198
+ await gw.raw(["merge", "--abort"]);
2199
+ return { ok: false, conflict: true, error: errMsg(e) };
2200
+ }
2201
+ return { ok: false, error: errMsg(e) };
2202
+ }
2203
+ return { ok: true };
2204
+ } catch (e) {
2205
+ return { ok: false, error: errMsg(e) };
2206
+ } finally {
2207
+ try {
2208
+ await g.raw(["worktree", "remove", "--force", wt]);
2209
+ } catch {}
2210
+ try {
2211
+ rmSync(dir, { recursive: true, force: true });
2212
+ } catch {}
2213
+ }
2214
+ }
2177
2215
  async function deleteBranchDetachingWorktrees(g, repoPath, branch) {
2178
2216
  for (const holder of await worktreesOnBranch(g, branch, repoPath)) {
2179
2217
  try {
@@ -2379,7 +2417,7 @@ var init_git = __esm(() => {
2379
2417
 
2380
2418
  // src/services/knowledge.ts
2381
2419
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync } from "fs";
2382
- import { join as join6, relative, resolve as resolve4, sep } from "path";
2420
+ import { join as join7, relative, resolve as resolve4, sep } from "path";
2383
2421
  function scaffoldDocs(ctx, feature) {
2384
2422
  const project = projectForFeature(ctx, feature);
2385
2423
  const dir = featureDocsDir(project, feature);
@@ -2434,7 +2472,7 @@ function listDocs(ctx, feature) {
2434
2472
  const dir = featureDocsDir(project, feature);
2435
2473
  if (!existsSync8(dir))
2436
2474
  return [];
2437
- return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join6(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
2475
+ return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => ({ relPath: e.name, title: readTitle(join7(dir, e.name), e.name) })).sort((a, b) => a.relPath.localeCompare(b.relPath));
2438
2476
  }
2439
2477
  function readDoc(ctx, feature, relPath) {
2440
2478
  const project = projectForFeature(ctx, feature);
@@ -2475,21 +2513,21 @@ var init_knowledge = __esm(() => {
2475
2513
 
2476
2514
  // src/launcher/skills-root.ts
2477
2515
  import { existsSync as existsSync9 } from "fs";
2478
- import { dirname as dirname2, join as join7, resolve as resolve5 } from "path";
2516
+ import { dirname as dirname2, join as join8, resolve as resolve5 } from "path";
2479
2517
  function resolveSkillsRoot(fromDir) {
2480
2518
  const override = process.env[SKILLS_DIR_ENV];
2481
2519
  if (override) {
2482
2520
  const root = resolve5(override);
2483
- if (existsSync9(join7(root, SKILLS_MARKER)))
2521
+ if (existsSync9(join8(root, SKILLS_MARKER)))
2484
2522
  return root;
2485
2523
  throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
2486
2524
  }
2487
2525
  const searched = [];
2488
2526
  let dir = fromDir;
2489
2527
  for (let i = 0;i < 8; i += 1) {
2490
- const candidate = join7(dir, "packages", "skills");
2528
+ const candidate = join8(dir, "packages", "skills");
2491
2529
  searched.push(candidate);
2492
- if (existsSync9(join7(candidate, SKILLS_MARKER)))
2530
+ if (existsSync9(join8(candidate, SKILLS_MARKER)))
2493
2531
  return candidate;
2494
2532
  const parent = dirname2(dir);
2495
2533
  if (parent === dir)
@@ -2502,7 +2540,7 @@ function resolveSkillsRoot(fromDir) {
2502
2540
  }
2503
2541
  var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
2504
2542
  var init_skills_root = __esm(() => {
2505
- SKILLS_MARKER = join7("packs", "runcastle");
2543
+ SKILLS_MARKER = join8("packs", "runcastle");
2506
2544
  });
2507
2545
 
2508
2546
  // src/services/agent-stream.ts
@@ -2547,7 +2585,7 @@ var init_agent_stream = __esm(() => {
2547
2585
 
2548
2586
  // src/workflows/ticket-burner.ts
2549
2587
  import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
2550
- import { dirname as dirname3, join as join8 } from "path";
2588
+ import { dirname as dirname3, join as join9 } from "path";
2551
2589
  import { fileURLToPath as fileURLToPath2 } from "url";
2552
2590
  import { claudeCode, run } from "@ai-hero/sandcastle";
2553
2591
  import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
@@ -2704,9 +2742,9 @@ function resolveBurnWorkspaceMode(config2, platform = process.platform) {
2704
2742
  function buildIsolatedSetupCommand(tempBranch, setupCommand) {
2705
2743
  const hookFile = `${ISOLATED_REPO_PATH}/.git/hooks/post-commit`;
2706
2744
  const parts = [
2707
- `git -C ${SANDBOX_WORKSPACE_PATH} config receive.denyCurrentBranch updateInstead`,
2745
+ `git config --global --add safe.directory '*'`,
2708
2746
  `git clone ${SANDBOX_WORKSPACE_PATH} ${ISOLATED_REPO_PATH}`,
2709
- `printf '#!/bin/sh\\nexec git push --quiet origin HEAD:%s\\n' '${tempBranch}' > ${hookFile}`,
2747
+ `printf '#!/bin/sh\\nunset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE\\ngit push --quiet origin HEAD:%s && exec git -C ${SANDBOX_WORKSPACE_PATH} reset --hard --quiet %s\\n' '${tempBranch}' '${tempBranch}' > ${hookFile}`,
2710
2748
  `chmod +x ${hookFile}`
2711
2749
  ];
2712
2750
  if (setupCommand)
@@ -2727,7 +2765,7 @@ function buildWorkspaceNotes(mode) {
2727
2765
  `);
2728
2766
  }
2729
2767
  function readRepoToolchain(repoPath) {
2730
- const pkgPath = join8(repoPath, "package.json");
2768
+ const pkgPath = join9(repoPath, "package.json");
2731
2769
  const hasPackageJson = existsSync10(pkgPath);
2732
2770
  let packageManagerField;
2733
2771
  if (hasPackageJson) {
@@ -2741,10 +2779,10 @@ function readRepoToolchain(repoPath) {
2741
2779
  hasPackageJson,
2742
2780
  ...packageManagerField !== undefined ? { packageManagerField } : {},
2743
2781
  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"))
2782
+ bun: existsSync10(join9(repoPath, "bun.lock")) || existsSync10(join9(repoPath, "bun.lockb")),
2783
+ pnpm: existsSync10(join9(repoPath, "pnpm-lock.yaml")),
2784
+ yarn: existsSync10(join9(repoPath, "yarn.lock")),
2785
+ npm: existsSync10(join9(repoPath, "package-lock.json"))
2748
2786
  }
2749
2787
  };
2750
2788
  }
@@ -2954,23 +2992,23 @@ async function burnRun(ctx, deps) {
2954
2992
  }
2955
2993
  function burnerTemplatePath() {
2956
2994
  const here = dirname3(fileURLToPath2(import.meta.url));
2957
- return join8(resolveSkillsRoot(here), "burner", "implement-ticket.md");
2995
+ return join9(resolveSkillsRoot(here), "burner", "implement-ticket.md");
2958
2996
  }
2959
2997
  function readDocsDigestFromDisk(projectId, slug) {
2960
2998
  const worktree = worktreeDir(projectId, slug);
2961
2999
  if (!existsSync10(worktree))
2962
3000
  return "_No talk worktree on disk \u2014 docs digest skipped._";
2963
- const docsDir = join8(worktree, ...featureDocsRel(slug).split("/"));
3001
+ const docsDir = join9(worktree, ...featureDocsRel(slug).split("/"));
2964
3002
  if (!existsSync10(docsDir))
2965
3003
  return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
2966
- const files = readdirSync3(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync4(join8(docsDir, e.name), "utf8") }));
3004
+ const files = readdirSync3(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync4(join9(docsDir, e.name), "utf8") }));
2967
3005
  return buildDocsDigest(files);
2968
3006
  }
2969
3007
  function readBlockedFile(dirs) {
2970
3008
  for (const dir of dirs) {
2971
3009
  if (!dir)
2972
3010
  continue;
2973
- const p = join8(dir, "BLOCKED.md");
3011
+ const p = join9(dir, "BLOCKED.md");
2974
3012
  if (existsSync10(p)) {
2975
3013
  try {
2976
3014
  return readFileSync4(p, "utf8");
@@ -3013,10 +3051,12 @@ function selectSandbox(config2, mounts = []) {
3013
3051
  return noSandbox();
3014
3052
  }
3015
3053
  }
3016
- async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
3054
+ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, ensureIsolatedPushTarget) {
3017
3055
  const { project, feature } = ctx;
3018
3056
  const tempBranch = ticketBranchName(feature.slug, ticket.seq, newId("b").slice(2, 10));
3019
3057
  const workspaceMode = resolveBurnWorkspaceMode(config2);
3058
+ if (workspaceMode === "isolated")
3059
+ await ensureIsolatedPushTarget();
3020
3060
  const template = readFileSync4(burnerTemplatePath(), "utf8");
3021
3061
  const prompt = renderTicketPrompt(template, {
3022
3062
  TICKET_JSON: buildTicketJson(ticket),
@@ -3045,7 +3085,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
3045
3085
  });
3046
3086
  }
3047
3087
  mkdirSync4(logsDir(), { recursive: true });
3048
- const logFilePath = join8(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
3088
+ const logFilePath = join9(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
3049
3089
  const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
3050
3090
  beginTranscript(ticket.id);
3051
3091
  const onStreamEvent = (event) => {
@@ -3130,11 +3170,13 @@ function resolveBurnDeps(ctx) {
3130
3170
  const token = readTokenFromEnvFile(envPath());
3131
3171
  const model = resolveModel("implement", config2, ctx.project, ctx.modelOverride);
3132
3172
  const land = createSerialQueue();
3173
+ let pushTargetReady;
3174
+ const ensureIsolatedPushTarget = () => pushTargetReady ??= allowPushToCheckedOutBranches(ctx.project.repoPath);
3133
3175
  return {
3134
3176
  config: config2,
3135
3177
  hasAuthToken: token !== undefined,
3136
3178
  concurrency: config2.burnConcurrency,
3137
- executeTicketRun: (c, ticket) => realExecuteTicketRun(c, ticket, config2, token, model, land)
3179
+ executeTicketRun: (c, ticket) => realExecuteTicketRun(c, ticket, config2, token, model, land, ensureIsolatedPushTarget)
3138
3180
  };
3139
3181
  }
3140
3182
  function readTokenFromEnvFile(path) {
@@ -3179,7 +3221,7 @@ var init_ticket_burner = __esm(() => {
3179
3221
 
3180
3222
  // src/workflows/research.ts
3181
3223
  import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
3182
- import { dirname as dirname4, join as join9 } from "path";
3224
+ import { dirname as dirname4, join as join10 } from "path";
3183
3225
  import { fileURLToPath as fileURLToPath3 } from "url";
3184
3226
  import { run as run2 } from "@ai-hero/sandcastle";
3185
3227
  import { docker as docker2 } from "@ai-hero/sandcastle/sandboxes/docker";
@@ -3251,16 +3293,16 @@ function interpretResearchResult(result, docRelPath) {
3251
3293
  }
3252
3294
  function researchTemplatePath() {
3253
3295
  const here = dirname4(fileURLToPath3(import.meta.url));
3254
- return join9(resolveSkillsRoot(here), "burner", "research-waypoint.md");
3296
+ return join10(resolveSkillsRoot(here), "burner", "research-waypoint.md");
3255
3297
  }
3256
3298
  function readDocsDigestFromDisk2(projectId, slug) {
3257
3299
  const worktree = worktreeDir(projectId, slug);
3258
3300
  if (!existsSync11(worktree))
3259
3301
  return "_No talk worktree on disk \u2014 docs digest skipped._";
3260
- const docsDir = join9(worktree, ...featureDocsRel(slug).split("/"));
3302
+ const docsDir = join10(worktree, ...featureDocsRel(slug).split("/"));
3261
3303
  if (!existsSync11(docsDir))
3262
3304
  return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
3263
- const files = readdirSync4(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync5(join9(docsDir, e.name), "utf8") }));
3305
+ const files = readdirSync4(docsDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, content: readFileSync5(join10(docsDir, e.name), "utf8") }));
3264
3306
  return buildDocsDigest(files);
3265
3307
  }
3266
3308
  async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
@@ -3275,7 +3317,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
3275
3317
  RESEARCH_DOC_PATH: docRelPath
3276
3318
  });
3277
3319
  mkdirSync5(logsDir(), { recursive: true });
3278
- const logFilePath = join9(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
3320
+ const logFilePath = join10(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
3279
3321
  const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
3280
3322
  const runOptions = {
3281
3323
  agent: buildBurnAgent(config2, token, model),
@@ -4136,7 +4178,7 @@ var init_hooks = __esm(() => {
4136
4178
  // src/routes/web.ts
4137
4179
  import { existsSync as existsSync12, statSync } from "fs";
4138
4180
  import { readFile } from "fs/promises";
4139
- import { extname, join as join10, normalize, resolve as resolve6, sep as sep2 } from "path";
4181
+ import { extname, join as join11, normalize, resolve as resolve6, sep as sep2 } from "path";
4140
4182
  function contentType(path) {
4141
4183
  return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
4142
4184
  }
@@ -4154,7 +4196,7 @@ function safeJoin(root, pathname) {
4154
4196
  } catch {
4155
4197
  return null;
4156
4198
  }
4157
- const joined = normalize(join10(root, decoded));
4199
+ const joined = normalize(join11(root, decoded));
4158
4200
  if (joined !== root && !joined.startsWith(root + sep2))
4159
4201
  return null;
4160
4202
  return joined;
@@ -4167,7 +4209,7 @@ function resolveWebDist() {
4167
4209
  }
4168
4210
  function mountWebApp(app, distDir) {
4169
4211
  const root = resolve6(distDir);
4170
- const indexHtml = join10(root, "index.html");
4212
+ const indexHtml = join11(root, "index.html");
4171
4213
  app.get("*", async (c, next) => {
4172
4214
  const { pathname } = new URL(c.req.url);
4173
4215
  if (/^\/(api|mcp|ws|health)(\/|$)/.test(pathname))
@@ -4187,7 +4229,7 @@ function mountWebApp(app, distDir) {
4187
4229
  }
4188
4230
  function mountWebAppIfBuilt(app) {
4189
4231
  const dist = resolveWebDist();
4190
- if (existsSync12(join10(dist, "index.html")))
4232
+ if (existsSync12(join11(dist, "index.html")))
4191
4233
  mountWebApp(app, dist);
4192
4234
  }
4193
4235
  var MIME;
@@ -4263,7 +4305,7 @@ var init_events2 = __esm(() => {
4263
4305
  // src/launcher/artifacts.ts
4264
4306
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync2 } from "fs";
4265
4307
  import { fileURLToPath as fileURLToPath4 } from "url";
4266
- import { join as join11 } from "path";
4308
+ import { join as join12 } from "path";
4267
4309
  function hookClientPath() {
4268
4310
  return resolveAsset(ASSET_ENV.hookClient, fileURLToPath4(new URL("./hook-client.ts", import.meta.url)));
4269
4311
  }
@@ -4475,9 +4517,9 @@ async function writeSessionArtifacts(input) {
4475
4517
  const { session, feature, config: config2, waypoint } = input;
4476
4518
  const dir = sessionDir(session.id);
4477
4519
  mkdirSync6(dir, { recursive: true });
4478
- const systemPromptPath = join11(dir, "system-prompt.md");
4479
- const settingsPath = join11(dir, "settings.json");
4480
- const mcpConfigPath = join11(dir, "mcp.json");
4520
+ const systemPromptPath = join12(dir, "system-prompt.md");
4521
+ const settingsPath = join12(dir, "settings.json");
4522
+ const mcpConfigPath = join12(dir, "mcp.json");
4481
4523
  writeFileSync2(systemPromptPath, renderSystemPrompt(feature, session.kind, waypoint), "utf8");
4482
4524
  writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath()), null, 2), "utf8");
4483
4525
  writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
@@ -4539,7 +4581,7 @@ var init_end_session = __esm(() => {
4539
4581
 
4540
4582
  // src/launcher/launcher.ts
4541
4583
  import { existsSync as existsSync13 } from "fs";
4542
- import { dirname as dirname5, join as join12, resolve as resolve7 } from "path";
4584
+ import { dirname as dirname5, join as join13, resolve as resolve7 } from "path";
4543
4585
  import { fileURLToPath as fileURLToPath5 } from "url";
4544
4586
  import { and as and5, eq as eq8 } from "drizzle-orm";
4545
4587
  function ptyExitMessage(exitCode) {
@@ -4577,10 +4619,10 @@ function claudeSpawnTarget(claudeArgs) {
4577
4619
  return { file: exe, args: claudeArgs };
4578
4620
  }
4579
4621
  function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
4580
- const rel = join12("packages", "skills", "packs", "runcastle");
4622
+ const rel = join13("packages", "skills", "packs", "runcastle");
4581
4623
  const override = process.env[SKILLS_DIR_ENV];
4582
4624
  if (override) {
4583
- const dir2 = join12(resolve7(override), "packs", "runcastle");
4625
+ const dir2 = join13(resolve7(override), "packs", "runcastle");
4584
4626
  if (existsSync13(dir2))
4585
4627
  return dir2;
4586
4628
  throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
@@ -4588,7 +4630,7 @@ function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
4588
4630
  const searched = [];
4589
4631
  let dir = fromDir;
4590
4632
  for (let i = 0;i < 8; i += 1) {
4591
- const candidate = join12(dir, rel);
4633
+ const candidate = join13(dir, rel);
4592
4634
  searched.push(candidate);
4593
4635
  if (existsSync13(candidate))
4594
4636
  return candidate;
@@ -4942,7 +4984,7 @@ var init_feature = __esm(() => {
4942
4984
 
4943
4985
  // src/services/projects.ts
4944
4986
  import { existsSync as existsSync14 } from "fs";
4945
- import { basename, join as join13 } from "path";
4987
+ import { basename, join as join14 } from "path";
4946
4988
  import { eq as eq9, isNull } from "drizzle-orm";
4947
4989
  function listProjects(ctx) {
4948
4990
  return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
@@ -5018,7 +5060,7 @@ async function assertRepoTolerant(ctx, repoPath) {
5018
5060
  } catch (e) {
5019
5061
  if (!isNotImplemented(e))
5020
5062
  throw e;
5021
- if (!existsSync14(join13(repoPath, ".git"))) {
5063
+ if (!existsSync14(join14(repoPath, ".git"))) {
5022
5064
  throw new InvalidInputError(`not a git repository: ${repoPath}`);
5023
5065
  }
5024
5066
  }
@@ -5545,7 +5587,7 @@ var init_system_exec = __esm(() => {
5545
5587
 
5546
5588
  // src/services/setup.ts
5547
5589
  import { copyFileSync, existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
5548
- import { dirname as dirname7, join as join14 } from "path";
5590
+ import { dirname as dirname7, join as join15 } from "path";
5549
5591
  import { fileURLToPath as fileURLToPath6 } from "url";
5550
5592
  async function writeGitIdentity(exec, identity) {
5551
5593
  const name = identity.name.trim();
@@ -5623,11 +5665,11 @@ function resolveSandcastleBin() {
5623
5665
  const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
5624
5666
  let dir = dirname7(fileURLToPath6(mainUrl));
5625
5667
  for (let hops = 0;hops < 6; hops++) {
5626
- const manifestPath = join14(dir, "package.json");
5668
+ const manifestPath = join15(dir, "package.json");
5627
5669
  if (existsSync16(manifestPath)) {
5628
5670
  const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
5629
5671
  if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
5630
- return join14(dir, manifest.bin.sandcastle);
5672
+ return join15(dir, manifest.bin.sandcastle);
5631
5673
  }
5632
5674
  }
5633
5675
  const parent = dirname7(dir);
@@ -5655,12 +5697,12 @@ function sandcastleTemplateDir() {
5655
5697
  return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath6(new URL("../assets/sandcastle", import.meta.url)));
5656
5698
  }
5657
5699
  function scaffoldSandcastleConfig(templateDir, targetDir) {
5658
- const dir = join14(targetDir, ".sandcastle");
5700
+ const dir = join15(targetDir, ".sandcastle");
5659
5701
  if (existsSync16(dir))
5660
5702
  return { scaffolded: false, dir };
5661
5703
  mkdirSync8(dir, { recursive: true });
5662
5704
  for (const name of readdirSync5(templateDir)) {
5663
- copyFileSync(join14(templateDir, name), join14(dir, name));
5705
+ copyFileSync(join15(templateDir, name), join15(dir, name));
5664
5706
  }
5665
5707
  return { scaffolded: true, dir };
5666
5708
  }
@@ -5824,13 +5866,13 @@ var init_update_check = __esm(() => {
5824
5866
 
5825
5867
  // src/version.ts
5826
5868
  import { readFileSync as readFileSync8 } from "fs";
5827
- import { dirname as dirname8, join as join15 } from "path";
5869
+ import { dirname as dirname8, join as join16 } from "path";
5828
5870
  import { fileURLToPath as fileURLToPath7 } from "url";
5829
5871
  function runcastleVersion() {
5830
5872
  if (cached2)
5831
5873
  return cached2;
5832
5874
  const here = dirname8(fileURLToPath7(import.meta.url));
5833
- for (const pkgPath of [join15(here, "..", "package.json"), join15(here, "package.json")]) {
5875
+ for (const pkgPath of [join16(here, "..", "package.json"), join16(here, "package.json")]) {
5834
5876
  try {
5835
5877
  const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
5836
5878
  if (typeof pkg.version === "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Burn tickets into shipped features with Claude Code.",
5
5
  "license": "MIT",
6
6
  "type": "module",