runcastle 1.0.11 → 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 +85 -50
  2. package/index.js +85 -50
  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}`;
@@ -2140,7 +2141,7 @@ function ticketBranchName(slug, ticketSeq, unique) {
2140
2141
  return `${TICKET_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${ticketSeq}-${unique}`;
2141
2142
  }
2142
2143
  async function allowPushToCheckedOutBranches(repoPath) {
2143
- await git(repoPath).addConfig("receive.denyCurrentBranch", "updateInstead");
2144
+ await git(repoPath).addConfig("receive.denyCurrentBranch", "ignore");
2144
2145
  }
2145
2146
  async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
2146
2147
  const g = git(repoPath);
@@ -2171,13 +2172,47 @@ async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
2171
2172
  } else {
2172
2173
  try {
2173
2174
  await g.raw(["fetch", ".", `${tempBranch}:${featureBranchName}`]);
2174
- } catch (e) {
2175
- return { ok: false, error: errMsg(e) };
2175
+ } catch {
2176
+ const merged = await mergeInDisposableWorktree(g, featureBranchName, tempBranch);
2177
+ if (!merged.ok)
2178
+ return merged;
2176
2179
  }
2177
2180
  }
2178
2181
  await deleteBranchDetachingWorktrees(g, repoPath, tempBranch);
2179
2182
  return { ok: true };
2180
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
+ }
2181
2216
  async function deleteBranchDetachingWorktrees(g, repoPath, branch) {
2182
2217
  for (const holder of await worktreesOnBranch(g, branch, repoPath)) {
2183
2218
  try {
@@ -2383,7 +2418,7 @@ var init_git = __esm(() => {
2383
2418
 
2384
2419
  // src/services/knowledge.ts
2385
2420
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync } from "fs";
2386
- import { join as join6, relative, resolve as resolve4, sep } from "path";
2421
+ import { join as join7, relative, resolve as resolve4, sep } from "path";
2387
2422
  function scaffoldDocs(ctx, feature) {
2388
2423
  const project = projectForFeature(ctx, feature);
2389
2424
  const dir = featureDocsDir(project, feature);
@@ -2438,7 +2473,7 @@ function listDocs(ctx, feature) {
2438
2473
  const dir = featureDocsDir(project, feature);
2439
2474
  if (!existsSync8(dir))
2440
2475
  return [];
2441
- 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));
2442
2477
  }
2443
2478
  function readDoc(ctx, feature, relPath) {
2444
2479
  const project = projectForFeature(ctx, feature);
@@ -2479,21 +2514,21 @@ var init_knowledge = __esm(() => {
2479
2514
 
2480
2515
  // src/launcher/skills-root.ts
2481
2516
  import { existsSync as existsSync9 } from "fs";
2482
- 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";
2483
2518
  function resolveSkillsRoot(fromDir) {
2484
2519
  const override = process.env[SKILLS_DIR_ENV];
2485
2520
  if (override) {
2486
2521
  const root = resolve5(override);
2487
- if (existsSync9(join7(root, SKILLS_MARKER)))
2522
+ if (existsSync9(join8(root, SKILLS_MARKER)))
2488
2523
  return root;
2489
2524
  throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
2490
2525
  }
2491
2526
  const searched = [];
2492
2527
  let dir = fromDir;
2493
2528
  for (let i = 0;i < 8; i += 1) {
2494
- const candidate = join7(dir, "packages", "skills");
2529
+ const candidate = join8(dir, "packages", "skills");
2495
2530
  searched.push(candidate);
2496
- if (existsSync9(join7(candidate, SKILLS_MARKER)))
2531
+ if (existsSync9(join8(candidate, SKILLS_MARKER)))
2497
2532
  return candidate;
2498
2533
  const parent = dirname2(dir);
2499
2534
  if (parent === dir)
@@ -2506,7 +2541,7 @@ function resolveSkillsRoot(fromDir) {
2506
2541
  }
2507
2542
  var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
2508
2543
  var init_skills_root = __esm(() => {
2509
- SKILLS_MARKER = join7("packs", "runcastle");
2544
+ SKILLS_MARKER = join8("packs", "runcastle");
2510
2545
  });
2511
2546
 
2512
2547
  // src/services/agent-stream.ts
@@ -2551,7 +2586,7 @@ var init_agent_stream = __esm(() => {
2551
2586
 
2552
2587
  // src/workflows/ticket-burner.ts
2553
2588
  import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
2554
- import { dirname as dirname3, join as join8 } from "path";
2589
+ import { dirname as dirname3, join as join9 } from "path";
2555
2590
  import { fileURLToPath as fileURLToPath2 } from "url";
2556
2591
  import { claudeCode, run } from "@ai-hero/sandcastle";
2557
2592
  import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
@@ -2710,7 +2745,7 @@ function buildIsolatedSetupCommand(tempBranch, setupCommand) {
2710
2745
  const parts = [
2711
2746
  `git config --global --add safe.directory '*'`,
2712
2747
  `git clone ${SANDBOX_WORKSPACE_PATH} ${ISOLATED_REPO_PATH}`,
2713
- `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}`,
2714
2749
  `chmod +x ${hookFile}`
2715
2750
  ];
2716
2751
  if (setupCommand)
@@ -2731,7 +2766,7 @@ function buildWorkspaceNotes(mode) {
2731
2766
  `);
2732
2767
  }
2733
2768
  function readRepoToolchain(repoPath) {
2734
- const pkgPath = join8(repoPath, "package.json");
2769
+ const pkgPath = join9(repoPath, "package.json");
2735
2770
  const hasPackageJson = existsSync10(pkgPath);
2736
2771
  let packageManagerField;
2737
2772
  if (hasPackageJson) {
@@ -2745,10 +2780,10 @@ function readRepoToolchain(repoPath) {
2745
2780
  hasPackageJson,
2746
2781
  ...packageManagerField !== undefined ? { packageManagerField } : {},
2747
2782
  lockfiles: {
2748
- bun: existsSync10(join8(repoPath, "bun.lock")) || existsSync10(join8(repoPath, "bun.lockb")),
2749
- pnpm: existsSync10(join8(repoPath, "pnpm-lock.yaml")),
2750
- yarn: existsSync10(join8(repoPath, "yarn.lock")),
2751
- 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"))
2752
2787
  }
2753
2788
  };
2754
2789
  }
@@ -2958,23 +2993,23 @@ async function burnRun(ctx, deps) {
2958
2993
  }
2959
2994
  function burnerTemplatePath() {
2960
2995
  const here = dirname3(fileURLToPath2(import.meta.url));
2961
- return join8(resolveSkillsRoot(here), "burner", "implement-ticket.md");
2996
+ return join9(resolveSkillsRoot(here), "burner", "implement-ticket.md");
2962
2997
  }
2963
2998
  function readDocsDigestFromDisk(projectId, slug) {
2964
2999
  const worktree = worktreeDir(projectId, slug);
2965
3000
  if (!existsSync10(worktree))
2966
3001
  return "_No talk worktree on disk \u2014 docs digest skipped._";
2967
- const docsDir = join8(worktree, ...featureDocsRel(slug).split("/"));
3002
+ const docsDir = join9(worktree, ...featureDocsRel(slug).split("/"));
2968
3003
  if (!existsSync10(docsDir))
2969
3004
  return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
2970
- 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") }));
2971
3006
  return buildDocsDigest(files);
2972
3007
  }
2973
3008
  function readBlockedFile(dirs) {
2974
3009
  for (const dir of dirs) {
2975
3010
  if (!dir)
2976
3011
  continue;
2977
- const p = join8(dir, "BLOCKED.md");
3012
+ const p = join9(dir, "BLOCKED.md");
2978
3013
  if (existsSync10(p)) {
2979
3014
  try {
2980
3015
  return readFileSync4(p, "utf8");
@@ -3051,7 +3086,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
3051
3086
  });
3052
3087
  }
3053
3088
  mkdirSync4(logsDir(), { recursive: true });
3054
- const logFilePath = join8(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
3089
+ const logFilePath = join9(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
3055
3090
  const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
3056
3091
  beginTranscript(ticket.id);
3057
3092
  const onStreamEvent = (event) => {
@@ -3187,7 +3222,7 @@ var init_ticket_burner = __esm(() => {
3187
3222
 
3188
3223
  // src/workflows/research.ts
3189
3224
  import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
3190
- import { dirname as dirname4, join as join9 } from "path";
3225
+ import { dirname as dirname4, join as join10 } from "path";
3191
3226
  import { fileURLToPath as fileURLToPath3 } from "url";
3192
3227
  import { run as run2 } from "@ai-hero/sandcastle";
3193
3228
  import { docker as docker2 } from "@ai-hero/sandcastle/sandboxes/docker";
@@ -3259,16 +3294,16 @@ function interpretResearchResult(result, docRelPath) {
3259
3294
  }
3260
3295
  function researchTemplatePath() {
3261
3296
  const here = dirname4(fileURLToPath3(import.meta.url));
3262
- return join9(resolveSkillsRoot(here), "burner", "research-waypoint.md");
3297
+ return join10(resolveSkillsRoot(here), "burner", "research-waypoint.md");
3263
3298
  }
3264
3299
  function readDocsDigestFromDisk2(projectId, slug) {
3265
3300
  const worktree = worktreeDir(projectId, slug);
3266
3301
  if (!existsSync11(worktree))
3267
3302
  return "_No talk worktree on disk \u2014 docs digest skipped._";
3268
- const docsDir = join9(worktree, ...featureDocsRel(slug).split("/"));
3303
+ const docsDir = join10(worktree, ...featureDocsRel(slug).split("/"));
3269
3304
  if (!existsSync11(docsDir))
3270
3305
  return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
3271
- 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") }));
3272
3307
  return buildDocsDigest(files);
3273
3308
  }
3274
3309
  async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
@@ -3283,7 +3318,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
3283
3318
  RESEARCH_DOC_PATH: docRelPath
3284
3319
  });
3285
3320
  mkdirSync5(logsDir(), { recursive: true });
3286
- const logFilePath = join9(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
3321
+ const logFilePath = join10(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
3287
3322
  const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
3288
3323
  const runOptions = {
3289
3324
  agent: buildBurnAgent(config2, token, model),
@@ -4144,7 +4179,7 @@ var init_hooks = __esm(() => {
4144
4179
  // src/routes/web.ts
4145
4180
  import { existsSync as existsSync12, statSync } from "fs";
4146
4181
  import { readFile } from "fs/promises";
4147
- 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";
4148
4183
  function contentType(path) {
4149
4184
  return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
4150
4185
  }
@@ -4162,7 +4197,7 @@ function safeJoin(root, pathname) {
4162
4197
  } catch {
4163
4198
  return null;
4164
4199
  }
4165
- const joined = normalize(join10(root, decoded));
4200
+ const joined = normalize(join11(root, decoded));
4166
4201
  if (joined !== root && !joined.startsWith(root + sep2))
4167
4202
  return null;
4168
4203
  return joined;
@@ -4175,7 +4210,7 @@ function resolveWebDist() {
4175
4210
  }
4176
4211
  function mountWebApp(app, distDir) {
4177
4212
  const root = resolve6(distDir);
4178
- const indexHtml = join10(root, "index.html");
4213
+ const indexHtml = join11(root, "index.html");
4179
4214
  app.get("*", async (c, next) => {
4180
4215
  const { pathname } = new URL(c.req.url);
4181
4216
  if (/^\/(api|mcp|ws|health)(\/|$)/.test(pathname))
@@ -4195,7 +4230,7 @@ function mountWebApp(app, distDir) {
4195
4230
  }
4196
4231
  function mountWebAppIfBuilt(app) {
4197
4232
  const dist = resolveWebDist();
4198
- if (existsSync12(join10(dist, "index.html")))
4233
+ if (existsSync12(join11(dist, "index.html")))
4199
4234
  mountWebApp(app, dist);
4200
4235
  }
4201
4236
  var MIME;
@@ -4271,7 +4306,7 @@ var init_events2 = __esm(() => {
4271
4306
  // src/launcher/artifacts.ts
4272
4307
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync2 } from "fs";
4273
4308
  import { fileURLToPath as fileURLToPath4 } from "url";
4274
- import { join as join11 } from "path";
4309
+ import { join as join12 } from "path";
4275
4310
  function hookClientPath() {
4276
4311
  return resolveAsset(ASSET_ENV.hookClient, fileURLToPath4(new URL("./hook-client.ts", import.meta.url)));
4277
4312
  }
@@ -4483,9 +4518,9 @@ async function writeSessionArtifacts(input) {
4483
4518
  const { session, feature, config: config2, waypoint } = input;
4484
4519
  const dir = sessionDir(session.id);
4485
4520
  mkdirSync6(dir, { recursive: true });
4486
- const systemPromptPath = join11(dir, "system-prompt.md");
4487
- const settingsPath = join11(dir, "settings.json");
4488
- 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");
4489
4524
  writeFileSync2(systemPromptPath, renderSystemPrompt(feature, session.kind, waypoint), "utf8");
4490
4525
  writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath()), null, 2), "utf8");
4491
4526
  writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
@@ -4547,7 +4582,7 @@ var init_end_session = __esm(() => {
4547
4582
 
4548
4583
  // src/launcher/launcher.ts
4549
4584
  import { existsSync as existsSync13 } from "fs";
4550
- 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";
4551
4586
  import { fileURLToPath as fileURLToPath5 } from "url";
4552
4587
  import { and as and5, eq as eq8 } from "drizzle-orm";
4553
4588
  function ptyExitMessage(exitCode) {
@@ -4585,10 +4620,10 @@ function claudeSpawnTarget(claudeArgs) {
4585
4620
  return { file: exe, args: claudeArgs };
4586
4621
  }
4587
4622
  function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
4588
- const rel = join12("packages", "skills", "packs", "runcastle");
4623
+ const rel = join13("packages", "skills", "packs", "runcastle");
4589
4624
  const override = process.env[SKILLS_DIR_ENV];
4590
4625
  if (override) {
4591
- const dir2 = join12(resolve7(override), "packs", "runcastle");
4626
+ const dir2 = join13(resolve7(override), "packs", "runcastle");
4592
4627
  if (existsSync13(dir2))
4593
4628
  return dir2;
4594
4629
  throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
@@ -4596,7 +4631,7 @@ function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
4596
4631
  const searched = [];
4597
4632
  let dir = fromDir;
4598
4633
  for (let i = 0;i < 8; i += 1) {
4599
- const candidate = join12(dir, rel);
4634
+ const candidate = join13(dir, rel);
4600
4635
  searched.push(candidate);
4601
4636
  if (existsSync13(candidate))
4602
4637
  return candidate;
@@ -4950,7 +4985,7 @@ var init_feature = __esm(() => {
4950
4985
 
4951
4986
  // src/services/projects.ts
4952
4987
  import { existsSync as existsSync14 } from "fs";
4953
- import { basename, join as join13 } from "path";
4988
+ import { basename, join as join14 } from "path";
4954
4989
  import { eq as eq9, isNull } from "drizzle-orm";
4955
4990
  function listProjects(ctx) {
4956
4991
  return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
@@ -5026,7 +5061,7 @@ async function assertRepoTolerant(ctx, repoPath) {
5026
5061
  } catch (e) {
5027
5062
  if (!isNotImplemented(e))
5028
5063
  throw e;
5029
- if (!existsSync14(join13(repoPath, ".git"))) {
5064
+ if (!existsSync14(join14(repoPath, ".git"))) {
5030
5065
  throw new InvalidInputError(`not a git repository: ${repoPath}`);
5031
5066
  }
5032
5067
  }
@@ -5553,7 +5588,7 @@ var init_system_exec = __esm(() => {
5553
5588
 
5554
5589
  // src/services/setup.ts
5555
5590
  import { copyFileSync, existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
5556
- import { dirname as dirname7, join as join14 } from "path";
5591
+ import { dirname as dirname7, join as join15 } from "path";
5557
5592
  import { fileURLToPath as fileURLToPath6 } from "url";
5558
5593
  async function writeGitIdentity(exec, identity) {
5559
5594
  const name = identity.name.trim();
@@ -5631,11 +5666,11 @@ function resolveSandcastleBin() {
5631
5666
  const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
5632
5667
  let dir = dirname7(fileURLToPath6(mainUrl));
5633
5668
  for (let hops = 0;hops < 6; hops++) {
5634
- const manifestPath = join14(dir, "package.json");
5669
+ const manifestPath = join15(dir, "package.json");
5635
5670
  if (existsSync16(manifestPath)) {
5636
5671
  const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
5637
5672
  if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
5638
- return join14(dir, manifest.bin.sandcastle);
5673
+ return join15(dir, manifest.bin.sandcastle);
5639
5674
  }
5640
5675
  }
5641
5676
  const parent = dirname7(dir);
@@ -5663,12 +5698,12 @@ function sandcastleTemplateDir() {
5663
5698
  return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath6(new URL("../assets/sandcastle", import.meta.url)));
5664
5699
  }
5665
5700
  function scaffoldSandcastleConfig(templateDir, targetDir) {
5666
- const dir = join14(targetDir, ".sandcastle");
5701
+ const dir = join15(targetDir, ".sandcastle");
5667
5702
  if (existsSync16(dir))
5668
5703
  return { scaffolded: false, dir };
5669
5704
  mkdirSync8(dir, { recursive: true });
5670
5705
  for (const name of readdirSync5(templateDir)) {
5671
- copyFileSync(join14(templateDir, name), join14(dir, name));
5706
+ copyFileSync(join15(templateDir, name), join15(dir, name));
5672
5707
  }
5673
5708
  return { scaffolded: true, dir };
5674
5709
  }
@@ -5832,13 +5867,13 @@ var init_update_check = __esm(() => {
5832
5867
 
5833
5868
  // src/version.ts
5834
5869
  import { readFileSync as readFileSync8 } from "fs";
5835
- import { dirname as dirname8, join as join15 } from "path";
5870
+ import { dirname as dirname8, join as join16 } from "path";
5836
5871
  import { fileURLToPath as fileURLToPath7 } from "url";
5837
5872
  function runcastleVersion() {
5838
5873
  if (cached2)
5839
5874
  return cached2;
5840
5875
  const here = dirname8(fileURLToPath7(import.meta.url));
5841
- 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")]) {
5842
5877
  try {
5843
5878
  const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
5844
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}`;
@@ -2139,7 +2140,7 @@ function ticketBranchName(slug, ticketSeq, unique) {
2139
2140
  return `${TICKET_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${ticketSeq}-${unique}`;
2140
2141
  }
2141
2142
  async function allowPushToCheckedOutBranches(repoPath) {
2142
- await git(repoPath).addConfig("receive.denyCurrentBranch", "updateInstead");
2143
+ await git(repoPath).addConfig("receive.denyCurrentBranch", "ignore");
2143
2144
  }
2144
2145
  async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
2145
2146
  const g = git(repoPath);
@@ -2170,13 +2171,47 @@ async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
2170
2171
  } else {
2171
2172
  try {
2172
2173
  await g.raw(["fetch", ".", `${tempBranch}:${featureBranchName}`]);
2173
- } catch (e) {
2174
- return { ok: false, error: errMsg(e) };
2174
+ } catch {
2175
+ const merged = await mergeInDisposableWorktree(g, featureBranchName, tempBranch);
2176
+ if (!merged.ok)
2177
+ return merged;
2175
2178
  }
2176
2179
  }
2177
2180
  await deleteBranchDetachingWorktrees(g, repoPath, tempBranch);
2178
2181
  return { ok: true };
2179
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
+ }
2180
2215
  async function deleteBranchDetachingWorktrees(g, repoPath, branch) {
2181
2216
  for (const holder of await worktreesOnBranch(g, branch, repoPath)) {
2182
2217
  try {
@@ -2382,7 +2417,7 @@ var init_git = __esm(() => {
2382
2417
 
2383
2418
  // src/services/knowledge.ts
2384
2419
  import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync } from "fs";
2385
- import { join as join6, relative, resolve as resolve4, sep } from "path";
2420
+ import { join as join7, relative, resolve as resolve4, sep } from "path";
2386
2421
  function scaffoldDocs(ctx, feature) {
2387
2422
  const project = projectForFeature(ctx, feature);
2388
2423
  const dir = featureDocsDir(project, feature);
@@ -2437,7 +2472,7 @@ function listDocs(ctx, feature) {
2437
2472
  const dir = featureDocsDir(project, feature);
2438
2473
  if (!existsSync8(dir))
2439
2474
  return [];
2440
- 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));
2441
2476
  }
2442
2477
  function readDoc(ctx, feature, relPath) {
2443
2478
  const project = projectForFeature(ctx, feature);
@@ -2478,21 +2513,21 @@ var init_knowledge = __esm(() => {
2478
2513
 
2479
2514
  // src/launcher/skills-root.ts
2480
2515
  import { existsSync as existsSync9 } from "fs";
2481
- 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";
2482
2517
  function resolveSkillsRoot(fromDir) {
2483
2518
  const override = process.env[SKILLS_DIR_ENV];
2484
2519
  if (override) {
2485
2520
  const root = resolve5(override);
2486
- if (existsSync9(join7(root, SKILLS_MARKER)))
2521
+ if (existsSync9(join8(root, SKILLS_MARKER)))
2487
2522
  return root;
2488
2523
  throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
2489
2524
  }
2490
2525
  const searched = [];
2491
2526
  let dir = fromDir;
2492
2527
  for (let i = 0;i < 8; i += 1) {
2493
- const candidate = join7(dir, "packages", "skills");
2528
+ const candidate = join8(dir, "packages", "skills");
2494
2529
  searched.push(candidate);
2495
- if (existsSync9(join7(candidate, SKILLS_MARKER)))
2530
+ if (existsSync9(join8(candidate, SKILLS_MARKER)))
2496
2531
  return candidate;
2497
2532
  const parent = dirname2(dir);
2498
2533
  if (parent === dir)
@@ -2505,7 +2540,7 @@ function resolveSkillsRoot(fromDir) {
2505
2540
  }
2506
2541
  var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
2507
2542
  var init_skills_root = __esm(() => {
2508
- SKILLS_MARKER = join7("packs", "runcastle");
2543
+ SKILLS_MARKER = join8("packs", "runcastle");
2509
2544
  });
2510
2545
 
2511
2546
  // src/services/agent-stream.ts
@@ -2550,7 +2585,7 @@ var init_agent_stream = __esm(() => {
2550
2585
 
2551
2586
  // src/workflows/ticket-burner.ts
2552
2587
  import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
2553
- import { dirname as dirname3, join as join8 } from "path";
2588
+ import { dirname as dirname3, join as join9 } from "path";
2554
2589
  import { fileURLToPath as fileURLToPath2 } from "url";
2555
2590
  import { claudeCode, run } from "@ai-hero/sandcastle";
2556
2591
  import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
@@ -2709,7 +2744,7 @@ function buildIsolatedSetupCommand(tempBranch, setupCommand) {
2709
2744
  const parts = [
2710
2745
  `git config --global --add safe.directory '*'`,
2711
2746
  `git clone ${SANDBOX_WORKSPACE_PATH} ${ISOLATED_REPO_PATH}`,
2712
- `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}`,
2713
2748
  `chmod +x ${hookFile}`
2714
2749
  ];
2715
2750
  if (setupCommand)
@@ -2730,7 +2765,7 @@ function buildWorkspaceNotes(mode) {
2730
2765
  `);
2731
2766
  }
2732
2767
  function readRepoToolchain(repoPath) {
2733
- const pkgPath = join8(repoPath, "package.json");
2768
+ const pkgPath = join9(repoPath, "package.json");
2734
2769
  const hasPackageJson = existsSync10(pkgPath);
2735
2770
  let packageManagerField;
2736
2771
  if (hasPackageJson) {
@@ -2744,10 +2779,10 @@ function readRepoToolchain(repoPath) {
2744
2779
  hasPackageJson,
2745
2780
  ...packageManagerField !== undefined ? { packageManagerField } : {},
2746
2781
  lockfiles: {
2747
- bun: existsSync10(join8(repoPath, "bun.lock")) || existsSync10(join8(repoPath, "bun.lockb")),
2748
- pnpm: existsSync10(join8(repoPath, "pnpm-lock.yaml")),
2749
- yarn: existsSync10(join8(repoPath, "yarn.lock")),
2750
- 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"))
2751
2786
  }
2752
2787
  };
2753
2788
  }
@@ -2957,23 +2992,23 @@ async function burnRun(ctx, deps) {
2957
2992
  }
2958
2993
  function burnerTemplatePath() {
2959
2994
  const here = dirname3(fileURLToPath2(import.meta.url));
2960
- return join8(resolveSkillsRoot(here), "burner", "implement-ticket.md");
2995
+ return join9(resolveSkillsRoot(here), "burner", "implement-ticket.md");
2961
2996
  }
2962
2997
  function readDocsDigestFromDisk(projectId, slug) {
2963
2998
  const worktree = worktreeDir(projectId, slug);
2964
2999
  if (!existsSync10(worktree))
2965
3000
  return "_No talk worktree on disk \u2014 docs digest skipped._";
2966
- const docsDir = join8(worktree, ...featureDocsRel(slug).split("/"));
3001
+ const docsDir = join9(worktree, ...featureDocsRel(slug).split("/"));
2967
3002
  if (!existsSync10(docsDir))
2968
3003
  return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
2969
- 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") }));
2970
3005
  return buildDocsDigest(files);
2971
3006
  }
2972
3007
  function readBlockedFile(dirs) {
2973
3008
  for (const dir of dirs) {
2974
3009
  if (!dir)
2975
3010
  continue;
2976
- const p = join8(dir, "BLOCKED.md");
3011
+ const p = join9(dir, "BLOCKED.md");
2977
3012
  if (existsSync10(p)) {
2978
3013
  try {
2979
3014
  return readFileSync4(p, "utf8");
@@ -3050,7 +3085,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land, en
3050
3085
  });
3051
3086
  }
3052
3087
  mkdirSync4(logsDir(), { recursive: true });
3053
- const logFilePath = join8(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
3088
+ const logFilePath = join9(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
3054
3089
  const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
3055
3090
  beginTranscript(ticket.id);
3056
3091
  const onStreamEvent = (event) => {
@@ -3186,7 +3221,7 @@ var init_ticket_burner = __esm(() => {
3186
3221
 
3187
3222
  // src/workflows/research.ts
3188
3223
  import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
3189
- import { dirname as dirname4, join as join9 } from "path";
3224
+ import { dirname as dirname4, join as join10 } from "path";
3190
3225
  import { fileURLToPath as fileURLToPath3 } from "url";
3191
3226
  import { run as run2 } from "@ai-hero/sandcastle";
3192
3227
  import { docker as docker2 } from "@ai-hero/sandcastle/sandboxes/docker";
@@ -3258,16 +3293,16 @@ function interpretResearchResult(result, docRelPath) {
3258
3293
  }
3259
3294
  function researchTemplatePath() {
3260
3295
  const here = dirname4(fileURLToPath3(import.meta.url));
3261
- return join9(resolveSkillsRoot(here), "burner", "research-waypoint.md");
3296
+ return join10(resolveSkillsRoot(here), "burner", "research-waypoint.md");
3262
3297
  }
3263
3298
  function readDocsDigestFromDisk2(projectId, slug) {
3264
3299
  const worktree = worktreeDir(projectId, slug);
3265
3300
  if (!existsSync11(worktree))
3266
3301
  return "_No talk worktree on disk \u2014 docs digest skipped._";
3267
- const docsDir = join9(worktree, ...featureDocsRel(slug).split("/"));
3302
+ const docsDir = join10(worktree, ...featureDocsRel(slug).split("/"));
3268
3303
  if (!existsSync11(docsDir))
3269
3304
  return "_No docs/features dir in the talk worktree \u2014 docs digest skipped._";
3270
- 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") }));
3271
3306
  return buildDocsDigest(files);
3272
3307
  }
3273
3308
  async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
@@ -3282,7 +3317,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
3282
3317
  RESEARCH_DOC_PATH: docRelPath
3283
3318
  });
3284
3319
  mkdirSync5(logsDir(), { recursive: true });
3285
- const logFilePath = join9(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
3320
+ const logFilePath = join10(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
3286
3321
  const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
3287
3322
  const runOptions = {
3288
3323
  agent: buildBurnAgent(config2, token, model),
@@ -4143,7 +4178,7 @@ var init_hooks = __esm(() => {
4143
4178
  // src/routes/web.ts
4144
4179
  import { existsSync as existsSync12, statSync } from "fs";
4145
4180
  import { readFile } from "fs/promises";
4146
- 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";
4147
4182
  function contentType(path) {
4148
4183
  return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
4149
4184
  }
@@ -4161,7 +4196,7 @@ function safeJoin(root, pathname) {
4161
4196
  } catch {
4162
4197
  return null;
4163
4198
  }
4164
- const joined = normalize(join10(root, decoded));
4199
+ const joined = normalize(join11(root, decoded));
4165
4200
  if (joined !== root && !joined.startsWith(root + sep2))
4166
4201
  return null;
4167
4202
  return joined;
@@ -4174,7 +4209,7 @@ function resolveWebDist() {
4174
4209
  }
4175
4210
  function mountWebApp(app, distDir) {
4176
4211
  const root = resolve6(distDir);
4177
- const indexHtml = join10(root, "index.html");
4212
+ const indexHtml = join11(root, "index.html");
4178
4213
  app.get("*", async (c, next) => {
4179
4214
  const { pathname } = new URL(c.req.url);
4180
4215
  if (/^\/(api|mcp|ws|health)(\/|$)/.test(pathname))
@@ -4194,7 +4229,7 @@ function mountWebApp(app, distDir) {
4194
4229
  }
4195
4230
  function mountWebAppIfBuilt(app) {
4196
4231
  const dist = resolveWebDist();
4197
- if (existsSync12(join10(dist, "index.html")))
4232
+ if (existsSync12(join11(dist, "index.html")))
4198
4233
  mountWebApp(app, dist);
4199
4234
  }
4200
4235
  var MIME;
@@ -4270,7 +4305,7 @@ var init_events2 = __esm(() => {
4270
4305
  // src/launcher/artifacts.ts
4271
4306
  import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync2 } from "fs";
4272
4307
  import { fileURLToPath as fileURLToPath4 } from "url";
4273
- import { join as join11 } from "path";
4308
+ import { join as join12 } from "path";
4274
4309
  function hookClientPath() {
4275
4310
  return resolveAsset(ASSET_ENV.hookClient, fileURLToPath4(new URL("./hook-client.ts", import.meta.url)));
4276
4311
  }
@@ -4482,9 +4517,9 @@ async function writeSessionArtifacts(input) {
4482
4517
  const { session, feature, config: config2, waypoint } = input;
4483
4518
  const dir = sessionDir(session.id);
4484
4519
  mkdirSync6(dir, { recursive: true });
4485
- const systemPromptPath = join11(dir, "system-prompt.md");
4486
- const settingsPath = join11(dir, "settings.json");
4487
- 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");
4488
4523
  writeFileSync2(systemPromptPath, renderSystemPrompt(feature, session.kind, waypoint), "utf8");
4489
4524
  writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath()), null, 2), "utf8");
4490
4525
  writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
@@ -4546,7 +4581,7 @@ var init_end_session = __esm(() => {
4546
4581
 
4547
4582
  // src/launcher/launcher.ts
4548
4583
  import { existsSync as existsSync13 } from "fs";
4549
- 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";
4550
4585
  import { fileURLToPath as fileURLToPath5 } from "url";
4551
4586
  import { and as and5, eq as eq8 } from "drizzle-orm";
4552
4587
  function ptyExitMessage(exitCode) {
@@ -4584,10 +4619,10 @@ function claudeSpawnTarget(claudeArgs) {
4584
4619
  return { file: exe, args: claudeArgs };
4585
4620
  }
4586
4621
  function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
4587
- const rel = join12("packages", "skills", "packs", "runcastle");
4622
+ const rel = join13("packages", "skills", "packs", "runcastle");
4588
4623
  const override = process.env[SKILLS_DIR_ENV];
4589
4624
  if (override) {
4590
- const dir2 = join12(resolve7(override), "packs", "runcastle");
4625
+ const dir2 = join13(resolve7(override), "packs", "runcastle");
4591
4626
  if (existsSync13(dir2))
4592
4627
  return dir2;
4593
4628
  throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
@@ -4595,7 +4630,7 @@ function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
4595
4630
  const searched = [];
4596
4631
  let dir = fromDir;
4597
4632
  for (let i = 0;i < 8; i += 1) {
4598
- const candidate = join12(dir, rel);
4633
+ const candidate = join13(dir, rel);
4599
4634
  searched.push(candidate);
4600
4635
  if (existsSync13(candidate))
4601
4636
  return candidate;
@@ -4949,7 +4984,7 @@ var init_feature = __esm(() => {
4949
4984
 
4950
4985
  // src/services/projects.ts
4951
4986
  import { existsSync as existsSync14 } from "fs";
4952
- import { basename, join as join13 } from "path";
4987
+ import { basename, join as join14 } from "path";
4953
4988
  import { eq as eq9, isNull } from "drizzle-orm";
4954
4989
  function listProjects(ctx) {
4955
4990
  return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
@@ -5025,7 +5060,7 @@ async function assertRepoTolerant(ctx, repoPath) {
5025
5060
  } catch (e) {
5026
5061
  if (!isNotImplemented(e))
5027
5062
  throw e;
5028
- if (!existsSync14(join13(repoPath, ".git"))) {
5063
+ if (!existsSync14(join14(repoPath, ".git"))) {
5029
5064
  throw new InvalidInputError(`not a git repository: ${repoPath}`);
5030
5065
  }
5031
5066
  }
@@ -5552,7 +5587,7 @@ var init_system_exec = __esm(() => {
5552
5587
 
5553
5588
  // src/services/setup.ts
5554
5589
  import { copyFileSync, existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
5555
- import { dirname as dirname7, join as join14 } from "path";
5590
+ import { dirname as dirname7, join as join15 } from "path";
5556
5591
  import { fileURLToPath as fileURLToPath6 } from "url";
5557
5592
  async function writeGitIdentity(exec, identity) {
5558
5593
  const name = identity.name.trim();
@@ -5630,11 +5665,11 @@ function resolveSandcastleBin() {
5630
5665
  const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
5631
5666
  let dir = dirname7(fileURLToPath6(mainUrl));
5632
5667
  for (let hops = 0;hops < 6; hops++) {
5633
- const manifestPath = join14(dir, "package.json");
5668
+ const manifestPath = join15(dir, "package.json");
5634
5669
  if (existsSync16(manifestPath)) {
5635
5670
  const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
5636
5671
  if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
5637
- return join14(dir, manifest.bin.sandcastle);
5672
+ return join15(dir, manifest.bin.sandcastle);
5638
5673
  }
5639
5674
  }
5640
5675
  const parent = dirname7(dir);
@@ -5662,12 +5697,12 @@ function sandcastleTemplateDir() {
5662
5697
  return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath6(new URL("../assets/sandcastle", import.meta.url)));
5663
5698
  }
5664
5699
  function scaffoldSandcastleConfig(templateDir, targetDir) {
5665
- const dir = join14(targetDir, ".sandcastle");
5700
+ const dir = join15(targetDir, ".sandcastle");
5666
5701
  if (existsSync16(dir))
5667
5702
  return { scaffolded: false, dir };
5668
5703
  mkdirSync8(dir, { recursive: true });
5669
5704
  for (const name of readdirSync5(templateDir)) {
5670
- copyFileSync(join14(templateDir, name), join14(dir, name));
5705
+ copyFileSync(join15(templateDir, name), join15(dir, name));
5671
5706
  }
5672
5707
  return { scaffolded: true, dir };
5673
5708
  }
@@ -5831,13 +5866,13 @@ var init_update_check = __esm(() => {
5831
5866
 
5832
5867
  // src/version.ts
5833
5868
  import { readFileSync as readFileSync8 } from "fs";
5834
- import { dirname as dirname8, join as join15 } from "path";
5869
+ import { dirname as dirname8, join as join16 } from "path";
5835
5870
  import { fileURLToPath as fileURLToPath7 } from "url";
5836
5871
  function runcastleVersion() {
5837
5872
  if (cached2)
5838
5873
  return cached2;
5839
5874
  const here = dirname8(fileURLToPath7(import.meta.url));
5840
- 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")]) {
5841
5876
  try {
5842
5877
  const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
5843
5878
  if (typeof pkg.version === "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Burn tickets into shipped features with Claude Code.",
5
5
  "license": "MIT",
6
6
  "type": "module",