runcastle 1.0.2 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,12 +1,16 @@
1
1
  // @bun
2
2
  var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
3
7
  var __export = (target, all) => {
4
8
  for (var name in all)
5
9
  __defProp(target, name, {
6
10
  get: all[name],
7
11
  enumerable: true,
8
12
  configurable: true,
9
- set: (newValue) => all[name] = () => newValue
13
+ set: __exportSetter.bind(all, name)
10
14
  });
11
15
  };
12
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -57,6 +61,9 @@ var init_paths = () => {};
57
61
 
58
62
  // ../core/src/config.ts
59
63
  import * as z from "zod";
64
+ function resolveSandboxImage(config) {
65
+ return config.sandboxImage ?? DEFAULT_SANDBOX_IMAGE;
66
+ }
60
67
  function resolveModel(step, config, project, runOverride) {
61
68
  return runOverride ?? config.stepModels[step] ?? project?.model ?? config.model;
62
69
  }
@@ -72,7 +79,7 @@ var MODEL_STEPS, ModelStep, DEFAULT_SMOKE_MODEL = "claude-haiku-4-5-20251001", f
72
79
  }
73
80
  delete obj.smokeModel;
74
81
  return obj;
75
- }, RuncastleConfig;
82
+ }, RuncastleConfig, DEFAULT_SANDBOX_IMAGE = "sandcastle:runcastle";
76
83
  var init_config = __esm(() => {
77
84
  MODEL_STEPS = [
78
85
  "ideation",
@@ -225,6 +232,7 @@ var init_schemas = __esm(() => {
225
232
  mapped: z2.boolean(),
226
233
  phase: Phase,
227
234
  branch: z2.string(),
235
+ baseBranch: z2.string().optional(),
228
236
  status: FeatureStatus,
229
237
  createdAt: z2.number()
230
238
  });
@@ -438,6 +446,7 @@ var init_db_schema = __esm(() => {
438
446
  mapped: integer("mapped", { mode: "boolean" }).notNull().default(false),
439
447
  phase: text("phase").notNull().$type(),
440
448
  branch: text("branch").notNull(),
449
+ baseBranch: text("base_branch"),
441
450
  status: text("status").notNull().$type(),
442
451
  createdAt: integer("created_at").notNull()
443
452
  });
@@ -1164,6 +1173,7 @@ function rowToFeature(row) {
1164
1173
  mapped: row.mapped,
1165
1174
  phase: row.phase,
1166
1175
  branch: row.branch,
1176
+ baseBranch: row.baseBranch ?? undefined,
1167
1177
  status: row.status,
1168
1178
  createdAt: row.createdAt
1169
1179
  };
@@ -1900,20 +1910,59 @@ async function detectMainBranch(repoPath) {
1900
1910
  } catch {}
1901
1911
  return "main";
1902
1912
  }
1903
- async function createFeatureBranch(project, slug) {
1913
+ async function createFeatureBranch(project, slug, base) {
1904
1914
  const branch = featureBranch(slug);
1915
+ const from = base?.trim() || project.mainBranch;
1905
1916
  const g = git(project.repoPath);
1906
1917
  const branches = await g.branchLocal();
1907
1918
  if (branches.all.includes(branch))
1908
1919
  return branch;
1909
- await g.raw(["branch", branch, project.mainBranch]);
1920
+ if (!branches.all.includes(from)) {
1921
+ throw new Error(`base branch "${from}" does not exist in ${project.repoPath}`);
1922
+ }
1923
+ await g.raw(["branch", branch, from]);
1910
1924
  return branch;
1911
1925
  }
1926
+ async function listBranches(project) {
1927
+ const g = git(project.repoPath);
1928
+ const local = await g.branchLocal();
1929
+ const branches = local.all.filter((name) => !name.startsWith("feature/"));
1930
+ const localSet = new Set(local.all);
1931
+ let remoteBranches = [];
1932
+ try {
1933
+ const remote = await g.branch(["-r"]);
1934
+ remoteBranches = remote.all.filter((r) => !r.includes(" -> ") && !r.endsWith("/HEAD")).filter((r) => {
1935
+ const short = r.replace(/^[^/]+\//, "");
1936
+ return !short.startsWith("feature/") && !localSet.has(short);
1937
+ });
1938
+ } catch {}
1939
+ return { current: local.current, mainBranch: project.mainBranch, branches, remoteBranches };
1940
+ }
1941
+ async function resolveBaseBranch(project, base) {
1942
+ const g = git(project.repoPath);
1943
+ const local = await g.branchLocal();
1944
+ if (local.all.includes(base))
1945
+ return base;
1946
+ const remotes = (await g.getRemotes()).map((r) => r.name);
1947
+ for (const rem of remotes) {
1948
+ const prefix = `${rem}/`;
1949
+ if (!base.startsWith(prefix))
1950
+ continue;
1951
+ const name = base.slice(prefix.length);
1952
+ if (!name || name.endsWith("/HEAD"))
1953
+ break;
1954
+ if (local.all.includes(name))
1955
+ return name;
1956
+ await g.raw(["branch", "--track", name, base]);
1957
+ return name;
1958
+ }
1959
+ throw new Error(`base branch "${base}" does not exist in ${project.repoPath}`);
1960
+ }
1912
1961
  async function ensureTalkWorktree(project, feature) {
1913
1962
  const worktreePath = worktreeDir(project.id, feature.slug);
1914
1963
  const branch = featureBranch(feature.slug);
1915
1964
  const g = git(project.repoPath);
1916
- await ensureBranchExists(g, branch, project.mainBranch);
1965
+ await ensureBranchExists(g, branch, feature.baseBranch ?? project.mainBranch);
1917
1966
  if (await worktreeIsValid(g, worktreePath, branch))
1918
1967
  return worktreePath;
1919
1968
  mkdirSync2(dirname(worktreePath), { recursive: true });
@@ -2184,6 +2233,7 @@ async function testDrive(ctx, project, feature, action) {
2184
2233
  return { ok: true, branch };
2185
2234
  }
2186
2235
  async function mergeFeature(project, feature) {
2236
+ const target = feature.baseBranch ?? project.mainBranch;
2187
2237
  if (testDriveState) {
2188
2238
  throw new GateError("Cannot merge while a test drive is active \u2014 stop it first");
2189
2239
  }
@@ -2192,19 +2242,33 @@ async function mergeFeature(project, feature) {
2192
2242
  if (porcelain !== "") {
2193
2243
  throw new GateError("Cannot merge \u2014 working tree has uncommitted changes");
2194
2244
  }
2245
+ const branches = await g.branchLocal();
2246
+ if (!branches.all.includes(target)) {
2247
+ throw new GateError(`Cannot merge \u2014 base branch "${target}" no longer exists`);
2248
+ }
2249
+ const previous = (await g.revparse(["--abbrev-ref", "HEAD"])).trim();
2195
2250
  const branch = featureBranch(feature.slug);
2196
- await g.checkout(project.mainBranch);
2251
+ await g.checkout(target);
2197
2252
  try {
2198
2253
  await g.merge(["--no-ff", branch]);
2199
- return { ok: true };
2254
+ await restoreBranch(g, previous, target);
2255
+ return { ok: true, target };
2200
2256
  } catch (e) {
2201
2257
  if (await mergeInProgress(g)) {
2202
2258
  await g.raw(["merge", "--abort"]);
2203
- return { ok: false, conflict: true };
2259
+ await restoreBranch(g, previous, target);
2260
+ return { ok: false, conflict: true, target };
2204
2261
  }
2205
2262
  throw e instanceof Error ? e : new Error(errMsg(e));
2206
2263
  }
2207
2264
  }
2265
+ async function restoreBranch(g, previous, target) {
2266
+ if (previous === target || previous === "HEAD" || !previous)
2267
+ return;
2268
+ try {
2269
+ await g.checkout(previous);
2270
+ } catch {}
2271
+ }
2208
2272
  async function mergeInProgress(g) {
2209
2273
  try {
2210
2274
  await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"]);
@@ -2694,7 +2758,7 @@ function buildBurnAgent(config2, token, model) {
2694
2758
  return agent;
2695
2759
  }
2696
2760
  function selectSandbox(config2) {
2697
- const imageOpts = config2.sandboxImage ? { imageName: config2.sandboxImage } : {};
2761
+ const imageOpts = { imageName: resolveSandboxImage(config2) };
2698
2762
  switch (config2.sandbox) {
2699
2763
  case "docker":
2700
2764
  return docker(imageOpts);
@@ -2886,7 +2950,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
2886
2950
  const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
2887
2951
  const runOptions = {
2888
2952
  agent: buildBurnAgent(config2, token, model),
2889
- sandbox: config2.sandbox === "docker" ? docker2(config2.sandboxImage ? { imageName: config2.sandboxImage } : {}) : noSandbox2(),
2953
+ sandbox: config2.sandbox === "docker" ? docker2({ imageName: resolveSandboxImage(config2) }) : noSandbox2(),
2890
2954
  cwd: project.repoPath,
2891
2955
  prompt,
2892
2956
  branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
@@ -3118,7 +3182,8 @@ async function createFeature(ctx, input) {
3118
3182
  const project = requireProjectById(ctx, input.projectId);
3119
3183
  const slug = uniqueSlug(ctx, project.id, input.title);
3120
3184
  const branch = `feature/${slug}`;
3121
- const { branchReady } = await ensureFeatureBranch(project, slug);
3185
+ const requestedBase = input.baseBranch?.trim() || project.mainBranch;
3186
+ const { branchReady, baseBranch } = await ensureFeatureBranch(project, slug, requestedBase);
3122
3187
  const row = {
3123
3188
  id: newId("feat"),
3124
3189
  projectId: project.id,
@@ -3129,6 +3194,7 @@ async function createFeature(ctx, input) {
3129
3194
  mapped: input.mapped ?? false,
3130
3195
  phase: "ideation",
3131
3196
  branch,
3197
+ baseBranch,
3132
3198
  status: "active",
3133
3199
  createdAt: Date.now()
3134
3200
  };
@@ -3136,8 +3202,8 @@ async function createFeature(ctx, input) {
3136
3202
  const feature = rowToFeature(inserted);
3137
3203
  emit(ctx, feature.id, {
3138
3204
  type: "feature.created",
3139
- message: branchReady ? `feature.created (${branch})` : "feature.created (branch pending)",
3140
- data: { slug, branch, branchReady }
3205
+ message: branchReady ? `feature.created (${branch} \u2190 ${baseBranch})` : "feature.created (branch pending)",
3206
+ data: { slug, branch, baseBranch, branchReady }
3141
3207
  });
3142
3208
  scaffoldDocs(ctx, feature);
3143
3209
  if (branchReady) {
@@ -3147,13 +3213,14 @@ async function createFeature(ctx, input) {
3147
3213
  }
3148
3214
  return feature;
3149
3215
  }
3150
- async function ensureFeatureBranch(project, slug) {
3216
+ async function ensureFeatureBranch(project, slug, requestedBase) {
3151
3217
  try {
3152
- await createFeatureBranch(project, slug);
3153
- return { branchReady: true };
3218
+ const baseBranch = await resolveBaseBranch(project, requestedBase);
3219
+ await createFeatureBranch(project, slug, baseBranch);
3220
+ return { branchReady: true, baseBranch };
3154
3221
  } catch (e) {
3155
3222
  if (isNotImplemented(e))
3156
- return { branchReady: false };
3223
+ return { branchReady: false, baseBranch: requestedBase };
3157
3224
  throw e;
3158
3225
  }
3159
3226
  }
@@ -4382,7 +4449,8 @@ var init_feature = __esm(() => {
4382
4449
  title: z6.string().min(1),
4383
4450
  oneLiner: z6.string(),
4384
4451
  size: FeatureSize,
4385
- mapped: z6.boolean().optional()
4452
+ mapped: z6.boolean().optional(),
4453
+ baseBranch: z6.string().optional()
4386
4454
  })).mutation(({ ctx, input }) => createFeature(ctx, input)),
4387
4455
  list: publicProcedure.input(z6.object({ projectId: z6.string() })).query(({ ctx, input }) => list(ctx, input.projectId)),
4388
4456
  get: publicProcedure.input(z6.object({ id: z6.string() })).query(({ ctx, input }) => getFeatureFull(ctx, input.id)),
@@ -4407,7 +4475,7 @@ var init_feature = __esm(() => {
4407
4475
  }
4408
4476
  const res = await mergeFeature(project, feature);
4409
4477
  if (res.ok) {
4410
- setPhase(ctx, input.featureId, "shipped", "feature.shipped", `merged to ${project.mainBranch}`);
4478
+ setPhase(ctx, input.featureId, "shipped", "feature.shipped", `merged to ${res.target}`);
4411
4479
  setFeatureStatus(ctx, input.featureId, "shipped");
4412
4480
  } else {
4413
4481
  emit(ctx, input.featureId, {
@@ -4513,10 +4581,13 @@ var init_projects = __esm(() => {
4513
4581
  import * as z7 from "zod";
4514
4582
  var projectRouter;
4515
4583
  var init_project = __esm(() => {
4584
+ init_git();
4516
4585
  init_projects();
4586
+ init_repo();
4517
4587
  init_context();
4518
4588
  projectRouter = router({
4519
4589
  list: publicProcedure.query(({ ctx }) => listProjects(ctx)),
4590
+ branches: publicProcedure.input(z7.object({ projectId: z7.string() })).query(({ ctx, input }) => listBranches(requireProjectById(ctx, input.projectId))),
4520
4591
  open: publicProcedure.input(z7.object({ repoPath: z7.string().min(1) })).mutation(({ ctx, input }) => openProject(ctx, input.repoPath)),
4521
4592
  close: publicProcedure.input(z7.object({ projectId: z7.string() })).mutation(({ ctx, input }) => closeProject(ctx, input.projectId)),
4522
4593
  rename: publicProcedure.input(z7.object({ projectId: z7.string(), name: z7.string().min(1) })).mutation(({ ctx, input }) => renameProject(ctx, input.projectId, input.name))
@@ -4927,7 +4998,7 @@ function afkTokenProbe(env) {
4927
4998
  async function runDoctor(env) {
4928
4999
  const { exec } = env;
4929
5000
  const processEnv = env.env ?? process.env;
4930
- const imageName = env.imageName ?? "sandcastle:runcastle";
5001
+ const imageName = env.imageName ?? DEFAULT_SANDBOX_IMAGE;
4931
5002
  const results = [
4932
5003
  await bunProbe(exec),
4933
5004
  await nodeProbe(exec),
@@ -4949,6 +5020,9 @@ function exitCodeFor(report, mode) {
4949
5020
  return report.tier1Ok ? 0 : 1;
4950
5021
  return report.ok ? 0 : 1;
4951
5022
  }
5023
+ var init_doctor = __esm(() => {
5024
+ init_src();
5025
+ });
4952
5026
 
4953
5027
  // src/doctor/system-exec.ts
4954
5028
  import { spawn as spawn2 } from "child_process";
@@ -5132,6 +5206,7 @@ function fileAfkTokenIo(exec) {
5132
5206
  var AFK_TOKEN_KEY = "CLAUDE_CODE_OAUTH_TOKEN";
5133
5207
  var init_setup = __esm(() => {
5134
5208
  init_paths();
5209
+ init_doctor();
5135
5210
  init_errors();
5136
5211
  init_asset_paths();
5137
5212
  });
@@ -5141,6 +5216,7 @@ import * as z11 from "zod";
5141
5216
  var setupRouter;
5142
5217
  var init_setup2 = __esm(() => {
5143
5218
  init_src();
5219
+ init_doctor();
5144
5220
  init_system_exec();
5145
5221
  init_registry();
5146
5222
  init_setup();
@@ -5158,7 +5234,7 @@ var init_setup2 = __esm(() => {
5158
5234
  const exec = createSystemExec();
5159
5235
  const preferred = ctx.config.sandbox === "podman" ? "podman" : "docker";
5160
5236
  const runtime = await resolveRuntime(exec, preferred);
5161
- const imageName = ctx.config.sandboxImage ?? "sandcastle:runcastle";
5237
+ const imageName = resolveSandboxImage(ctx.config);
5162
5238
  const sandcastleBin = input.kind === "build-image" ? resolveSandcastleBin() ?? undefined : undefined;
5163
5239
  const spec = terminalSpec(input.kind, { runtime, imageName, sandcastleBin });
5164
5240
  const sessionId = newId("setup");
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "version": "1.0.2",
3
+ "version": "1.0.6",
4
+ "description": "Burn tickets into shipped features with Claude Code.",
5
+ "license": "MIT",
4
6
  "type": "module",
5
7
  "bin": {
6
8
  "runcastle": "./bin/runcastle.js"
@@ -8,6 +10,14 @@
8
10
  "engines": {
9
11
  "bun": ">=1.3.14"
10
12
  },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/MuathZahir/runcastle.git"
16
+ },
17
+ "homepage": "https://github.com/MuathZahir/runcastle#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/MuathZahir/runcastle/issues"
20
+ },
11
21
  "files": [
12
22
  "index.js",
13
23
  "bin",