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