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/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: (
|
|
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
|
-
|
|
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(
|
|
2251
|
+
await g.checkout(target);
|
|
2197
2252
|
try {
|
|
2198
2253
|
await g.merge(["--no-ff", branch]);
|
|
2199
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
3153
|
-
|
|
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 ${
|
|
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 ??
|
|
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
|
|
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.
|
|
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",
|