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 +100 -22
- package/drizzle/0006_living_typhoid_mary.sql +1 -0
- package/drizzle/meta/0006_snapshot.json +603 -0
- package/drizzle/meta/_journal.json +8 -1
- package/index.js +97 -21
- package/package.json +11 -1
- package/web/assets/{index-YBuT7tTz.js → index-CRhFvrAu.js} +2 -2
- package/web/assets/{index-CKZ5Z7vc.css → index-ClNOSIVg.css} +1 -1
- package/web/index.html +2 -2
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: (
|
|
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
|
-
|
|
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(
|
|
2252
|
+
await g.checkout(target);
|
|
2198
2253
|
try {
|
|
2199
2254
|
await g.merge(["--no-ff", branch]);
|
|
2200
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
3154
|
-
|
|
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 ${
|
|
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 ??
|
|
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
|
|
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()
|
|
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;
|