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/bin/runcastle.js CHANGED
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
4
8
  var __export = (target, all) => {
5
9
  for (var name in all)
6
10
  __defProp(target, name, {
7
11
  get: all[name],
8
12
  enumerable: true,
9
13
  configurable: true,
10
- set: (newValue) => all[name] = () => newValue
14
+ set: __exportSetter.bind(all, name)
11
15
  });
12
16
  };
13
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -58,6 +62,9 @@ var init_paths = () => {};
58
62
 
59
63
  // ../core/src/config.ts
60
64
  import * as z from "zod";
65
+ function resolveSandboxImage(config) {
66
+ return config.sandboxImage ?? DEFAULT_SANDBOX_IMAGE;
67
+ }
61
68
  function resolveModel(step, config, project, runOverride) {
62
69
  return runOverride ?? config.stepModels[step] ?? project?.model ?? config.model;
63
70
  }
@@ -73,7 +80,7 @@ var MODEL_STEPS, ModelStep, DEFAULT_SMOKE_MODEL = "claude-haiku-4-5-20251001", f
73
80
  }
74
81
  delete obj.smokeModel;
75
82
  return obj;
76
- }, RuncastleConfig;
83
+ }, RuncastleConfig, DEFAULT_SANDBOX_IMAGE = "sandcastle:runcastle";
77
84
  var init_config = __esm(() => {
78
85
  MODEL_STEPS = [
79
86
  "ideation",
@@ -226,6 +233,7 @@ var init_schemas = __esm(() => {
226
233
  mapped: z2.boolean(),
227
234
  phase: Phase,
228
235
  branch: z2.string(),
236
+ baseBranch: z2.string().optional(),
229
237
  status: FeatureStatus,
230
238
  createdAt: z2.number()
231
239
  });
@@ -439,6 +447,7 @@ var init_db_schema = __esm(() => {
439
447
  mapped: integer("mapped", { mode: "boolean" }).notNull().default(false),
440
448
  phase: text("phase").notNull().$type(),
441
449
  branch: text("branch").notNull(),
450
+ baseBranch: text("base_branch"),
442
451
  status: text("status").notNull().$type(),
443
452
  createdAt: integer("created_at").notNull()
444
453
  });
@@ -1165,6 +1174,7 @@ function rowToFeature(row) {
1165
1174
  mapped: row.mapped,
1166
1175
  phase: row.phase,
1167
1176
  branch: row.branch,
1177
+ baseBranch: row.baseBranch ?? undefined,
1168
1178
  status: row.status,
1169
1179
  createdAt: row.createdAt
1170
1180
  };
@@ -1901,20 +1911,59 @@ async function detectMainBranch(repoPath) {
1901
1911
  } catch {}
1902
1912
  return "main";
1903
1913
  }
1904
- async function createFeatureBranch(project, slug) {
1914
+ async function createFeatureBranch(project, slug, base) {
1905
1915
  const branch = featureBranch(slug);
1916
+ const from = base?.trim() || project.mainBranch;
1906
1917
  const g = git(project.repoPath);
1907
1918
  const branches = await g.branchLocal();
1908
1919
  if (branches.all.includes(branch))
1909
1920
  return branch;
1910
- await g.raw(["branch", branch, project.mainBranch]);
1921
+ if (!branches.all.includes(from)) {
1922
+ throw new Error(`base branch "${from}" does not exist in ${project.repoPath}`);
1923
+ }
1924
+ await g.raw(["branch", branch, from]);
1911
1925
  return branch;
1912
1926
  }
1927
+ async function listBranches(project) {
1928
+ const g = git(project.repoPath);
1929
+ const local = await g.branchLocal();
1930
+ const branches = local.all.filter((name) => !name.startsWith("feature/"));
1931
+ const localSet = new Set(local.all);
1932
+ let remoteBranches = [];
1933
+ try {
1934
+ const remote = await g.branch(["-r"]);
1935
+ remoteBranches = remote.all.filter((r) => !r.includes(" -> ") && !r.endsWith("/HEAD")).filter((r) => {
1936
+ const short = r.replace(/^[^/]+\//, "");
1937
+ return !short.startsWith("feature/") && !localSet.has(short);
1938
+ });
1939
+ } catch {}
1940
+ return { current: local.current, mainBranch: project.mainBranch, branches, remoteBranches };
1941
+ }
1942
+ async function resolveBaseBranch(project, base) {
1943
+ const g = git(project.repoPath);
1944
+ const local = await g.branchLocal();
1945
+ if (local.all.includes(base))
1946
+ return base;
1947
+ const remotes = (await g.getRemotes()).map((r) => r.name);
1948
+ for (const rem of remotes) {
1949
+ const prefix = `${rem}/`;
1950
+ if (!base.startsWith(prefix))
1951
+ continue;
1952
+ const name = base.slice(prefix.length);
1953
+ if (!name || name.endsWith("/HEAD"))
1954
+ break;
1955
+ if (local.all.includes(name))
1956
+ return name;
1957
+ await g.raw(["branch", "--track", name, base]);
1958
+ return name;
1959
+ }
1960
+ throw new Error(`base branch "${base}" does not exist in ${project.repoPath}`);
1961
+ }
1913
1962
  async function ensureTalkWorktree(project, feature) {
1914
1963
  const worktreePath = worktreeDir(project.id, feature.slug);
1915
1964
  const branch = featureBranch(feature.slug);
1916
1965
  const g = git(project.repoPath);
1917
- await ensureBranchExists(g, branch, project.mainBranch);
1966
+ await ensureBranchExists(g, branch, feature.baseBranch ?? project.mainBranch);
1918
1967
  if (await worktreeIsValid(g, worktreePath, branch))
1919
1968
  return worktreePath;
1920
1969
  mkdirSync2(dirname(worktreePath), { recursive: true });
@@ -2185,6 +2234,7 @@ async function testDrive(ctx, project, feature, action) {
2185
2234
  return { ok: true, branch };
2186
2235
  }
2187
2236
  async function mergeFeature(project, feature) {
2237
+ const target = feature.baseBranch ?? project.mainBranch;
2188
2238
  if (testDriveState) {
2189
2239
  throw new GateError("Cannot merge while a test drive is active \u2014 stop it first");
2190
2240
  }
@@ -2193,19 +2243,33 @@ async function mergeFeature(project, feature) {
2193
2243
  if (porcelain !== "") {
2194
2244
  throw new GateError("Cannot merge \u2014 working tree has uncommitted changes");
2195
2245
  }
2246
+ const branches = await g.branchLocal();
2247
+ if (!branches.all.includes(target)) {
2248
+ throw new GateError(`Cannot merge \u2014 base branch "${target}" no longer exists`);
2249
+ }
2250
+ const previous = (await g.revparse(["--abbrev-ref", "HEAD"])).trim();
2196
2251
  const branch = featureBranch(feature.slug);
2197
- await g.checkout(project.mainBranch);
2252
+ await g.checkout(target);
2198
2253
  try {
2199
2254
  await g.merge(["--no-ff", branch]);
2200
- return { ok: true };
2255
+ await restoreBranch(g, previous, target);
2256
+ return { ok: true, target };
2201
2257
  } catch (e) {
2202
2258
  if (await mergeInProgress(g)) {
2203
2259
  await g.raw(["merge", "--abort"]);
2204
- return { ok: false, conflict: true };
2260
+ await restoreBranch(g, previous, target);
2261
+ return { ok: false, conflict: true, target };
2205
2262
  }
2206
2263
  throw e instanceof Error ? e : new Error(errMsg(e));
2207
2264
  }
2208
2265
  }
2266
+ async function restoreBranch(g, previous, target) {
2267
+ if (previous === target || previous === "HEAD" || !previous)
2268
+ return;
2269
+ try {
2270
+ await g.checkout(previous);
2271
+ } catch {}
2272
+ }
2209
2273
  async function mergeInProgress(g) {
2210
2274
  try {
2211
2275
  await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"]);
@@ -2695,7 +2759,7 @@ function buildBurnAgent(config2, token, model) {
2695
2759
  return agent;
2696
2760
  }
2697
2761
  function selectSandbox(config2) {
2698
- const imageOpts = config2.sandboxImage ? { imageName: config2.sandboxImage } : {};
2762
+ const imageOpts = { imageName: resolveSandboxImage(config2) };
2699
2763
  switch (config2.sandbox) {
2700
2764
  case "docker":
2701
2765
  return docker(imageOpts);
@@ -2887,7 +2951,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
2887
2951
  const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
2888
2952
  const runOptions = {
2889
2953
  agent: buildBurnAgent(config2, token, model),
2890
- sandbox: config2.sandbox === "docker" ? docker2(config2.sandboxImage ? { imageName: config2.sandboxImage } : {}) : noSandbox2(),
2954
+ sandbox: config2.sandbox === "docker" ? docker2({ imageName: resolveSandboxImage(config2) }) : noSandbox2(),
2891
2955
  cwd: project.repoPath,
2892
2956
  prompt,
2893
2957
  branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
@@ -3119,7 +3183,8 @@ async function createFeature(ctx, input) {
3119
3183
  const project = requireProjectById(ctx, input.projectId);
3120
3184
  const slug = uniqueSlug(ctx, project.id, input.title);
3121
3185
  const branch = `feature/${slug}`;
3122
- const { branchReady } = await ensureFeatureBranch(project, slug);
3186
+ const requestedBase = input.baseBranch?.trim() || project.mainBranch;
3187
+ const { branchReady, baseBranch } = await ensureFeatureBranch(project, slug, requestedBase);
3123
3188
  const row = {
3124
3189
  id: newId("feat"),
3125
3190
  projectId: project.id,
@@ -3130,6 +3195,7 @@ async function createFeature(ctx, input) {
3130
3195
  mapped: input.mapped ?? false,
3131
3196
  phase: "ideation",
3132
3197
  branch,
3198
+ baseBranch,
3133
3199
  status: "active",
3134
3200
  createdAt: Date.now()
3135
3201
  };
@@ -3137,8 +3203,8 @@ async function createFeature(ctx, input) {
3137
3203
  const feature = rowToFeature(inserted);
3138
3204
  emit(ctx, feature.id, {
3139
3205
  type: "feature.created",
3140
- message: branchReady ? `feature.created (${branch})` : "feature.created (branch pending)",
3141
- data: { slug, branch, branchReady }
3206
+ message: branchReady ? `feature.created (${branch} \u2190 ${baseBranch})` : "feature.created (branch pending)",
3207
+ data: { slug, branch, baseBranch, branchReady }
3142
3208
  });
3143
3209
  scaffoldDocs(ctx, feature);
3144
3210
  if (branchReady) {
@@ -3148,13 +3214,14 @@ async function createFeature(ctx, input) {
3148
3214
  }
3149
3215
  return feature;
3150
3216
  }
3151
- async function ensureFeatureBranch(project, slug) {
3217
+ async function ensureFeatureBranch(project, slug, requestedBase) {
3152
3218
  try {
3153
- await createFeatureBranch(project, slug);
3154
- return { branchReady: true };
3219
+ const baseBranch = await resolveBaseBranch(project, requestedBase);
3220
+ await createFeatureBranch(project, slug, baseBranch);
3221
+ return { branchReady: true, baseBranch };
3155
3222
  } catch (e) {
3156
3223
  if (isNotImplemented(e))
3157
- return { branchReady: false };
3224
+ return { branchReady: false, baseBranch: requestedBase };
3158
3225
  throw e;
3159
3226
  }
3160
3227
  }
@@ -4383,7 +4450,8 @@ var init_feature = __esm(() => {
4383
4450
  title: z6.string().min(1),
4384
4451
  oneLiner: z6.string(),
4385
4452
  size: FeatureSize,
4386
- mapped: z6.boolean().optional()
4453
+ mapped: z6.boolean().optional(),
4454
+ baseBranch: z6.string().optional()
4387
4455
  })).mutation(({ ctx, input }) => createFeature(ctx, input)),
4388
4456
  list: publicProcedure.input(z6.object({ projectId: z6.string() })).query(({ ctx, input }) => list(ctx, input.projectId)),
4389
4457
  get: publicProcedure.input(z6.object({ id: z6.string() })).query(({ ctx, input }) => getFeatureFull(ctx, input.id)),
@@ -4408,7 +4476,7 @@ var init_feature = __esm(() => {
4408
4476
  }
4409
4477
  const res = await mergeFeature(project, feature);
4410
4478
  if (res.ok) {
4411
- setPhase(ctx, input.featureId, "shipped", "feature.shipped", `merged to ${project.mainBranch}`);
4479
+ setPhase(ctx, input.featureId, "shipped", "feature.shipped", `merged to ${res.target}`);
4412
4480
  setFeatureStatus(ctx, input.featureId, "shipped");
4413
4481
  } else {
4414
4482
  emit(ctx, input.featureId, {
@@ -4514,10 +4582,13 @@ var init_projects = __esm(() => {
4514
4582
  import * as z7 from "zod";
4515
4583
  var projectRouter;
4516
4584
  var init_project = __esm(() => {
4585
+ init_git();
4517
4586
  init_projects();
4587
+ init_repo();
4518
4588
  init_context();
4519
4589
  projectRouter = router({
4520
4590
  list: publicProcedure.query(({ ctx }) => listProjects(ctx)),
4591
+ branches: publicProcedure.input(z7.object({ projectId: z7.string() })).query(({ ctx, input }) => listBranches(requireProjectById(ctx, input.projectId))),
4521
4592
  open: publicProcedure.input(z7.object({ repoPath: z7.string().min(1) })).mutation(({ ctx, input }) => openProject(ctx, input.repoPath)),
4522
4593
  close: publicProcedure.input(z7.object({ projectId: z7.string() })).mutation(({ ctx, input }) => closeProject(ctx, input.projectId)),
4523
4594
  rename: publicProcedure.input(z7.object({ projectId: z7.string(), name: z7.string().min(1) })).mutation(({ ctx, input }) => renameProject(ctx, input.projectId, input.name))
@@ -4928,7 +4999,7 @@ function afkTokenProbe(env) {
4928
4999
  async function runDoctor(env) {
4929
5000
  const { exec } = env;
4930
5001
  const processEnv = env.env ?? process.env;
4931
- const imageName = env.imageName ?? "sandcastle:runcastle";
5002
+ const imageName = env.imageName ?? DEFAULT_SANDBOX_IMAGE;
4932
5003
  const results = [
4933
5004
  await bunProbe(exec),
4934
5005
  await nodeProbe(exec),
@@ -4950,6 +5021,9 @@ function exitCodeFor(report, mode) {
4950
5021
  return report.tier1Ok ? 0 : 1;
4951
5022
  return report.ok ? 0 : 1;
4952
5023
  }
5024
+ var init_doctor = __esm(() => {
5025
+ init_src();
5026
+ });
4953
5027
 
4954
5028
  // src/doctor/system-exec.ts
4955
5029
  import { spawn as spawn2 } from "child_process";
@@ -5133,6 +5207,7 @@ function fileAfkTokenIo(exec) {
5133
5207
  var AFK_TOKEN_KEY = "CLAUDE_CODE_OAUTH_TOKEN";
5134
5208
  var init_setup = __esm(() => {
5135
5209
  init_paths();
5210
+ init_doctor();
5136
5211
  init_errors();
5137
5212
  init_asset_paths();
5138
5213
  });
@@ -5142,6 +5217,7 @@ import * as z11 from "zod";
5142
5217
  var setupRouter;
5143
5218
  var init_setup2 = __esm(() => {
5144
5219
  init_src();
5220
+ init_doctor();
5145
5221
  init_system_exec();
5146
5222
  init_registry();
5147
5223
  init_setup();
@@ -5159,7 +5235,7 @@ var init_setup2 = __esm(() => {
5159
5235
  const exec = createSystemExec();
5160
5236
  const preferred = ctx.config.sandbox === "podman" ? "podman" : "docker";
5161
5237
  const runtime = await resolveRuntime(exec, preferred);
5162
- const imageName = ctx.config.sandboxImage ?? "sandcastle:runcastle";
5238
+ const imageName = resolveSandboxImage(ctx.config);
5163
5239
  const sandcastleBin = input.kind === "build-image" ? resolveSandcastleBin() ?? undefined : undefined;
5164
5240
  const spec = terminalSpec(input.kind, { runtime, imageName, sandcastleBin });
5165
5241
  const sessionId = newId("setup");
@@ -5517,7 +5593,7 @@ function envWithToken() {
5517
5593
  function resolveDoctorEnv() {
5518
5594
  let imageName;
5519
5595
  try {
5520
- imageName = loadConfig().sandboxImage;
5596
+ imageName = resolveSandboxImage(loadConfig());
5521
5597
  } catch {
5522
5598
  imageName = undefined;
5523
5599
  }
@@ -5536,9 +5612,11 @@ async function runCli(argv, log = console.log) {
5536
5612
  return exitCodeFor(report, mode);
5537
5613
  }
5538
5614
  var init_cli = __esm(() => {
5615
+ init_src();
5539
5616
  init_paths();
5540
5617
  init_config_load();
5541
5618
  init_ticket_burner();
5619
+ init_doctor();
5542
5620
  init_report();
5543
5621
  init_system_exec();
5544
5622
  if (false) {}
@@ -0,0 +1 @@
1
+ ALTER TABLE `features` ADD `base_branch` text;