runcastle 1.4.0 → 1.4.2

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
@@ -908,14 +908,32 @@ var init_src = __esm(() => {
908
908
  });
909
909
 
910
910
  // ../core/src/paths.ts
911
- import { homedir } from "os";
911
+ import { homedir, userInfo } from "os";
912
912
  import { join, resolve } from "path";
913
913
  function prodDataDir() {
914
914
  return join(homedir(), ".runcastle");
915
915
  }
916
916
  function dataDir() {
917
917
  const override = process.env.RUNCASTLE_DATA_DIR;
918
- return override ? resolve(override) : prodDataDir();
918
+ const dir = override ? resolve(override) : prodDataDir();
919
+ if (process.env.VITEST && sameDataDir(dir, join(realHomeDir(), ".runcastle"))) {
920
+ throw new Error(`dataDir() resolved to the real ${dir} under Vitest; a test must run against a temp data dir (useDataDir / withTempDataDir)`);
921
+ }
922
+ return dir;
923
+ }
924
+ function realHomeDir() {
925
+ try {
926
+ return userInfo().homedir;
927
+ } catch {
928
+ return homedir();
929
+ }
930
+ }
931
+ function sameDataDir(a, b) {
932
+ const canon = (p) => {
933
+ const abs = resolve(p).replace(/[\\/]+$/, "");
934
+ return process.platform === "win32" ? abs.toLowerCase().replace(/\//g, "\\") : abs;
935
+ };
936
+ return canon(a) === canon(b);
919
937
  }
920
938
  function dbPath() {
921
939
  return join(dataDir(), "runcastle.db");
@@ -956,8 +974,11 @@ function projectNotePath(noteId) {
956
974
  function attachmentRelPath(noteId) {
957
975
  return `${ATTACHMENTS_DIR}/${noteId}.png`;
958
976
  }
977
+ function worktreesRoot() {
978
+ return join(dataDir(), "worktrees");
979
+ }
959
980
  function projectWorktreesDir(projectId) {
960
- return join(dataDir(), "worktrees", projectId);
981
+ return join(worktreesRoot(), projectId);
961
982
  }
962
983
  function worktreeDir(projectId, slug) {
963
984
  return join(projectWorktreesDir(projectId), slug);
@@ -3031,6 +3052,7 @@ import {
3031
3052
  mkdtempSync,
3032
3053
  readFileSync as readFileSync3,
3033
3054
  realpathSync,
3055
+ rmdirSync,
3034
3056
  rmSync,
3035
3057
  writeFileSync
3036
3058
  } from "fs";
@@ -3268,7 +3290,21 @@ async function reclaimTalkBranch(g, repoPath, worktreePath, branch) {
3268
3290
  throw new InvalidInputError(`${branch} is checked out at ${strays[0] ?? repoPath}; end the test drive or remove that worktree`);
3269
3291
  }
3270
3292
  async function addWorktree(g, worktreePath, branch, label) {
3271
- mkdirSync2(dirname2(worktreePath), { recursive: true });
3293
+ const parent = dirname2(worktreePath);
3294
+ mkdirSync2(parent, { recursive: true });
3295
+ try {
3296
+ return await addWorktreeOrRepair(g, worktreePath, branch, label);
3297
+ } catch (e) {
3298
+ removeIfEmpty(parent);
3299
+ throw e;
3300
+ }
3301
+ }
3302
+ function removeIfEmpty(dir) {
3303
+ try {
3304
+ rmdirSync(dir);
3305
+ } catch {}
3306
+ }
3307
+ async function addWorktreeOrRepair(g, worktreePath, branch, label) {
3272
3308
  try {
3273
3309
  await g.raw(["worktree", "add", worktreePath, branch]);
3274
3310
  return worktreePath;
@@ -3523,14 +3559,18 @@ async function unexcludePath(repoPath, pattern) {
3523
3559
  writeFileSync(file, next);
3524
3560
  }
3525
3561
  async function cleanupBurnWorktree(repoPath, branch, opts = {}) {
3526
- const path = burnWorktreePath(repoPath, branch);
3562
+ return discardWorktree(repoPath, burnWorktreePath(repoPath, branch), opts);
3563
+ }
3564
+ async function discardWorktree(repoPath, path, opts = {}) {
3527
3565
  const attempts = Math.max(1, opts.attempts ?? 3);
3528
3566
  const delayMs = opts.delayMs ?? 750;
3529
- const g = git(repoPath);
3530
- for (let attempt = 1;attempt <= attempts && existsSync6(path); attempt++) {
3567
+ const gitQuietly = async (args) => {
3531
3568
  try {
3532
- await g.raw(["worktree", "remove", "--force", path]);
3569
+ await git(repoPath).raw(args);
3533
3570
  } catch {}
3571
+ };
3572
+ for (let attempt = 1;attempt <= attempts && existsSync6(path); attempt++) {
3573
+ await gitQuietly(["worktree", "remove", "--force", path]);
3534
3574
  if (existsSync6(path) && attempt < attempts)
3535
3575
  await sleep2(delayMs);
3536
3576
  }
@@ -3539,9 +3579,7 @@ async function cleanupBurnWorktree(repoPath, branch, opts = {}) {
3539
3579
  rmSync(path, { recursive: true, force: true });
3540
3580
  } catch {}
3541
3581
  }
3542
- try {
3543
- await g.raw(["worktree", "prune"]);
3544
- } catch {}
3582
+ await gitQuietly(["worktree", "prune"]);
3545
3583
  return !existsSync6(path);
3546
3584
  }
3547
3585
  function sleep2(ms) {
@@ -3812,19 +3850,19 @@ async function cleanupTempBranches(repoPath) {
3812
3850
  return { deleted, kept };
3813
3851
  }
3814
3852
  async function removeTalkWorktree(repoPath, worktreePath) {
3815
- const g = git(repoPath);
3816
- try {
3817
- await g.raw(["worktree", "remove", "--force", worktreePath]);
3818
- } catch {
3819
- try {
3820
- rmSync(worktreePath, { recursive: true, force: true });
3821
- } catch {}
3853
+ if (!await discardWorktree(repoPath, worktreePath, { attempts: 1 })) {
3854
+ throw new InvalidInputError(`could not remove talk worktree at ${worktreePath} \u2014 a file may be locked; ` + "close anything using it and retry the delete");
3822
3855
  }
3856
+ }
3857
+ async function worktreeState(path) {
3858
+ if (!existsSync6(path))
3859
+ return "missing";
3860
+ if (!existsSync6(join6(path, ".git")))
3861
+ return "not-a-checkout";
3823
3862
  try {
3824
- await g.raw(["worktree", "prune"]);
3825
- } catch {}
3826
- if (existsSync6(worktreePath)) {
3827
- throw new InvalidInputError(`could not remove talk worktree at ${worktreePath} \u2014 a file may be locked; ` + "close anything using it and retry the delete");
3863
+ return (await git(path).raw(["status", "--porcelain"])).trim() === "" ? "clean" : "dirty";
3864
+ } catch {
3865
+ return "unreadable";
3828
3866
  }
3829
3867
  }
3830
3868
  async function deleteFeatureBranches(repoPath, slug) {
@@ -4041,6 +4079,7 @@ async function testDrive(ctx, project, feature, action, purpose = "human") {
4041
4079
  testDriveState = {
4042
4080
  kind: "feature",
4043
4081
  purpose,
4082
+ projectId: project.id,
4044
4083
  featureId: feature.id,
4045
4084
  branch,
4046
4085
  previousBranch,
@@ -4444,7 +4483,7 @@ async function detectDbDrift(ctx, project, feature, previousBranch, driveBranch)
4444
4483
  }
4445
4484
  async function mergeFeature(project, feature) {
4446
4485
  const target = featureBase(feature);
4447
- if (testDriveState) {
4486
+ if (testDriveState?.projectId === project.id) {
4448
4487
  throw new GateError("Cannot merge while a test drive is active \u2014 stop it first");
4449
4488
  }
4450
4489
  const g = git(project.repoPath);
@@ -6948,6 +6987,15 @@ function staleImage(detail, unchecked = "") {
6948
6987
  fix: "Open Settings \u2192 Burns (Rebuild image)."
6949
6988
  };
6950
6989
  }
6990
+ function imageUncheckable() {
6991
+ return {
6992
+ ...IMAGE_ROW,
6993
+ status: "unhealthy",
6994
+ severity: "error",
6995
+ detail: "cannot inspect the image while the container runtime is not responding",
6996
+ fix: "Get the container runtime answering first (see the container runtime row), then re-run doctor."
6997
+ };
6998
+ }
6951
6999
  async function runDoctor(env) {
6952
7000
  const { exec } = env;
6953
7001
  const processEnv = env.env ?? process.env;
@@ -6968,7 +7016,8 @@ async function runDoctor(env) {
6968
7016
  severity: required.has(runtime) ? "error" : "info"
6969
7017
  }));
6970
7018
  }
6971
- results.push(await gitIdentityProbe(exec, env.cwd), await containerRuntimeProbe(exec), await sandcastleImageProbe({
7019
+ const containerRuntime = await containerRuntimeProbe(exec);
7020
+ results.push(await gitIdentityProbe(exec, env.cwd), containerRuntime, RUNTIME_DOWN.has(containerRuntime.status) ? imageUncheckable() : await sandcastleImageProbe({
6972
7021
  exec,
6973
7022
  imageName,
6974
7023
  burnerDockerfile,
@@ -6989,7 +7038,7 @@ function exitCodeFor(report, mode) {
6989
7038
  return report.tier1Ok ? 0 : 1;
6990
7039
  return report.ok ? 0 : 1;
6991
7040
  }
6992
- var RUNTIME_SPECS, IMAGE_ROW;
7041
+ var RUNTIME_SPECS, IMAGE_ROW, RUNTIME_DOWN;
6993
7042
  var init_doctor = __esm(() => {
6994
7043
  init_src();
6995
7044
  init_asset_paths();
@@ -7028,6 +7077,7 @@ var init_doctor = __esm(() => {
7028
7077
  }
7029
7078
  };
7030
7079
  IMAGE_ROW = { id: "sandcastle-image", label: "Sandcastle container image", tier: 2 };
7080
+ RUNTIME_DOWN = new Set(["daemon-dead", "machine-stopped"]);
7031
7081
  });
7032
7082
 
7033
7083
  // src/launcher/runtimes/codex.ts
@@ -7604,9 +7654,111 @@ var init_runtime = __esm(() => {
7604
7654
  init_paths();
7605
7655
  });
7606
7656
 
7657
+ // src/services/feature-worktrees.ts
7658
+ import { readdirSync as readdirSync3, rmSync as rmSync2 } from "fs";
7659
+ import { join as join14 } from "path";
7660
+ async function retireShippedWorktree(ctx, project, feature) {
7661
+ await retireWorktree(ctx, project, feature, { requireClean: false });
7662
+ }
7663
+ async function retireArchivedWorktree(ctx, project, feature) {
7664
+ await retireWorktree(ctx, project, feature, { requireClean: true });
7665
+ }
7666
+ async function retireWorktree(ctx, project, feature, opts) {
7667
+ const path = worktreeDir(project.id, feature.slug);
7668
+ const state = await worktreeState(path);
7669
+ if (state === "missing")
7670
+ return;
7671
+ const kept = keptReason(ctx, feature, state, opts);
7672
+ if (kept) {
7673
+ emit(ctx, feature.id, {
7674
+ type: "feature.worktree_kept",
7675
+ message: `talk worktree kept \u2014 ${kept}`,
7676
+ data: { path, reason: kept }
7677
+ });
7678
+ return;
7679
+ }
7680
+ if (await discardWorktree(project.repoPath, path)) {
7681
+ emit(ctx, feature.id, {
7682
+ type: "feature.worktree_removed",
7683
+ message: "talk worktree removed",
7684
+ data: { path }
7685
+ });
7686
+ } else {
7687
+ emit(ctx, feature.id, {
7688
+ type: "feature.worktree_remove_failed",
7689
+ message: `could not remove the talk worktree at ${path} \u2014 a file may be locked`,
7690
+ data: { path }
7691
+ });
7692
+ }
7693
+ }
7694
+ function keptReason(ctx, feature, state, opts) {
7695
+ if (activeSessionsForFeature(ctx, feature.id).length > 0)
7696
+ return "a session is live on it";
7697
+ if (hasActiveRun(ctx, feature.id))
7698
+ return "a run is in progress";
7699
+ if (opts.requireClean && state !== "clean")
7700
+ return "uncommitted changes";
7701
+ return;
7702
+ }
7703
+ async function sweepFeatureWorktrees(ctx) {
7704
+ const sweep = { removed: 0, kept: 0, failed: 0, orphanProjects: 0 };
7705
+ const root = worktreesRoot();
7706
+ const projectsById = new Map(allProjects(ctx).map((p) => [p.id, p]));
7707
+ const statusBySlug = new Map(ctx.db.select({ projectId: features.projectId, slug: features.slug, status: features.status }).from(features).all().map((f) => [`${f.projectId}/${f.slug}`, f.status]));
7708
+ for (const projectId of subdirs(root)) {
7709
+ const project = projectsById.get(projectId);
7710
+ if (!project) {
7711
+ try {
7712
+ rmSync2(join14(root, projectId), { recursive: true, force: true });
7713
+ sweep.orphanProjects++;
7714
+ } catch {
7715
+ sweep.failed++;
7716
+ }
7717
+ continue;
7718
+ }
7719
+ for (const slug of subdirs(join14(root, projectId))) {
7720
+ if (slug.startsWith("_"))
7721
+ continue;
7722
+ const status = statusBySlug.get(`${projectId}/${slug}`);
7723
+ if (status === "active" || status === "draft")
7724
+ continue;
7725
+ const path = worktreeDir(projectId, slug);
7726
+ const state = await worktreeState(path);
7727
+ if (!sweepable(status, state)) {
7728
+ sweep.kept++;
7729
+ } else if (await discardWorktree(project.repoPath, path, { attempts: 1 })) {
7730
+ sweep.removed++;
7731
+ } else {
7732
+ sweep.failed++;
7733
+ }
7734
+ }
7735
+ }
7736
+ return sweep;
7737
+ }
7738
+ function sweepable(status, state) {
7739
+ if (status === "shipped")
7740
+ return true;
7741
+ return state === "clean" || state === "not-a-checkout";
7742
+ }
7743
+ function subdirs(dir) {
7744
+ try {
7745
+ return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
7746
+ } catch {
7747
+ return [];
7748
+ }
7749
+ }
7750
+ var init_feature_worktrees = __esm(() => {
7751
+ init_paths();
7752
+ init_schema();
7753
+ init_sessions();
7754
+ init_events();
7755
+ init_git();
7756
+ init_repo();
7757
+ });
7758
+
7607
7759
  // src/services/knowledge.ts
7608
- import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync8, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
7609
- import { join as join14, relative as relative2, resolve as resolve8, sep } from "path";
7760
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync8, readdirSync as readdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
7761
+ import { join as join15, relative as relative2, resolve as resolve8, sep } from "path";
7610
7762
  function scaffoldDocs(ctx, feature, opts = {}) {
7611
7763
  const project = projectForFeature(ctx, feature);
7612
7764
  const dir = featureDocsDir(project, feature);
@@ -7659,7 +7811,7 @@ function mapDocBody(feature, seed) {
7659
7811
  `);
7660
7812
  }
7661
7813
  function readCharter(repoRoot) {
7662
- const path = join14(repoRoot, CHARTER_FILE);
7814
+ const path = join15(repoRoot, CHARTER_FILE);
7663
7815
  if (!existsSync12(path))
7664
7816
  return;
7665
7817
  try {
@@ -7669,16 +7821,16 @@ function readCharter(repoRoot) {
7669
7821
  }
7670
7822
  }
7671
7823
  function listLiveAdrs(repoRoot) {
7672
- const dir = join14(repoRoot, ADR_DIR_REL);
7824
+ const dir = join15(repoRoot, ADR_DIR_REL);
7673
7825
  if (!existsSync12(dir))
7674
7826
  return [];
7675
7827
  const docs2 = [];
7676
- for (const entry of readdirSync3(dir, { withFileTypes: true })) {
7828
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
7677
7829
  if (!entry.isFile() || !entry.name.endsWith(".md"))
7678
7830
  continue;
7679
7831
  let content;
7680
7832
  try {
7681
- content = readFileSync8(join14(dir, entry.name), "utf8");
7833
+ content = readFileSync8(join15(dir, entry.name), "utf8");
7682
7834
  } catch {
7683
7835
  continue;
7684
7836
  }
@@ -7693,8 +7845,8 @@ function listDocs(ctx, feature) {
7693
7845
  const dir = featureDocsDir(project, feature);
7694
7846
  if (!existsSync12(dir))
7695
7847
  return [];
7696
- return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => {
7697
- const path = join14(dir, e.name);
7848
+ return readdirSync4(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => {
7849
+ const path = join15(dir, e.name);
7698
7850
  return { relPath: e.name, title: readTitle(path, e.name), ...writtenAt(path) };
7699
7851
  }).sort((a, b) => a.relPath.localeCompare(b.relPath));
7700
7852
  }
@@ -9174,7 +9326,7 @@ var Effect_exports, isFunction = (input) => typeof input === "function", dual =
9174
9326
  next4 = b2;
9175
9327
  }
9176
9328
  return out;
9177
- }, getEquivalence2, dedupeWith, dedupe = (self) => dedupeWith(self, equivalence()), join15, TypeId4, emptyArray, getEquivalence3 = (isEquivalent) => make((self, that) => self.length === that.length && toReadonlyArray(self).every((value5, i) => isEquivalent(value5, unsafeGet2(that, i)))), _equivalence3, ChunkProto, makeChunk = (backing) => {
9329
+ }, getEquivalence2, dedupeWith, dedupe = (self) => dedupeWith(self, equivalence()), join16, TypeId4, emptyArray, getEquivalence3 = (isEquivalent) => make((self, that) => self.length === that.length && toReadonlyArray(self).every((value5, i) => isEquivalent(value5, unsafeGet2(that, i)))), _equivalence3, ChunkProto, makeChunk = (backing) => {
9178
9330
  const chunk4 = Object.create(ChunkProto);
9179
9331
  chunk4.backing = backing;
9180
9332
  switch (backing._tag) {
@@ -10873,7 +11025,7 @@ ${prefix}}`;
10873
11025
  Object.defineProperty(error3, "toString", {
10874
11026
  enumerable: false,
10875
11027
  value() {
10876
- const path2 = pipe(this.path, join15(options.pathDelim));
11028
+ const path2 = pipe(this.path, join16(options.pathDelim));
10877
11029
  return `(Invalid data at ${path2}: "${this.message}")`;
10878
11030
  }
10879
11031
  });
@@ -10888,7 +11040,7 @@ ${prefix}}`;
10888
11040
  Object.defineProperty(error3, "toString", {
10889
11041
  enumerable: false,
10890
11042
  value() {
10891
- const path2 = pipe(this.path, join15(options.pathDelim));
11043
+ const path2 = pipe(this.path, join16(options.pathDelim));
10892
11044
  return `(Missing data at ${path2}: "${this.message}")`;
10893
11045
  }
10894
11046
  });
@@ -10904,7 +11056,7 @@ ${prefix}}`;
10904
11056
  Object.defineProperty(error3, "toString", {
10905
11057
  enumerable: false,
10906
11058
  value() {
10907
- const path2 = pipe(this.path, join15(options.pathDelim));
11059
+ const path2 = pipe(this.path, join16(options.pathDelim));
10908
11060
  return `(Source unavailable at ${path2}: "${this.message}")`;
10909
11061
  }
10910
11062
  });
@@ -10919,7 +11071,7 @@ ${prefix}}`;
10919
11071
  Object.defineProperty(error3, "toString", {
10920
11072
  enumerable: false,
10921
11073
  value() {
10922
- const path2 = pipe(this.path, join15(options.pathDelim));
11074
+ const path2 = pipe(this.path, join16(options.pathDelim));
10923
11075
  return `(Unsupported operation at ${path2}: "${this.message}")`;
10924
11076
  }
10925
11077
  });
@@ -10949,7 +11101,7 @@ ${prefix}}`;
10949
11101
  pathDelim: "_",
10950
11102
  seqDelim: ","
10951
11103
  }, options);
10952
- const makePathString = (path) => pipe(path, join15(pathDelim));
11104
+ const makePathString = (path) => pipe(path, join16(pathDelim));
10953
11105
  const unmakePathString = (pathString) => pathString.split(pathDelim);
10954
11106
  const getEnv = () => typeof process !== "undefined" && ("env" in process) && typeof process.env === "object" ? process.env : {};
10955
11107
  const load = (path, primitive2, split2 = true) => {
@@ -11070,7 +11222,7 @@ ${prefix}}`;
11070
11222
  return fail2(right3.left);
11071
11223
  }
11072
11224
  if (isRight2(left3) && isRight2(right3)) {
11073
- const path = pipe(prefix, join15("."));
11225
+ const path = pipe(prefix, join16("."));
11074
11226
  const fail19 = fromFlatLoopFail(prefix, path);
11075
11227
  const [lefts, rights] = extend(fail19, fail19, pipe(left3.right, map3(right2)), pipe(right3.right, map3(right2)));
11076
11228
  return pipe(lefts, zip(rights), forEachSequential(([left4, right4]) => pipe(zip2(left4, right4), map10(([left5, right5]) => op.zip(left5, right5)))));
@@ -20908,7 +21060,7 @@ var init_chunk_VOG34SRF = __esm(() => {
20908
21060
  }
20909
21061
  return [];
20910
21062
  });
20911
- join15 = /* @__PURE__ */ dual(2, (self, sep2) => fromIterable2(self).join(sep2));
21063
+ join16 = /* @__PURE__ */ dual(2, (self, sep2) => fromIterable2(self).join(sep2));
20912
21064
  TypeId4 = /* @__PURE__ */ Symbol.for("effect/Chunk");
20913
21065
  emptyArray = [];
20914
21066
  _equivalence3 = /* @__PURE__ */ getEquivalence3(equals);
@@ -54662,7 +54814,7 @@ var init_chunk_62WN33RK = __esm(() => {
54662
54814
  // ../../node_modules/.bun/@ai-hero+sandcastle@0.12.0/node_modules/@ai-hero/sandcastle/dist/index.js
54663
54815
  import { createRequire as createRequire8 } from "module";
54664
54816
  import { mkdirSync as mkdirSync7, appendFileSync } from "fs";
54665
- import path, { join as join17, posix as posix4, dirname as dirname8, relative as relative3 } from "path";
54817
+ import path, { join as join18, posix as posix4, dirname as dirname8, relative as relative3 } from "path";
54666
54818
  import { styleText as styleText2 } from "util";
54667
54819
  import * as clack2 from "@clack/prompts";
54668
54820
  import { readdir as readdir2, access as access3, readFile as readFile4, mkdir as mkdir3, writeFile as writeFile3, rm as rm3 } from "fs/promises";
@@ -54731,7 +54883,7 @@ async function run6(options) {
54731
54883
  const targetBranch = effectiveBranchType === "merge-to-head" ? currentHostBranch : undefined;
54732
54884
  const resolvedLogging = options.logging ?? {
54733
54885
  type: "file",
54734
- path: join17(hostRepoDir, ".sandcastle", "logs", buildLogFilename(resolvedBranch, targetBranch, options.name))
54886
+ path: join18(hostRepoDir, ".sandcastle", "logs", buildLogFilename(resolvedBranch, targetBranch, options.name))
54735
54887
  };
54736
54888
  const displayLayer = resolvedLogging.type === "file" ? (() => {
54737
54889
  printFileDisplayStartup({
@@ -55365,7 +55517,7 @@ ${errorDetail}`
55365
55517
  }
55366
55518
  return vars;
55367
55519
  }), resolveEnv = (repoDir) => Effect_exports.gen(function* () {
55368
- const sandcastleEnv = yield* parseEnvFile2(join17(repoDir, ".sandcastle", ".env"));
55520
+ const sandcastleEnv = yield* parseEnvFile2(join18(repoDir, ".sandcastle", ".env"));
55369
55521
  const result = {};
55370
55522
  for (const key of Object.keys(sandcastleEnv)) {
55371
55523
  const value = sandcastleEnv[key] || process.env[key];
@@ -55587,11 +55739,11 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
55587
55739
  const normalized = isRoot ? cwd : cwd.replace(/[\\/]+$/, "");
55588
55740
  return normalized.replace(/^([A-Za-z]):/, "$1").replace(/[\\/]/g, "-");
55589
55741
  }, claudeHostSessionPath = (cwd, id3, projectsDir) => {
55590
- const base = projectsDir ?? join17(process.env.HOME ?? "~", ".claude", "projects");
55591
- return join17(base, encodeProjectPath(cwd), `${id3}.jsonl`);
55742
+ const base = projectsDir ?? join18(process.env.HOME ?? "~", ".claude", "projects");
55743
+ return join18(base, encodeProjectPath(cwd), `${id3}.jsonl`);
55592
55744
  }, claudeSandboxSessionPath = (cwd, id3, projectsDir) => posix4.join(projectsDir, encodeProjectPath(cwd), `${id3}.jsonl`), claudeSubagentsDirInSandbox = (cwd, id3, projectsDir) => posix4.join(projectsDir, encodeProjectPath(cwd), id3, "subagents"), claudeSubagentsDirOnHost = (cwd, id3, projectsDir) => {
55593
- const base = projectsDir ?? join17(process.env.HOME ?? "~", ".claude", "projects");
55594
- return join17(base, encodeProjectPath(cwd), id3, "subagents");
55745
+ const base = projectsDir ?? join18(process.env.HOME ?? "~", ".claude", "projects");
55746
+ return join18(base, encodeProjectPath(cwd), id3, "subagents");
55595
55747
  }, listClaudeSubagentSessionsInSandbox = async (cwd, id3, handle, sandboxProjectsDir) => {
55596
55748
  const dir = claudeSubagentsDirInSandbox(cwd, id3, sandboxProjectsDir);
55597
55749
  const result = await handle.exec(`find ${JSON.stringify(dir)} -type f -name ${JSON.stringify("agent-*.jsonl")} 2>/dev/null`);
@@ -55603,7 +55755,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
55603
55755
  return stdout.split(`
55604
55756
  `).filter((line) => line !== "");
55605
55757
  }, findClaudeSessionOnHost = async (id3, projectsDir) => {
55606
- const root = projectsDir ?? join17(process.env.HOME ?? "~", ".claude", "projects");
55758
+ const root = projectsDir ?? join18(process.env.HOME ?? "~", ".claude", "projects");
55607
55759
  let entries;
55608
55760
  try {
55609
55761
  entries = await readdir2(root, { withFileTypes: true });
@@ -55613,7 +55765,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
55613
55765
  for (const entry of entries) {
55614
55766
  if (!entry.isDirectory())
55615
55767
  continue;
55616
- const candidate = join17(root, entry.name, `${id3}.jsonl`);
55768
+ const candidate = join18(root, entry.name, `${id3}.jsonl`);
55617
55769
  if (await pathExists(candidate)) {
55618
55770
  return { path: candidate, searchedRoot: root };
55619
55771
  }
@@ -55649,7 +55801,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
55649
55801
  return;
55650
55802
  }
55651
55803
  for (const entry of entries) {
55652
- const child = join17(dir, entry.name);
55804
+ const child = join18(dir, entry.name);
55653
55805
  if (entry.isFile() && isCodexSessionFilename(entry.name, id3)) {
55654
55806
  return child;
55655
55807
  }
@@ -55663,11 +55815,11 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
55663
55815
  };
55664
55816
  return visit(rootDir);
55665
55817
  }, findCodexSessionOnHost = async (id3, sessionsDir) => {
55666
- const root = sessionsDir ?? join17(process.env.HOME ?? "~", ".codex", "sessions");
55818
+ const root = sessionsDir ?? join18(process.env.HOME ?? "~", ".codex", "sessions");
55667
55819
  const path2 = await findCodexSessionPath(root, id3);
55668
55820
  return { path: path2, searchedRoot: root };
55669
55821
  }, locateCodexHostSession = async (id3, sessionsDir) => {
55670
- const root = sessionsDir ?? join17(process.env.HOME ?? "~", ".codex", "sessions");
55822
+ const root = sessionsDir ?? join18(process.env.HOME ?? "~", ".codex", "sessions");
55671
55823
  const path2 = await findCodexSessionPath(root, id3);
55672
55824
  if (!path2)
55673
55825
  throw new Error(`session ${id3} not found in ${root}`);
@@ -55743,7 +55895,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
55743
55895
  } catch {}
55744
55896
  return [];
55745
55897
  }, CURSOR_PRINT_PROMPT_MAX_BYTES, readSandboxFile = async (handle, sandboxPath, tag) => {
55746
- const tmpPath = join17(tmpdir4(), `sandcastle-${tag}-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`);
55898
+ const tmpPath = join18(tmpdir4(), `sandcastle-${tag}-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`);
55747
55899
  await handle.copyFileOut(sandboxPath, tmpPath);
55748
55900
  try {
55749
55901
  return await readFile4(tmpPath, "utf-8");
@@ -55751,7 +55903,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
55751
55903
  await rm3(tmpPath, { force: true }).catch(() => {});
55752
55904
  }
55753
55905
  }, writeSandboxFile = async (handle, sandboxPath, content, tag) => {
55754
- const tmpPath = join17(tmpdir4(), `sandcastle-${tag}-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`);
55906
+ const tmpPath = join18(tmpdir4(), `sandcastle-${tag}-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl`);
55755
55907
  await writeFile3(tmpPath, content);
55756
55908
  try {
55757
55909
  await handle.exec(`mkdir -p ${JSON.stringify(posix4.dirname(sandboxPath))}`);
@@ -55801,7 +55953,7 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
55801
55953
  sourcePath: sandboxSubagentPath,
55802
55954
  fromCwd: sandboxCwd,
55803
55955
  toCwd: hostCwd,
55804
- destPath: join17(hostSubagentsDir, posix4.basename(sandboxSubagentPath)),
55956
+ destPath: join18(hostSubagentsDir, posix4.basename(sandboxSubagentPath)),
55805
55957
  tag: "claude-sub"
55806
55958
  });
55807
55959
  } catch (err) {
@@ -55838,8 +55990,8 @@ Emit only a corrected <${error.tag}> block. Do not change files or run commands.
55838
55990
  const located = await locateCodexSandboxSession(sessionId, handle, sandboxSessionsDir);
55839
55991
  const jsonl = await readSandboxFile(handle, located.path, "codex-cap");
55840
55992
  const rewritten = transferCodexSession(jsonl, sandboxCwd, hostCwd);
55841
- const root = hostSessionsDir ?? join17(process.env.HOME ?? "~", ".codex", "sessions");
55842
- const target = join17(root, located.relativePath);
55993
+ const root = hostSessionsDir ?? join18(process.env.HOME ?? "~", ".codex", "sessions");
55994
+ const target = join18(root, located.relativePath);
55843
55995
  await mkdir3(dirname8(target), { recursive: true });
55844
55996
  await writeFile3(target, rewritten);
55845
55997
  capturedPaths.set(sessionId, target);
@@ -56478,8 +56630,8 @@ var init_settings = __esm(() => {
56478
56630
  });
56479
56631
 
56480
56632
  // src/services/setup.ts
56481
- import { copyFileSync as copyFileSync2, existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync10, readdirSync as readdirSync4, writeFileSync as writeFileSync7 } from "fs";
56482
- import { dirname as dirname10, join as join18 } from "path";
56633
+ import { copyFileSync as copyFileSync2, existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync10, readdirSync as readdirSync5, writeFileSync as writeFileSync7 } from "fs";
56634
+ import { dirname as dirname10, join as join19 } from "path";
56483
56635
  async function writeGitIdentity(exec3, identity4) {
56484
56636
  const name = identity4.name.trim();
56485
56637
  const email = identity4.email.trim();
@@ -56589,10 +56741,10 @@ function terminalSpec(kind) {
56589
56741
  throw new InvalidInputError(`no terminal command for ${kind}`);
56590
56742
  }
56591
56743
  function refreshSandcastleConfig(templateDir, targetDir) {
56592
- const dir = join18(targetDir, ".sandcastle");
56744
+ const dir = join19(targetDir, ".sandcastle");
56593
56745
  mkdirSync9(dir, { recursive: true });
56594
- for (const name of readdirSync4(templateDir)) {
56595
- copyFileSync2(join18(templateDir, name), join18(dir, name));
56746
+ for (const name of readdirSync5(templateDir)) {
56747
+ copyFileSync2(join19(templateDir, name), join19(dir, name));
56596
56748
  }
56597
56749
  return dir;
56598
56750
  }
@@ -56668,11 +56820,33 @@ var init_setup = __esm(() => {
56668
56820
  // src/doctor/system-exec.ts
56669
56821
  import { spawn as spawn7 } from "child_process";
56670
56822
  function createSystemExec(opts = {}) {
56823
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
56671
56824
  return (command, args2) => new Promise((resolve10) => {
56672
56825
  const { file, args: spawnArgs } = resolveSpawnTarget(command, args2);
56673
56826
  let stdout = "";
56674
56827
  let stderr = "";
56828
+ let settled = false;
56829
+ const settle = (outcome) => {
56830
+ if (settled)
56831
+ return;
56832
+ settled = true;
56833
+ clearTimeout(timer);
56834
+ resolve10(outcome);
56835
+ };
56675
56836
  const child = spawn7(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
56837
+ const timer = setTimeout(() => {
56838
+ if (child.pid !== undefined)
56839
+ killProcessTree(child.pid);
56840
+ const note2 = `${command} ${args2.join(" ")} timed out after ${Math.round(timeoutMs / 1000)}s`;
56841
+ settle({
56842
+ ok: true,
56843
+ code: null,
56844
+ stdout,
56845
+ stderr: stderr ? `${stderr}
56846
+ ${note2}` : note2,
56847
+ timedOut: true
56848
+ });
56849
+ }, timeoutMs);
56676
56850
  child.stdout?.on("data", (d) => {
56677
56851
  stdout += d.toString();
56678
56852
  });
@@ -56680,14 +56854,16 @@ function createSystemExec(opts = {}) {
56680
56854
  stderr += d.toString();
56681
56855
  });
56682
56856
  child.on("error", (err) => {
56683
- resolve10({ ok: false, code: null, stdout, stderr: stderr || String(err) });
56857
+ settle({ ok: false, code: null, stdout, stderr: stderr || String(err) });
56684
56858
  });
56685
56859
  child.on("close", (code) => {
56686
- resolve10({ ok: true, code: code ?? 0, stdout, stderr });
56860
+ settle({ ok: true, code: code ?? 0, stdout, stderr });
56687
56861
  });
56688
56862
  });
56689
56863
  }
56864
+ var DEFAULT_EXEC_TIMEOUT_MS = 60000;
56690
56865
  var init_system_exec = __esm(() => {
56866
+ init_kill_tree();
56691
56867
  init_resolve_executable();
56692
56868
  });
56693
56869
 
@@ -56879,13 +57055,13 @@ var init_no_sandbox = __esm(() => {
56879
57055
  // src/workflows/recorder-reap.ts
56880
57056
  import { spawn as spawn8 } from "child_process";
56881
57057
  import { closeSync, existsSync as existsSync16, openSync } from "fs";
56882
- import { delimiter, join as join19 } from "path";
57058
+ import { delimiter, join as join20 } from "path";
56883
57059
  function findOnPath(bin, env = process.env, platform = process.platform) {
56884
57060
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
56885
57061
  const suffixes = platform === "win32" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
56886
57062
  for (const dir of dirs) {
56887
57063
  for (const suffix of suffixes) {
56888
- const candidate = join19(dir, `${bin}${suffix}`);
57064
+ const candidate = join20(dir, `${bin}${suffix}`);
56889
57065
  if (existsSync16(candidate))
56890
57066
  return candidate;
56891
57067
  }
@@ -56983,9 +57159,9 @@ var init_recorder_reap = __esm(() => {
56983
57159
  });
56984
57160
 
56985
57161
  // src/workflows/review-ticket.ts
56986
- import { existsSync as existsSync17, mkdirSync as mkdirSync10, readFileSync as readFileSync11, readdirSync as readdirSync5, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
57162
+ import { existsSync as existsSync17, mkdirSync as mkdirSync10, readFileSync as readFileSync11, readdirSync as readdirSync6, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "fs";
56987
57163
  import { spawnSync } from "child_process";
56988
- import { basename as basename3, dirname as dirname11, join as join20 } from "path";
57164
+ import { basename as basename3, dirname as dirname11, join as join21 } from "path";
56989
57165
  function reviewTemplatePath(ticket) {
56990
57166
  return burnerAssetPath(ticket?.passKind === "verification" ? "verify-fixes.md" : "review-ticket.md");
56991
57167
  }
@@ -56993,18 +57169,34 @@ function renderReviewPrompt(templateOrTicket, values) {
56993
57169
  const template = typeof templateOrTicket === "string" ? templateOrTicket : readFileSync11(reviewTemplatePath(templateOrTicket), "utf8");
56994
57170
  return renderTemplate(template, values);
56995
57171
  }
56996
- function executableIsHealthy(path2) {
56997
- if (!path2)
56998
- return false;
56999
- const result = spawnSync(path2, ["--version"], { timeout: 3000, stdio: "ignore" });
57000
- return result.status === 0 && !result.error;
57172
+ function probeExecutable(path2, spawn9) {
57173
+ const started = performance.now();
57174
+ const result = spawn9(path2);
57175
+ const elapsedMs = Math.round(performance.now() - started);
57176
+ const error = result.error ? result.error.code ?? result.error.message : undefined;
57177
+ return {
57178
+ ok: result.status === 0 && !result.error,
57179
+ path: path2,
57180
+ status: result.status,
57181
+ signal: result.signal,
57182
+ ...error ? { error } : {},
57183
+ elapsedMs
57184
+ };
57001
57185
  }
57002
- function missingDrivePieces(browserPath, devCommand, browserHealthy, ffmpegPath) {
57186
+ function checkExecutableHealth(path2, spawn9 = spawnVersion) {
57187
+ const first = probeExecutable(path2, spawn9);
57188
+ return first.ok ? first : probeExecutable(path2, spawn9);
57189
+ }
57190
+ function describeHealthFailure(health) {
57191
+ const cause3 = health.error ?? (health.signal ? `killed by ${health.signal}` : `exited with status ${health.status}`);
57192
+ return `${health.path} --version: ${cause3} after ${health.elapsedMs}ms`;
57193
+ }
57194
+ function missingDrivePieces(browserPath, devCommand, browserFailure, ffmpegPath) {
57003
57195
  const missing = [];
57004
57196
  if (!browserPath) {
57005
57197
  missing.push(`\`${AGENT_BROWSER_BIN}\` is not on this machine's PATH, so there is no browser to walk the app with`);
57006
- } else if (!browserHealthy) {
57007
- missing.push(`\`${AGENT_BROWSER_BIN}\` is on PATH but failed its health check`);
57198
+ } else if (browserFailure) {
57199
+ missing.push(`\`${AGENT_BROWSER_BIN}\` is on PATH but failed its health check (${browserFailure})`);
57008
57200
  }
57009
57201
  if (!ffmpegPath)
57010
57202
  missing.push(`\`${FFMPEG_BIN}\` is not on this machine's PATH, so a drive cannot be recorded`);
@@ -57013,17 +57205,17 @@ function missingDrivePieces(browserPath, devCommand, browserHealthy, ffmpegPath)
57013
57205
  }
57014
57206
  return missing;
57015
57207
  }
57016
- function driveWithheldReason(browserPath, devCommand, browserHealthy, ffmpegPath) {
57017
- const missing = missingDrivePieces(browserPath, devCommand, browserHealthy, ffmpegPath);
57208
+ function driveWithheldReason(browserPath, devCommand, browserFailure, ffmpegPath) {
57209
+ const missing = missingDrivePieces(browserPath, devCommand, browserFailure, ffmpegPath);
57018
57210
  if (missing.length === 0)
57019
57211
  return;
57020
57212
  return `Drive was unavailable: ${missing.join(", and ")}.`;
57021
57213
  }
57022
- function buildDriveAvailability(browserPath, devCommand, inheritedMode, browserHealthy = true, ffmpegPath = browserPath) {
57214
+ function buildDriveAvailability(browserPath, devCommand, inheritedMode, browserFailure, ffmpegPath = browserPath) {
57023
57215
  if (inheritedMode === "gates") {
57024
57216
  return "Inherited mode: **Gates**. The pass being verified recorded Gates mode, so run Gates mode; do not choose Drive mode or call `review_drive`.";
57025
57217
  }
57026
- const missing = missingDrivePieces(browserPath, devCommand, browserHealthy, ffmpegPath);
57218
+ const missing = missingDrivePieces(browserPath, devCommand, browserFailure, ffmpegPath);
57027
57219
  if (inheritedMode === "drive" && missing.length === 0) {
57028
57220
  return "Inherited mode: **Drive**. The pass being verified recorded Drive mode, so run Drive mode and record the full tour; do not choose Gates mode.";
57029
57221
  }
@@ -57088,9 +57280,10 @@ function buildDriveInstructions(instructions) {
57088
57280
  ].join(`
57089
57281
  `);
57090
57282
  }
57091
- function buildGateNotes(config2) {
57092
- const commands = config2.verifyCommands?.trim();
57093
- const failures2 = config2.knownFailures?.trim();
57283
+ function buildGateNotes(config2, project3) {
57284
+ const prepared = resolvePreparedSettings(config2, project3);
57285
+ const commands = prepared.verifyCommands;
57286
+ const failures2 = prepared.knownFailures;
57094
57287
  const out = [];
57095
57288
  if (commands) {
57096
57289
  out.push("Run exactly these, once each \u2014 they are this project's own verify commands, so do not go looking for alternatives, add concurrency flags, or re-run one to re-read its output (redirect to a file and read that instead):", "", "```", commands, "```");
@@ -57115,7 +57308,7 @@ function prepareReviewDirectory(dir, options = {}) {
57115
57308
  if (!sibling.startsWith(stalePrefix))
57116
57309
  continue;
57117
57310
  try {
57118
- fs.remove(join20(parent, sibling), { recursive: true, force: true });
57311
+ fs.remove(join21(parent, sibling), { recursive: true, force: true });
57119
57312
  } catch {}
57120
57313
  }
57121
57314
  } catch {}
@@ -57138,14 +57331,14 @@ async function prepareReviewArtifactsDirectory(ticketId, dir = reviewDir(ticketI
57138
57331
  async function writeReviewArtifacts(ticket, runId, config2, recorderReap = reapRecorder) {
57139
57332
  const dir = reviewDir(ticket.id);
57140
57333
  await prepareReviewArtifactsDirectory(ticket.id, dir, { recorderReap });
57141
- const mcpConfigPath = join20(dir, "mcp.json");
57334
+ const mcpConfigPath = join21(dir, "mcp.json");
57142
57335
  const mcpConfig = renderRunMcpConfig(runId, config2);
57143
57336
  writeFileSync8(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
57144
57337
  return {
57145
57338
  dir,
57146
57339
  mcp: { path: mcpConfigPath, config: mcpConfig },
57147
- digestPath: join20(dir, "DIGEST.md"),
57148
- blockedPath: join20(dir, "BLOCKED.md"),
57340
+ digestPath: join21(dir, "DIGEST.md"),
57341
+ blockedPath: join21(dir, "BLOCKED.md"),
57149
57342
  walkthroughPath: reviewWalkthroughPath(ticket.id)
57150
57343
  };
57151
57344
  }
@@ -57177,8 +57370,11 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
57177
57370
  const inheritedMode = ticket.passKind === "verification" ? inheritedReviewMode(verifies?.reviewMode) : undefined;
57178
57371
  const browserPath = findOnPath(AGENT_BROWSER_BIN);
57179
57372
  const ffmpegPath = findOnPath(FFMPEG_BIN);
57180
- const browserHealthy = executableIsHealthy(browserPath);
57181
- const withheldReason = driveWithheldReason(browserPath, project3.devCommand, browserHealthy, ffmpegPath);
57373
+ const browserHealth = browserPath ? checkExecutableHealth(browserPath) : undefined;
57374
+ const browserFailure = browserHealth && !browserHealth.ok ? describeHealthFailure(browserHealth) : undefined;
57375
+ if (browserFailure)
57376
+ console.error(`[review] ${AGENT_BROWSER_BIN} failed its health check: ${browserFailure}`);
57377
+ const withheldReason = driveWithheldReason(browserPath, project3.devCommand, browserFailure, ffmpegPath);
57182
57378
  const offeredMode = inheritedMode === "gates" || withheldReason ? "gates" : "drive";
57183
57379
  const prompt = renderReviewPrompt(ticket, {
57184
57380
  TICKET_JSON: buildTicketJson(ticket),
@@ -57187,9 +57383,9 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
57187
57383
  LAP_DIGESTS: buildLapDigestsBlock(deps.lapDigests),
57188
57384
  FEATURE_BRANCH: feature.branch,
57189
57385
  BASE_BRANCH: feature.baseBranch,
57190
- DRIVE_AVAILABILITY: buildDriveAvailability(browserPath, project3.devCommand, inheritedMode, browserHealthy, ffmpegPath),
57386
+ DRIVE_AVAILABILITY: buildDriveAvailability(browserPath, project3.devCommand, inheritedMode, browserFailure, ffmpegPath),
57191
57387
  DRIVE_INSTRUCTIONS: buildDriveInstructions(project3.driveInstructions),
57192
- GATE_NOTES: buildGateNotes(deps.config),
57388
+ GATE_NOTES: buildGateNotes(deps.config, project3),
57193
57389
  DIGEST_PATH: artifacts.digestPath,
57194
57390
  BLOCKED_PATH: artifacts.blockedPath,
57195
57391
  WALKTHROUGH_PATH: artifacts.walkthroughPath,
@@ -57236,7 +57432,7 @@ async function reviewTicketOutcome(ctx, ticket, deps, timer) {
57236
57432
  maxIterations: deps.config.burnMaxIterations,
57237
57433
  logging: {
57238
57434
  type: "file",
57239
- path: join20(logsDir(), `review-${feature.id}-${ticket.seq}.log`),
57435
+ path: join21(logsDir(), `review-${feature.id}-${ticket.seq}.log`),
57240
57436
  onAgentStreamEvent: onStreamEvent
57241
57437
  }
57242
57438
  };
@@ -57307,8 +57503,9 @@ function couldNotReview(ticket, reason, digest) {
57307
57503
  **${headline}**` : `**${headline}**`
57308
57504
  };
57309
57505
  }
57310
- var FFMPEG_BIN = "ffmpeg", DECLARATION, REVIEW_DIRECTORY_FS;
57506
+ var FFMPEG_BIN = "ffmpeg", spawnVersion = (path2) => spawnSync(path2, ["--version"], { timeout: 3000, stdio: "ignore" }), DECLARATION, REVIEW_DIRECTORY_FS;
57311
57507
  var init_review_ticket = __esm(() => {
57508
+ init_src();
57312
57509
  init_paths();
57313
57510
  init_dist();
57314
57511
  init_no_sandbox();
@@ -57323,9 +57520,9 @@ var init_review_ticket = __esm(() => {
57323
57520
  DECLARATION = /(?:^|\n)REVIEW-MODE:\s*(drive|gates)\s*\nREVIEW-VERDICT:\s*(verified|unverified)\s*\nREVIEW-REASON:\s*([^\r\n]*)/i;
57324
57521
  REVIEW_DIRECTORY_FS = {
57325
57522
  mkdir: mkdirSync10,
57326
- readDir: readdirSync5,
57523
+ readDir: readdirSync6,
57327
57524
  rename: renameSync,
57328
- remove: rmSync2
57525
+ remove: rmSync3
57329
57526
  };
57330
57527
  });
57331
57528
 
@@ -57883,8 +58080,8 @@ var init_podman = __esm(() => {
57883
58080
  });
57884
58081
 
57885
58082
  // src/workflows/ticket-burner.ts
57886
- import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync12, readdirSync as readdirSync6, rmSync as rmSync3 } from "fs";
57887
- import { dirname as dirname12, join as join21, posix as posixPath, win32 as winPath } from "path";
58083
+ import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync12, readdirSync as readdirSync7, rmSync as rmSync4 } from "fs";
58084
+ import { dirname as dirname12, join as join23, posix as posixPath, win32 as winPath } from "path";
57888
58085
  import { fileURLToPath as fileURLToPath6 } from "url";
57889
58086
  function missingImageRuntimeMessage(runtime5, image) {
57890
58087
  const binary = RUNTIME_BINARY[runtime5];
@@ -58145,10 +58342,10 @@ _(The "${DROPPED_MAP_SECTIONS.join('" and "')}" sections are omitted here: they
58145
58342
  }
58146
58343
  function buildProjectStandards(repoPath) {
58147
58344
  const present = [];
58148
- if (existsSync18(join21(repoPath, AGENT_CONVENTIONS_FILE))) {
58345
+ if (existsSync18(join23(repoPath, AGENT_CONVENTIONS_FILE))) {
58149
58346
  present.push("`CLAUDE.md` \u2014 this repo's agent-facing conventions, and the highest authority on them");
58150
58347
  }
58151
- if (existsSync18(join21(repoPath, CHARTER_FILE))) {
58348
+ if (existsSync18(join23(repoPath, CHARTER_FILE))) {
58152
58349
  present.push(`\`${CHARTER_FILE}\` \u2014 the project charter: what this codebase is for`);
58153
58350
  }
58154
58351
  const adrs = listLiveAdrs(repoPath).map((a) => a.relPath);
@@ -58179,7 +58376,7 @@ function buildDriveNotes(settings) {
58179
58376
  add5("stop", settings.driveStopCommand);
58180
58377
  add5("dev server", settings.devCommand);
58181
58378
  add5("db reset", settings.dbResetCommand);
58182
- const hasDir = existsSync18(join21(settings.repoPath, ".runcastle"));
58379
+ const hasDir = existsSync18(join23(settings.repoPath, ".runcastle"));
58183
58380
  if (configured.length === 0 && !hasDir) {
58184
58381
  return [
58185
58382
  "This project has no test-drive machinery configured, so there is nothing here for you to keep in step \u2014 do not invent any.",
@@ -58399,14 +58596,14 @@ function parseSetupMarker(text3) {
58399
58596
  return { cold: m[1] === "1", syncMs: Number(m[2]), installMs: Number(m[3]) };
58400
58597
  }
58401
58598
  function takeSetupMarker(worktreePath) {
58402
- const file = join21(worktreePath, SETUP_MARKER_FILE);
58599
+ const file = join23(worktreePath, SETUP_MARKER_FILE);
58403
58600
  let text3;
58404
58601
  try {
58405
58602
  text3 = readFileSync12(file, "utf8");
58406
58603
  } catch {
58407
58604
  return;
58408
58605
  }
58409
- rmSync3(file, { force: true });
58606
+ rmSync4(file, { force: true });
58410
58607
  return parseSetupMarker(text3);
58411
58608
  }
58412
58609
  function fmtSeconds(ms) {
@@ -58499,7 +58696,7 @@ function buildAttachmentCopyCommands(sources, platform = process.platform) {
58499
58696
  ];
58500
58697
  }
58501
58698
  async function clearAttachments(workspacePath, repoPath) {
58502
- rmSync3(join21(workspacePath, ATTACHMENTS_DIR), { recursive: true, force: true });
58699
+ rmSync4(join23(workspacePath, ATTACHMENTS_DIR), { recursive: true, force: true });
58503
58700
  await unexcludePath(repoPath, ATTACHMENTS_PREFIX);
58504
58701
  }
58505
58702
  function buildVerifyNotes(config2) {
@@ -58521,7 +58718,7 @@ function buildVerifyNotes(config2) {
58521
58718
  `);
58522
58719
  }
58523
58720
  function readRepoToolchain(repoPath) {
58524
- const pkgPath = join21(repoPath, "package.json");
58721
+ const pkgPath = join23(repoPath, "package.json");
58525
58722
  const hasPackageJson = existsSync18(pkgPath);
58526
58723
  let packageManagerField;
58527
58724
  if (hasPackageJson) {
@@ -58535,10 +58732,10 @@ function readRepoToolchain(repoPath) {
58535
58732
  hasPackageJson,
58536
58733
  ...packageManagerField !== undefined ? { packageManagerField } : {},
58537
58734
  lockfiles: {
58538
- bun: existsSync18(join21(repoPath, "bun.lock")) || existsSync18(join21(repoPath, "bun.lockb")),
58539
- pnpm: existsSync18(join21(repoPath, "pnpm-lock.yaml")),
58540
- yarn: existsSync18(join21(repoPath, "yarn.lock")),
58541
- npm: existsSync18(join21(repoPath, "package-lock.json"))
58735
+ bun: existsSync18(join23(repoPath, "bun.lock")) || existsSync18(join23(repoPath, "bun.lockb")),
58736
+ pnpm: existsSync18(join23(repoPath, "pnpm-lock.yaml")),
58737
+ yarn: existsSync18(join23(repoPath, "yarn.lock")),
58738
+ npm: existsSync18(join23(repoPath, "package-lock.json"))
58542
58739
  }
58543
58740
  };
58544
58741
  }
@@ -59292,16 +59489,16 @@ function resolverTemplatePath() {
59292
59489
  }
59293
59490
  function burnerAssetPath(file) {
59294
59491
  const here = dirname12(fileURLToPath6(import.meta.url));
59295
- return join21(resolveSkillsRoot(here), "burner", file);
59492
+ return join23(resolveSkillsRoot(here), "burner", file);
59296
59493
  }
59297
59494
  function listFeatureDocs(docsDir, prefix = "") {
59298
59495
  const out = [];
59299
- for (const e of readdirSync6(docsDir, { withFileTypes: true })) {
59496
+ for (const e of readdirSync7(docsDir, { withFileTypes: true })) {
59300
59497
  const rel = prefix ? `${prefix}/${e.name}` : e.name;
59301
59498
  if (e.isFile() && e.name.endsWith(".md"))
59302
59499
  out.push(rel);
59303
59500
  else if (e.isDirectory() && !prefix)
59304
- out.push(...listFeatureDocs(join21(docsDir, e.name), rel));
59501
+ out.push(...listFeatureDocs(join23(docsDir, e.name), rel));
59305
59502
  }
59306
59503
  return out;
59307
59504
  }
@@ -59318,7 +59515,7 @@ function readDocsDigest(projectId, slug) {
59318
59515
  if (!existsSync18(worktree)) {
59319
59516
  return done10("_No talk worktree on disk \u2014 docs digest skipped._", "no-worktree");
59320
59517
  }
59321
- const docsDir = join21(worktree, ...docsRel.split("/"));
59518
+ const docsDir = join23(worktree, ...docsRel.split("/"));
59322
59519
  if (!existsSync18(docsDir)) {
59323
59520
  return done10("_No docs/features dir in the talk worktree \u2014 docs digest skipped._", "no-docs-dir");
59324
59521
  }
@@ -59335,7 +59532,7 @@ function readDocsDigest(projectId, slug) {
59335
59532
  }
59336
59533
  let content;
59337
59534
  try {
59338
- content = readFileSync12(join21(docsDir, ...name.split("/")), "utf8");
59535
+ content = readFileSync12(join23(docsDir, ...name.split("/")), "utf8");
59339
59536
  } catch {
59340
59537
  continue;
59341
59538
  }
@@ -59382,7 +59579,7 @@ function readAgentFile(dirs, file) {
59382
59579
  for (const dir of dirs) {
59383
59580
  if (!dir)
59384
59581
  continue;
59385
- const p = join21(dir, file);
59582
+ const p = join23(dir, file);
59386
59583
  if (existsSync18(p)) {
59387
59584
  try {
59388
59585
  return readFileSync12(p, "utf8");
@@ -59562,7 +59759,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land2, ensureIsola
59562
59759
  const slotStamp = buildSlotStamp(resolveSandboxImage(config2, project3), toolchain.packageManagerField);
59563
59760
  const setupHookFor = (branch) => withPrelude(slot !== undefined ? buildSlotSetupCommand(slot, branch, setupCommand, pm, slotStamp) : workspaceMode === "isolated" ? buildIsolatedSetupCommand(branch, setupCommand, pm) : setupCommand);
59564
59761
  mkdirSync11(logsDir(), { recursive: true });
59565
- const logFilePath = join21(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
59762
+ const logFilePath = join23(logsDir(), `burn-${feature.id}-${ticket.seq}.log`);
59566
59763
  const throttle = createStreamThrottle((e) => ctx.emitEvent({ ...e, ticketId: ticket.id }));
59567
59764
  beginTranscript(ticket.id);
59568
59765
  const timer = createToolTimer(model.runtime);
@@ -59646,7 +59843,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land2, ensureIsola
59646
59843
  };
59647
59844
  };
59648
59845
  const discards = new Map;
59649
- const discardWorktree = (branch) => {
59846
+ const discardWorktree2 = (branch) => {
59650
59847
  const pending3 = discards.get(branch) ?? cleanupBurnWorktree(project3.repoPath, branch);
59651
59848
  discards.set(branch, pending3);
59652
59849
  return pending3;
@@ -59701,7 +59898,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land2, ensureIsola
59701
59898
  logging: { type: "file", path: logFilePath, onAgentStreamEvent: onStreamEvent }
59702
59899
  });
59703
59900
  } catch (err) {
59704
- await discardWorktree(resolveBranch);
59901
+ await discardWorktree2(resolveBranch);
59705
59902
  if (ctx.signal.aborted)
59706
59903
  throw err;
59707
59904
  const tip = await bestTip();
@@ -59715,7 +59912,7 @@ async function burnTicket(ctx, ticket, config2, token, model, land2, ensureIsola
59715
59912
  }
59716
59913
  }
59717
59914
  const blocked3 = readAgentFile([result?.preservedWorktreePath, project3.repoPath], "BLOCKED.md");
59718
- await discardWorktree(resolveBranch);
59915
+ await discardWorktree2(resolveBranch);
59719
59916
  if (blocked3 !== undefined && blocked3.trim().length > 0) {
59720
59917
  return {
59721
59918
  ok: false,
@@ -59849,7 +60046,7 @@ ${retryNotes}` : basePrompt,
59849
60046
  break;
59850
60047
  } catch (err) {
59851
60048
  await clearAttachmentsFor(tempBranch);
59852
- await discardWorktree(tempBranch);
60049
+ await discardWorktree2(tempBranch);
59853
60050
  if (ctx.signal.aborted)
59854
60051
  throw err;
59855
60052
  const msg = err instanceof Error ? err.message : String(err);
@@ -59864,7 +60061,7 @@ ${retryNotes}` : basePrompt,
59864
60061
  return stoppedOutcome(salvaged.length);
59865
60062
  }
59866
60063
  if (isWorktreeTeardownError(err)) {
59867
- const removed = await discardWorktree(tempBranch);
60064
+ const removed = await discardWorktree2(tempBranch);
59868
60065
  if (salvaged.length > 0) {
59869
60066
  ctx.emitEvent({
59870
60067
  type: "burn.worktree.teardown-failed",
@@ -59937,7 +60134,7 @@ ${retryNotes}` : basePrompt,
59937
60134
  const blocked3 = readAgentFile(agentFileDirs, "BLOCKED.md");
59938
60135
  emitUnrunnableGates(ctx, ticket, readAgentFile(agentFileDirs, "GATE_UNRUNNABLE.md"));
59939
60136
  harvestedDigest = harvestDigest(agentFileDirs);
59940
- await discardWorktree(tempBranch);
60137
+ await discardWorktree2(tempBranch);
59941
60138
  const chain = await branchCommitsAhead(project3.repoPath, feature.branch, tempBranch);
59942
60139
  let outcome;
59943
60140
  if (result.commits.length === 0 && blocked3 !== undefined && blocked3.trim().length > 0) {
@@ -60235,7 +60432,7 @@ var init_ticket_burner = __esm(() => {
60235
60432
 
60236
60433
  // src/workflows/research.ts
60237
60434
  import { mkdirSync as mkdirSync12, readFileSync as readFileSync13 } from "fs";
60238
- import { dirname as dirname13, join as join23 } from "path";
60435
+ import { dirname as dirname13, join as join25 } from "path";
60239
60436
  import { fileURLToPath as fileURLToPath7 } from "url";
60240
60437
  function waypointSlug(waypoint) {
60241
60438
  const base = waypoint.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -60304,7 +60501,7 @@ function interpretResearchResult(result, docRelPath) {
60304
60501
  }
60305
60502
  function researchTemplatePath() {
60306
60503
  const here = dirname13(fileURLToPath7(import.meta.url));
60307
- return join23(resolveSkillsRoot(here), "burner", "research-waypoint.md");
60504
+ return join25(resolveSkillsRoot(here), "burner", "research-waypoint.md");
60308
60505
  }
60309
60506
  function researchContainerName(runId, seq2) {
60310
60507
  return `runcastle-${runId}-w${seq2}`;
@@ -60328,7 +60525,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
60328
60525
  RESEARCH_DOC_PATH: docRelPath
60329
60526
  });
60330
60527
  mkdirSync12(logsDir(), { recursive: true });
60331
- const logFilePath = join23(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
60528
+ const logFilePath = join25(logsDir(), `research-${feature.id}-${waypoint.seq}.log`);
60332
60529
  const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
60333
60530
  const codexAuthMount = codexAuthMountFor(model.runtime, config2.sandbox);
60334
60531
  const runOptions = {
@@ -60591,29 +60788,43 @@ var init_runner = __esm(() => {
60591
60788
 
60592
60789
  // src/services/features.ts
60593
60790
  import { desc as desc4, eq as eq11 } from "drizzle-orm";
60594
- import { rmSync as rmSync4 } from "fs";
60791
+ import { rmSync as rmSync5 } from "fs";
60595
60792
  async function createFeature(ctx, input) {
60596
60793
  const project3 = requireProjectById(ctx, input.projectId);
60597
60794
  const slug = uniqueSlug(ctx, project3.id, input.title);
60598
60795
  const branch = `feature/${slug}`;
60599
- const cut = input.draft ? null : await ensureFeatureBranch(ctx, project3, slug, await requestedBase(ctx, project3, input.baseBranch));
60796
+ const { cut, baseBranch, status: status3 } = await parkOrCut(ctx, project3, slug, input);
60600
60797
  const row = {
60601
60798
  id: newId("feat"),
60602
60799
  projectId: project3.id,
60603
60800
  slug,
60604
60801
  title: input.title,
60605
60802
  oneLiner: input.oneLiner,
60606
- brief: input.draft ? input.brief ?? null : null,
60803
+ brief: cut ? null : input.brief ?? null,
60607
60804
  mapped: false,
60608
60805
  lap: 1,
60609
60806
  phase: "planning",
60610
60807
  branch,
60611
- baseBranch: cut?.baseBranch ?? null,
60612
- status: cut ? "active" : "draft",
60808
+ baseBranch,
60809
+ status: status3,
60613
60810
  createdAt: Date.now()
60614
60811
  };
60615
60812
  const inserted = ctx.db.insert(features).values(row).returning().get();
60616
60813
  const feature = rowToFeature(inserted);
60814
+ emitFeatureCreated(ctx, feature, cut);
60815
+ if (!cut)
60816
+ return feature;
60817
+ await scaffoldDocsOnFeatureBranch(ctx, project3, feature, { brief: input.brief });
60818
+ return feature;
60819
+ }
60820
+ async function parkOrCut(ctx, project3, slug, input) {
60821
+ if (input.draft)
60822
+ return { cut: null, baseBranch: null, status: "draft" };
60823
+ const cut = await ensureFeatureBranch(ctx, project3, slug, await requestedBase(ctx, project3, input.baseBranch));
60824
+ return { cut, baseBranch: cut.baseBranch, status: "active" };
60825
+ }
60826
+ function emitFeatureCreated(ctx, feature, cut) {
60827
+ const { slug, branch } = feature;
60617
60828
  emit(ctx, feature.id, {
60618
60829
  type: "feature.created",
60619
60830
  message: !cut ? `feature.created (draft \u2014 ${branch} not cut yet)` : cut.branchReady ? `feature.created (${branch} \u2190 ${cut.baseBranch})` : "feature.created (branch pending)",
@@ -60625,10 +60836,6 @@ async function createFeature(ctx, input) {
60625
60836
  draft: !cut
60626
60837
  }
60627
60838
  });
60628
- if (!cut)
60629
- return feature;
60630
- await scaffoldDocsOnFeatureBranch(ctx, project3, feature, { brief: input.brief });
60631
- return feature;
60632
60839
  }
60633
60840
  function requireNotDraft(feature) {
60634
60841
  if (feature.status === "draft") {
@@ -60697,8 +60904,8 @@ async function quickChange(ctx, input) {
60697
60904
  throw new InvalidInputError("a quick change needs a sentence describing the change");
60698
60905
  const slug = uniqueSlug(ctx, project3.id, title);
60699
60906
  const branch = `feature/${slug}`;
60700
- const base = await requestedBase(ctx, project3, input.baseBranch);
60701
- const { branchReady, baseBranch } = await ensureFeatureBranch(ctx, project3, slug, base);
60907
+ const brief = quickBrief(title, proses);
60908
+ const { cut, baseBranch, status: status3 } = await parkOrCut(ctx, project3, slug, input);
60702
60909
  const inserted = ctx.db.insert(features).values({
60703
60910
  id: newId("feat"),
60704
60911
  projectId: project3.id,
@@ -60706,22 +60913,20 @@ async function quickChange(ctx, input) {
60706
60913
  title,
60707
60914
  oneLiner: proses[0].split(`
60708
60915
  `)[0].trim(),
60916
+ brief: cut ? null : brief,
60709
60917
  mapped: false,
60710
60918
  lap: 1,
60711
60919
  ticketsReadyLap: 1,
60712
60920
  phase: "planning",
60713
60921
  branch,
60714
60922
  baseBranch,
60715
- status: "active",
60923
+ status: status3,
60716
60924
  createdAt: Date.now()
60717
60925
  }).returning().get();
60718
60926
  const feature = rowToFeature(inserted);
60719
- emit(ctx, feature.id, {
60720
- type: "feature.created",
60721
- message: branchReady ? `feature.created (${branch} \u2190 ${baseBranch})` : "feature.created (branch pending)",
60722
- data: { slug, branch, baseBranch, branchReady }
60723
- });
60724
- await scaffoldDocsOnFeatureBranch(ctx, project3, feature, { brief: quickBrief(title, proses) });
60927
+ emitFeatureCreated(ctx, feature, cut);
60928
+ if (cut)
60929
+ await scaffoldDocsOnFeatureBranch(ctx, project3, feature, { brief });
60725
60930
  const stored = storeTickets(ctx, feature.id, [
60726
60931
  ...proses.map((prose) => ({
60727
60932
  title: quickTicketTitle(prose),
@@ -61012,7 +61217,7 @@ function escalateToMap(ctx, featureId, input) {
61012
61217
  });
61013
61218
  return { ok: true };
61014
61219
  }
61015
- function archiveFeature(ctx, featureId) {
61220
+ async function archiveFeature(ctx, featureId) {
61016
61221
  const feature = getFeatureRow(ctx, featureId);
61017
61222
  requireNotDraft(feature);
61018
61223
  if (feature.status === "archived") {
@@ -61027,6 +61232,7 @@ function archiveFeature(ctx, featureId) {
61027
61232
  message: `feature ${feature.slug} archived`,
61028
61233
  data: { from: feature.status }
61029
61234
  });
61235
+ await retireArchivedWorktree(ctx, projectForFeature(ctx, feature), feature);
61030
61236
  return { ...feature, status: "archived" };
61031
61237
  }
61032
61238
  function unarchiveFeature(ctx, featureId) {
@@ -61068,7 +61274,7 @@ async function deleteFeature(ctx, featureId) {
61068
61274
  });
61069
61275
  for (const s of listSessionsByFeature(ctx, featureId)) {
61070
61276
  try {
61071
- rmSync4(sessionDir(s.id), { recursive: true, force: true });
61277
+ rmSync5(sessionDir(s.id), { recursive: true, force: true });
61072
61278
  } catch {}
61073
61279
  }
61074
61280
  deleteFeatureRows(ctx, featureId);
@@ -61103,6 +61309,7 @@ var init_features = __esm(() => {
61103
61309
  init_schema();
61104
61310
  init_errors();
61105
61311
  init_events();
61312
+ init_feature_worktrees();
61106
61313
  init_git();
61107
61314
  init_knowledge();
61108
61315
  init_repo();
@@ -61163,7 +61370,7 @@ var init_burn_warnings = __esm(() => {
61163
61370
  });
61164
61371
 
61165
61372
  // src/services/project-notes.ts
61166
- import { existsSync as existsSync21, mkdirSync as mkdirSync13, rmSync as rmSync5, writeFileSync as writeFileSync9 } from "fs";
61373
+ import { existsSync as existsSync21, mkdirSync as mkdirSync13, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
61167
61374
  import { and as and9, count as count3, desc as desc5, eq as eq12, inArray as inArray4 } from "drizzle-orm";
61168
61375
  function rowToNote(row) {
61169
61376
  return ProjectNote.parse({
@@ -61228,7 +61435,7 @@ function deleteNote(ctx, noteId) {
61228
61435
  const note2 = getNote(ctx, noteId);
61229
61436
  requireOpen(note2, "delete");
61230
61437
  ctx.db.delete(projectNotes).where(eq12(projectNotes.id, noteId)).run();
61231
- rmSync5(projectNotePath(noteId), { force: true });
61438
+ rmSync6(projectNotePath(noteId), { force: true });
61232
61439
  emitProject(ctx, note2.projectId, { type: "project_note.deleted", message: "project note deleted", data: { noteId } });
61233
61440
  }
61234
61441
  function triage(ctx, noteIds, outcome, featureId, verb = "triaged") {
@@ -61353,7 +61560,7 @@ var init_runs = __esm(() => {
61353
61560
  });
61354
61561
 
61355
61562
  // src/services/test-notes.ts
61356
- import { existsSync as existsSync22, mkdirSync as mkdirSync14, rmSync as rmSync6, writeFileSync as writeFileSync10 } from "fs";
61563
+ import { existsSync as existsSync22, mkdirSync as mkdirSync14, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
61357
61564
  import { dirname as dirname14 } from "path";
61358
61565
  import { and as and11, asc as asc6, eq as eq14, lt } from "drizzle-orm";
61359
61566
  function rowToNote2(row) {
@@ -61442,7 +61649,7 @@ function deleteNote2(ctx, noteId) {
61442
61649
  const current2 = getNote2(ctx, noteId);
61443
61650
  assertOpen(current2, "delete");
61444
61651
  ctx.db.delete(testNotes).where(eq14(testNotes.id, noteId)).run();
61445
- rmSync6(annotationPath(noteId), { force: true });
61652
+ rmSync7(annotationPath(noteId), { force: true });
61446
61653
  emit(ctx, current2.featureId, {
61447
61654
  type: "note.deleted",
61448
61655
  message: "note deleted",
@@ -61643,7 +61850,7 @@ var MCP_READ_CEILING_CHARS = 60000;
61643
61850
 
61644
61851
  // src/mcp/server.ts
61645
61852
  import { existsSync as existsSync23, readFileSync as readFileSync14 } from "fs";
61646
- import { join as join25, relative as relative4, resolve as resolve10, sep as sep2 } from "path";
61853
+ import { join as join26, relative as relative4, resolve as resolve10, sep as sep2 } from "path";
61647
61854
  import { StreamableHTTPTransport } from "@hono/mcp";
61648
61855
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
61649
61856
  import { Hono } from "hono";
@@ -61726,7 +61933,7 @@ function featureContext(ctx, reader) {
61726
61933
  doc: { relPath: summary.relPath, content },
61727
61934
  ref: {
61728
61935
  relPath: summary.relPath,
61729
- absPath: join25(docsDir, summary.relPath),
61936
+ absPath: join26(docsDir, summary.relPath),
61730
61937
  bytes: Buffer.byteLength(content, "utf8"),
61731
61938
  reason: notInlinedReason(summary.relPath)
61732
61939
  }
@@ -62140,7 +62347,8 @@ async function toolCreateFeature(ctx, session, input) {
62140
62347
  projectId: project3.id,
62141
62348
  title: input.title,
62142
62349
  tickets: input.tickets,
62143
- baseBranch: input.baseBranch
62350
+ baseBranch: input.baseBranch,
62351
+ draft: input.draft
62144
62352
  }) : await createFeature(ctx, {
62145
62353
  projectId: project3.id,
62146
62354
  title: input.title,
@@ -62218,7 +62426,7 @@ function adrRef(adr) {
62218
62426
  }
62219
62427
  function toolReadAdr(ctx, session, input) {
62220
62428
  requireProject(ctx, session);
62221
- const root = resolve10(join25(session.worktreePath, ADR_DIR_REL));
62429
+ const root = resolve10(join26(session.worktreePath, ADR_DIR_REL));
62222
62430
  const name = input.relPath.replace(/\\/g, "/").replace(/^\.?\/*docs\/adr\//i, "");
62223
62431
  const target = resolve10(root, name);
62224
62432
  const rel = relative4(root, target);
@@ -62247,7 +62455,7 @@ function toolReadFeatureBrief(ctx, session, input) {
62247
62455
  const feature = list(ctx, project3.id).find((f) => f.slug === input.slug);
62248
62456
  if (!feature)
62249
62457
  throw new NotFoundError(`feature not found: ${input.slug}`);
62250
- const onDisk = join25(session.worktreePath, featureDocsRel(feature.slug), "brief.md");
62458
+ const onDisk = join26(session.worktreePath, featureDocsRel(feature.slug), "brief.md");
62251
62459
  const brief = feature.status === "shipped" && existsSync23(onDisk) ? readFileSync14(onDisk, "utf8") : feature.brief;
62252
62460
  return {
62253
62461
  slug: feature.slug,
@@ -62473,7 +62681,7 @@ function buildMcpServer(audience) {
62473
62681
  oneLiner: z5.string().optional().describe("One line saying what this feature is. Required for every shape EXCEPT `tickets`, " + "where the first ticket\u2019s prose becomes the one-liner and anything sent here is " + "ignored."),
62474
62682
  baseBranch: z5.string().optional().describe("Branch to cut from \u2014 always pass it explicitly, naming the base you told the " + "human you would use (see `get_project_context`\u2019s `baseBranches`). Omitted, it " + "falls back to whatever the project checkout happens to be on. Drafts pass none: " + "a parked feature picks its base at Start."),
62475
62683
  brief: z5.string().optional().describe("The reasoning you just worked out with the human \u2014 why this feature exists and " + "what it must NOT swallow. Becomes brief.md verbatim, and without it that " + "reasoning is lost. Unused by the `tickets` shape, whose brief IS the proses."),
62476
- draft: z5.boolean().optional().describe("PARK it instead of starting it: a row and its brief, no branch and no files, " + "until the human clicks Start. Ask them per feature whether to start or park."),
62684
+ draft: z5.boolean().optional().describe("PARK it instead of starting it: a row and its brief, no branch and no files, " + "until the human clicks Start. With `tickets`, the draft keeps them and Start lands " + "it ready to Burn. Ask them per feature whether to start or park."),
62477
62685
  tickets: z5.array(z5.string().min(1)).min(1).optional().describe("The quick-change shape: work too small to deserve a grill. Creates ONE feature at " + "the implementation phase carrying a ticket per prose (plus the review ticket " + "every batch closes with). Send every sentence of one quick change in a SINGLE " + "call \u2014 calling this once per ticket makes one feature per ticket.")
62478
62686
  }
62479
62687
  }, async (args2, extra) => {
@@ -62995,7 +63203,7 @@ var init_ws = __esm(() => {
62995
63203
  // src/services/prep.ts
62996
63204
  import { and as and12, desc as desc6, eq as eq15, inArray as inArray6 } from "drizzle-orm";
62997
63205
  import { readdir as readdir3 } from "fs/promises";
62998
- import { join as join26 } from "path";
63206
+ import { join as join27 } from "path";
62999
63207
  function keysToPrepare(ctx, project3) {
63000
63208
  return unsetPreparedKeys(project3).filter((key) => isOverwritable(ctx, project3.id, key));
63001
63209
  }
@@ -63022,7 +63230,7 @@ async function isEmptyRepo(repoPath) {
63022
63230
  return false;
63023
63231
  if (!rootEntries.includes("docs"))
63024
63232
  return true;
63025
- const docsEntries = await readdir3(join26(repoPath, "docs"));
63233
+ const docsEntries = await readdir3(join27(repoPath, "docs"));
63026
63234
  return docsEntries.every((entry) => SCAFFOLDED_DOCS_DIRS.includes(entry));
63027
63235
  }
63028
63236
  async function prepView(ctx, project3) {
@@ -63508,7 +63716,7 @@ var init_stream = __esm(() => {
63508
63716
  // src/routes/web.ts
63509
63717
  import { existsSync as existsSync24, statSync as statSync6 } from "fs";
63510
63718
  import { readFile as readFile5 } from "fs/promises";
63511
- import { extname, join as join27, normalize as normalize2, resolve as resolve11, sep as sep3 } from "path";
63719
+ import { extname, join as join28, normalize as normalize2, resolve as resolve11, sep as sep3 } from "path";
63512
63720
  function contentType(path2) {
63513
63721
  return MIME[extname(path2).toLowerCase()] ?? "application/octet-stream";
63514
63722
  }
@@ -63526,7 +63734,7 @@ function safeJoin(root, pathname) {
63526
63734
  } catch {
63527
63735
  return null;
63528
63736
  }
63529
- const joined = normalize2(join27(root, decoded));
63737
+ const joined = normalize2(join28(root, decoded));
63530
63738
  if (joined !== root && !joined.startsWith(root + sep3))
63531
63739
  return null;
63532
63740
  return joined;
@@ -63539,7 +63747,7 @@ function resolveWebDist() {
63539
63747
  }
63540
63748
  function mountWebApp(app, distDir) {
63541
63749
  const root = resolve11(distDir);
63542
- const indexHtml = join27(root, "index.html");
63750
+ const indexHtml = join28(root, "index.html");
63543
63751
  app.get("*", async (c, next4) => {
63544
63752
  const { pathname } = new URL(c.req.url);
63545
63753
  if (/^\/(api|mcp|ws|health)(\/|$)/.test(pathname))
@@ -63559,7 +63767,7 @@ function mountWebApp(app, distDir) {
63559
63767
  }
63560
63768
  function mountWebAppIfBuilt(app) {
63561
63769
  const dist = resolveWebDist();
63562
- if (existsSync24(join27(dist, "index.html")))
63770
+ if (existsSync24(join28(dist, "index.html")))
63563
63771
  mountWebApp(app, dist);
63564
63772
  }
63565
63773
  var MIME;
@@ -63587,13 +63795,13 @@ var init_web = __esm(() => {
63587
63795
 
63588
63796
  // src/version.ts
63589
63797
  import { readFileSync as readFileSync15 } from "fs";
63590
- import { dirname as dirname15, join as join28 } from "path";
63798
+ import { dirname as dirname15, join as join29 } from "path";
63591
63799
  import { fileURLToPath as fileURLToPath8 } from "url";
63592
63800
  function runcastleVersion() {
63593
63801
  if (cached4)
63594
63802
  return cached4;
63595
63803
  const here = dirname15(fileURLToPath8(import.meta.url));
63596
- for (const pkgPath of [join28(here, "..", "package.json"), join28(here, "package.json")]) {
63804
+ for (const pkgPath of [join29(here, "..", "package.json"), join29(here, "package.json")]) {
63597
63805
  try {
63598
63806
  const pkg = JSON.parse(readFileSync15(pkgPath, "utf8"));
63599
63807
  if (typeof pkg.version === "string") {
@@ -63651,6 +63859,10 @@ function compareSemver(a, b) {
63651
63859
  if (diff8 !== 0)
63652
63860
  return diff8;
63653
63861
  }
63862
+ const la = pa.pre[0] === LOCAL_BUILD_PRERELEASE;
63863
+ const lb = pb.pre[0] === LOCAL_BUILD_PRERELEASE;
63864
+ if (la !== lb)
63865
+ return la ? 1 : -1;
63654
63866
  if (pa.pre.length === 0 && pb.pre.length === 0)
63655
63867
  return 0;
63656
63868
  if (pa.pre.length === 0)
@@ -63732,7 +63944,7 @@ async function getUpdateInfo(current2, fetchImpl) {
63732
63944
  cache = info;
63733
63945
  return info;
63734
63946
  }
63735
- var PACKAGE_NAME = "runcastle", UPDATE_COMMAND, PING_URL = "https://ping.runcastle.dev/ping", NPM_LATEST_URL, NO_UPDATE = (current2) => ({
63947
+ var PACKAGE_NAME = "runcastle", UPDATE_COMMAND, PING_URL = "https://ping.runcastle.dev/ping", NPM_LATEST_URL, LOCAL_BUILD_PRERELEASE = "local", NO_UPDATE = (current2) => ({
63736
63948
  current: current2,
63737
63949
  latest: null,
63738
63950
  updateAvailable: false,
@@ -63821,7 +64033,7 @@ var init_events2 = __esm(() => {
63821
64033
 
63822
64034
  // src/launcher/launcher.ts
63823
64035
  import { existsSync as existsSync26 } from "fs";
63824
- import { join as join29 } from "path";
64036
+ import { join as join30 } from "path";
63825
64037
  import { and as and13, eq as eq16 } from "drizzle-orm";
63826
64038
  function ptyExitMessage(exitCode) {
63827
64039
  const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
@@ -64436,7 +64648,7 @@ async function buildPrepareBrief(ctx, project3) {
64436
64648
  };
64437
64649
  }
64438
64650
  function probePrepareHost(repoPath) {
64439
- const has4 = (rel) => existsSync26(join29(repoPath, rel));
64651
+ const has4 = (rel) => existsSync26(join30(repoPath, rel));
64440
64652
  const lock = LOCKFILES.find(([file]) => has4(file));
64441
64653
  return {
64442
64654
  platform: process.platform,
@@ -64589,7 +64801,7 @@ var init_launcher = __esm(() => {
64589
64801
 
64590
64802
  // src/services/outcome.ts
64591
64803
  import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync12 } from "fs";
64592
- import { join as join30 } from "path";
64804
+ import { join as join31 } from "path";
64593
64805
  function digestHasSubstance(text3) {
64594
64806
  return text3.split(`
64595
64807
  `).some((line) => {
@@ -64689,9 +64901,9 @@ async function promoteOutcomeDoc(ctx, project3, feature, target, delta) {
64689
64901
  delta: { commits: delta.commits ?? 0, files: delta.files ?? 0 }
64690
64902
  });
64691
64903
  await onBranch(project3.repoPath, target, async () => {
64692
- const docsDir = join30(project3.repoPath, ...featureDocsRel(feature.slug).split("/"));
64904
+ const docsDir = join31(project3.repoPath, ...featureDocsRel(feature.slug).split("/"));
64693
64905
  mkdirSync16(docsDir, { recursive: true });
64694
- writeFileSync12(join30(docsDir, "outcome.md"), doc, "utf8");
64906
+ writeFileSync12(join31(docsDir, "outcome.md"), doc, "utf8");
64695
64907
  await commitDocs(project3.repoPath, docsCommitMessage(`outcome for ${feature.slug}`, project3.docsCommitPrefix));
64696
64908
  });
64697
64909
  } catch (error) {
@@ -64720,6 +64932,7 @@ var init_feature = __esm(() => {
64720
64932
  init_launcher();
64721
64933
  init_burn_warnings();
64722
64934
  init_events();
64935
+ init_feature_worktrees();
64723
64936
  init_features();
64724
64937
  init_git();
64725
64938
  init_outcome();
@@ -64799,6 +65012,7 @@ var init_feature = __esm(() => {
64799
65012
  await promoteOutcomeDoc(ctx, project3, feature, res.target, delta);
64800
65013
  setPhase(ctx, input.featureId, "shipped", "feature.shipped", `merged to ${res.target}`);
64801
65014
  setFeatureStatus(ctx, input.featureId, "shipped");
65015
+ await retireShippedWorktree(ctx, project3, feature);
64802
65016
  } else {
64803
65017
  emit(ctx, input.featureId, {
64804
65018
  type: "merge.conflict",
@@ -64812,9 +65026,9 @@ var init_feature = __esm(() => {
64812
65026
  });
64813
65027
 
64814
65028
  // src/services/fsbrowse.ts
64815
- import { existsSync as existsSync27, readdirSync as readdirSync7, statSync as statSync7 } from "fs";
65029
+ import { existsSync as existsSync27, readdirSync as readdirSync8, statSync as statSync7 } from "fs";
64816
65030
  import { homedir as homedir4 } from "os";
64817
- import { basename as basename4, dirname as dirname17, isAbsolute as isAbsolute3, join as join31, resolve as resolve12, sep as sep4 } from "path";
65031
+ import { basename as basename4, dirname as dirname17, isAbsolute as isAbsolute3, join as join33, resolve as resolve12, sep as sep4 } from "path";
64818
65032
  function expandPath(input, home = homedir4()) {
64819
65033
  const trimmed = input.trim();
64820
65034
  if (trimmed === "")
@@ -64823,12 +65037,12 @@ function expandPath(input, home = homedir4()) {
64823
65037
  if (out === "~")
64824
65038
  out = home;
64825
65039
  else if (out.startsWith("~/") || out.startsWith("~\\"))
64826
- out = join31(home, out.slice(2));
65040
+ out = join33(home, out.slice(2));
64827
65041
  return resolve12(out);
64828
65042
  }
64829
65043
  function looksLikeRepo(dir) {
64830
65044
  try {
64831
- return existsSync27(join31(dir, ".git"));
65045
+ return existsSync27(join33(dir, ".git"));
64832
65046
  } catch {
64833
65047
  return false;
64834
65048
  }
@@ -64850,7 +65064,7 @@ function listRoots(home = homedir4()) {
64850
65064
  roots3.push({ label: "Home", path: home, kind: "home" });
64851
65065
  const seen = new Set(roots3.map(pathKey));
64852
65066
  for (const name of COMMON_DIRS) {
64853
- const candidate = join31(home, name);
65067
+ const candidate = join33(home, name);
64854
65068
  const key = pathKey({ path: candidate });
64855
65069
  if (seen.has(key) || !existsSync27(candidate))
64856
65070
  continue;
@@ -64895,7 +65109,7 @@ function browseDir(input, showHidden = false, home = homedir4()) {
64895
65109
  }
64896
65110
  let dirents;
64897
65111
  try {
64898
- dirents = readdirSync7(dir, { withFileTypes: true, encoding: "utf8" });
65112
+ dirents = readdirSync8(dir, { withFileTypes: true, encoding: "utf8" });
64899
65113
  } catch (e) {
64900
65114
  throw new InvalidInputError(`cannot read directory: ${dir} (${errMsg2(e)})`);
64901
65115
  }
@@ -64911,7 +65125,7 @@ function browseDir(input, showHidden = false, home = homedir4()) {
64911
65125
  continue;
64912
65126
  if (!showHidden && name.startsWith("."))
64913
65127
  continue;
64914
- const path2 = join31(dir, name);
65128
+ const path2 = join33(dir, name);
64915
65129
  const isSymlink = dirent.isSymbolicLink();
64916
65130
  if (isSymlink && !showHidden)
64917
65131
  continue;
@@ -65153,7 +65367,7 @@ var init_conversations = __esm(() => {
65153
65367
 
65154
65368
  // src/services/projects.ts
65155
65369
  import { existsSync as existsSync28 } from "fs";
65156
- import { basename as basename5, join as join33 } from "path";
65370
+ import { basename as basename5, join as join35 } from "path";
65157
65371
  import { eq as eq17, isNull } from "drizzle-orm";
65158
65372
  function listProjects(ctx) {
65159
65373
  return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
@@ -65237,7 +65451,7 @@ async function assertRepoTolerant(ctx, repoPath) {
65237
65451
  } catch (e) {
65238
65452
  if (!isNotImplemented(e))
65239
65453
  throw e;
65240
- if (!existsSync28(join33(repoPath, ".git"))) {
65454
+ if (!existsSync28(join35(repoPath, ".git"))) {
65241
65455
  throw new InvalidInputError(`not a git repository: ${repoPath}`);
65242
65456
  }
65243
65457
  }
@@ -65441,7 +65655,7 @@ var init_project_image = __esm(() => {
65441
65655
  });
65442
65656
 
65443
65657
  // src/trpc/routers/setup.ts
65444
- import { join as join35 } from "path";
65658
+ import { join as join36 } from "path";
65445
65659
  import { isNotNull as isNotNull4 } from "drizzle-orm";
65446
65660
  import * as z14 from "zod";
65447
65661
  function projectImageEnv(ctx, project3) {
@@ -65462,7 +65676,7 @@ async function buildImageTerminal(ctx, projectId) {
65462
65676
  const notices = Object.values(host.problems);
65463
65677
  const stockFresh = imageFreshness({
65464
65678
  image: await inspectBuiltImage(exec3, runtime5, DEFAULT_SANDBOX_IMAGE),
65465
- expectedHash: hashDockerfile(join35(stockContext, "Dockerfile")),
65679
+ expectedHash: hashDockerfile(join36(stockContext, "Dockerfile")),
65466
65680
  host: host.versions
65467
65681
  }).fresh;
65468
65682
  const plan = planImageBuild({
@@ -65494,7 +65708,7 @@ async function buildImageTerminal(ctx, projectId) {
65494
65708
  }
65495
65709
  };
65496
65710
  }
65497
- var setupRouter;
65711
+ var DOCTOR_PROBE_TIMEOUT_MS = 1e4, setupRouter;
65498
65712
  var init_setup2 = __esm(() => {
65499
65713
  init_src();
65500
65714
  init_schema();
@@ -65520,7 +65734,7 @@ var init_setup2 = __esm(() => {
65520
65734
  const projectModels = ctx.db.select({ model: projects.model }).from(projects).where(isNotNull4(projects.model)).all().map((p) => p.model);
65521
65735
  const project3 = input?.projectId ? requireProjectById(ctx, input.projectId) : null;
65522
65736
  return runDoctor({
65523
- exec: createSystemExec(),
65737
+ exec: createSystemExec({ timeoutMs: DOCTOR_PROBE_TIMEOUT_MS }),
65524
65738
  burnerDockerfile: burnerDockerfilePath(),
65525
65739
  env: envWithAfkCredentials(),
65526
65740
  runtimes: configuredRuntimes(rosterConfig(ctx), projectModels),
@@ -65841,6 +66055,11 @@ async function startServer() {
65841
66055
  if (staleRuns.length > 0) {
65842
66056
  console.log(`reconciled ${staleRuns.length} stale run(s) from a previous server run`);
65843
66057
  }
66058
+ sweepFeatureWorktrees(ctx).then((s) => {
66059
+ if (s.removed + s.orphanProjects + s.failed > 0) {
66060
+ console.log(`worktree sweep: removed ${s.removed} feature worktree(s) and ` + `${s.orphanProjects} orphaned project dir(s); kept ${s.kept} dirty, ${s.failed} failed`);
66061
+ }
66062
+ });
65844
66063
  const server = Bun.serve({
65845
66064
  port: config2.serverPort,
65846
66065
  hostname: SERVE_HOSTNAME,
@@ -65883,6 +66102,7 @@ var init_src2 = __esm(() => {
65883
66102
  init_reviews();
65884
66103
  init_stream();
65885
66104
  init_web();
66105
+ init_feature_worktrees();
65886
66106
  init_model_discovery();
65887
66107
  init_settings();
65888
66108
  init_update_check();